tidy up a load of lintian warnings
[freeradius.git] / src / modules / rlm_cache / rlm_cache.c
index 2d1d2c0..e9572bc 100644 (file)
@@ -1,7 +1,8 @@
 /*
  *   This program is is free software; you can redistribute it and/or modify
- *   it under the terms of the GNU General Public License, version 2 if the
- *   License as published by the Free Software Foundation.
+ *   it under the terms of the GNU General Public License as published by
+ *   the Free Software Foundation; either version 2 of the License, or (at
+ *   your option) any later version.
  *
  *   This program is distributed in the hope that it will be useful,
  *   but WITHOUT ANY WARRANTY; without even the implied warranty of
  * @file rlm_cache.c
  * @brief Cache values and merge them back into future requests.
  *
- * @copyright 2012-201 The FreeRADIUS server project
+ * @copyright 2012-2014 The FreeRADIUS server project
  */
 RCSID("$Id$")
 
 #include <freeradius-devel/radiusd.h>
 #include <freeradius-devel/modules.h>
-#include <freeradius-devel/heap.h>
+#include <freeradius-devel/modpriv.h>
+#include <freeradius-devel/modcall.h>
 #include <freeradius-devel/rad_assert.h>
 
-#define PW_CACHE_TTL           1140
-#define PW_CACHE_STATUS_ONLY   1141
-#define PW_CACHE_MERGE         1142
-#define PW_CACHE_ENTRY_HITS    1143
+#include "rlm_cache.h"
 
 /*
- *     Define a structure for our module configuration.
+ *     A mapping of configuration file names to internal variables.
  *
- *     These variables do not need to be in a structure, but it's
- *     a lot cleaner to do so, and a pointer to the structure can
- *     be used as the instance handle.
+ *     Note that the string is dynamically allocated, so it MUST
+ *     be freed.  When the configuration file parse re-reads the string,
+ *     it free's the old one, and strdup's the new one, placing the pointer
+ *     to the strdup'd string into 'config.string'.  This gets around
+ *     buffer over-flows.
  */
-typedef struct rlm_cache_t {
-       char const              *xlat_name;
-       char                    *key;
-       int                     ttl;
-       int                     max_entries;
-       int                     epoch;
-       int                     stats;
-       CONF_SECTION            *cs;
-       rbtree_t                *cache;
-       fr_heap_t               *heap;
-
-       value_pair_map_t        *maps;  //!< Attribute map applied to users
-                                       //!< and profiles.
-#ifdef HAVE_PTHREAD_H
-       pthread_mutex_t cache_mutex;
-#endif
-} rlm_cache_t;
-
-typedef struct rlm_cache_entry_t {
-       char const      *key;
-       int             offset;
-       long long int   hits;
-       time_t          created;
-       time_t          expires;
-       VALUE_PAIR      *control;
-       VALUE_PAIR      *packet;
-       VALUE_PAIR      *reply;
-} rlm_cache_entry_t;
-
-#ifdef HAVE_PTHREAD_H
-#define PTHREAD_MUTEX_LOCK pthread_mutex_lock
-#define PTHREAD_MUTEX_UNLOCK pthread_mutex_unlock
-#else
-#define PTHREAD_MUTEX_LOCK(_x)
-#define PTHREAD_MUTEX_UNLOCK(_x)
-#endif
-
-#define MAX_ATTRMAP    128
+static const CONF_PARSER module_config[] = {
+       { "driver", FR_CONF_OFFSET(PW_TYPE_STRING, rlm_cache_t, driver_name), "rlm_cache_rbtree" },
+       { "key", FR_CONF_OFFSET(PW_TYPE_STRING | PW_TYPE_REQUIRED | PW_TYPE_XLAT, rlm_cache_t, key), NULL },
+       { "ttl", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_cache_t, ttl), "500" },
+       { "max_entries", FR_CONF_OFFSET(PW_TYPE_INTEGER, rlm_cache_t, max_entries), "0" },
+
+       /* Should be a type which matches time_t, @fixme before 2038 */
+       { "epoch", FR_CONF_OFFSET(PW_TYPE_SIGNED, rlm_cache_t, epoch), "0" },
+       { "add_stats", FR_CONF_OFFSET(PW_TYPE_BOOLEAN, rlm_cache_t, stats), "no" },
+       CONF_PARSER_TERMINATOR
+};
 
-/*
- *     Compare two entries by key.  There may only be one entry with
- *     the same key.
- */
-static int cache_entry_cmp(void const *one, void const *two)
+static int cache_acquire(rlm_cache_handle_t **out, rlm_cache_t *inst, REQUEST *request)
 {
-       rlm_cache_entry_t const *a = one;
-       rlm_cache_entry_t const *b = two;
+       if (!inst->module->acquire) return 0;
 
-       return strcmp(a->key, b->key);
+       return inst->module->acquire(out, inst, request);
 }
 
-static void cache_entry_free(void *data)
+static void cache_release(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle)
 {
-       rlm_cache_entry_t *c = data;
+       if (!inst->module->release) return;
+       if (!handle || !*handle) return;
+
+       inst->module->release(inst, request, handle);
+}
 
-       pairfree(&c->control);
-       pairfree(&c->packet);
-       pairfree(&c->reply);
+static int cache_reconnect(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle)
+{
+       rad_assert(inst->module->reconnect);
 
-       talloc_free(c);
+       return inst->module->reconnect(inst, request, handle);
 }
 
-/*
- *     Compare two entries by expiry time.  There may be multiple
- *     entries with the same expiry time.
+/** Allocate a cache entry
+ *
+ *  This is used so that drivers may use their own allocation functions
+ *  to allocate structures larger than the normal rlm_cache_entry_t.
+ *
+ *  If the driver doesn't specify a custom allocation function, the cache
+ *  entry is talloced in the NULL ctx.
  */
-static int cache_heap_cmp(void const *one, void const *two)
+static rlm_cache_entry_t *cache_alloc(rlm_cache_t *inst, REQUEST *request)
 {
-       rlm_cache_entry_t const *a = one;
-       rlm_cache_entry_t const *b = two;
+       if (inst->module->alloc) return inst->module->alloc(inst, request);
 
-       if (a->expires < b->expires) return -1;
-       if (a->expires > b->expires) return +1;
+       return talloc_zero(NULL, rlm_cache_entry_t);
+}
 
-       return 0;
+/** Free memory associated with a cache entry
+ *
+ * This does not necessarily remove the entry from the cache, cache_expire
+ * should be used for that.
+ *
+ * This function should be called when an entry that is known to have been
+ * retrieved or inserted into a data store successfully, is no longer needed.
+ *
+ * Some drivers (like rlm_cache_rbtree) don't register a free function.
+ * This means that the cache entry never needs to be explicitly freed.
+ *
+ * @param c Cache entry to free.
+ * @param inst Module instance.
+ */
+static void cache_free(rlm_cache_t *inst, rlm_cache_entry_t **c)
+{
+       if (!c || !*c || !inst->module->free) return;
+
+       inst->module->free(*c);
+       *c = NULL;
 }
 
 /*
  *     Merge a cached entry into a REQUEST.
  */
-static void cache_merge(rlm_cache_t *inst, REQUEST *request,
-                       rlm_cache_entry_t *c)
+static void CC_HINT(nonnull) cache_merge(rlm_cache_t *inst, REQUEST *request, rlm_cache_entry_t *c)
 {
        VALUE_PAIR *vp;
 
-       rad_assert(request != NULL);
-       rad_assert(c != NULL);
-
-       vp = pairfind(request->config_items, PW_CACHE_MERGE, 0, TAG_ANY);
+       vp = fr_pair_find_by_num(request->config, PW_CACHE_MERGE, 0, TAG_ANY);
        if (vp && (vp->vp_integer == 0)) {
                RDEBUG2("Told not to merge entry into request");
                return;
        }
 
-       if (c->control) {
-               RDEBUG2("Merging cached control list:");
-               rdebug_pair_list(2, request, c->control);
-
-               pairadd(&request->config_items, paircopy(request, c->control));
-       }
+       RDEBUG2("Merging cache entry into request");
 
        if (c->packet && request->packet) {
-               RDEBUG2("Merging cached request list:");
-               rdebug_pair_list(2, request, c->packet);
-
-               pairadd(&request->packet->vps,
-                       paircopy(request->packet, c->packet));
+               rdebug_pair_list(L_DBG_LVL_2, request, c->packet, "&request:");
+               radius_pairmove(request, &request->packet->vps, fr_pair_list_copy(request->packet, c->packet), false);
        }
 
        if (c->reply && request->reply) {
-               RDEBUG2("Merging cached reply list:");
-               rdebug_pair_list(2, request, c->reply);
+               rdebug_pair_list(L_DBG_LVL_2, request, c->reply, "&reply:");
+               radius_pairmove(request, &request->reply->vps, fr_pair_list_copy(request->reply, c->reply), false);
+       }
 
-               pairadd(&request->reply->vps,
-                       paircopy(request->reply, c->reply));
+       if (c->control) {
+               rdebug_pair_list(L_DBG_LVL_2, request, c->control, "&control:");
+               radius_pairmove(request, &request->config, fr_pair_list_copy(request, c->control), false);
        }
 
-       if (inst->stats) {
-               vp = paircreate(request->packet, PW_CACHE_ENTRY_HITS, 0);
-               rad_assert(vp != NULL);
+       if (c->state) {
+               rdebug_pair_list(L_DBG_LVL_2, request, c->state, "&session-state:");
+               radius_pairmove(request, &request->state, fr_pair_list_copy(request->state, c->state), false);
+       }
 
+       if (inst->stats) {
+               rad_assert(request->packet != NULL);
+               vp = fr_pair_find_by_num(request->packet->vps, PW_CACHE_ENTRY_HITS, 0, TAG_ANY);
+               if (!vp) {
+                       vp = fr_pair_afrom_num(request->packet, PW_CACHE_ENTRY_HITS, 0);
+                       rad_assert(vp != NULL);
+                       fr_pair_add(&request->packet->vps, vp);
+               }
                vp->vp_integer = c->hits;
-
-               pairadd(&request->packet->vps, vp);
        }
 }
 
 
-/*
- *     Find a cached entry.
+/** Find a cached entry.
+ *
+ * @return RLM_MODULE_OK on success, RLM_MODULE_FAIL on failure, RLM_MODULE_NOTFOUND if notfound.
  */
-static rlm_cache_entry_t *cache_find(rlm_cache_t *inst, REQUEST *request,
-                                    char const *key)
+static rlm_rcode_t cache_find(rlm_cache_entry_t **out, rlm_cache_t *inst, REQUEST *request,
+                             rlm_cache_handle_t **handle, char const *key)
 {
-       int ttl;
-       rlm_cache_entry_t *c, my_c;
-       VALUE_PAIR *vp;
+       cache_status_t ret;
 
-       /*
-        *      Look at the expiry heap.
-        */
-       c = fr_heap_peek(inst->heap);
-       if (!c) {
-               rad_assert(rbtree_num_elements(inst->cache) == 0);
-               return NULL;
-       }
+       rlm_cache_entry_t *c;
 
-       /*
-        *      If it's time to expire an old entry, do so now.
-        */
-       if (c->expires < request->timestamp) {
-               fr_heap_extract(inst->heap, c);
-               rbtree_deletebydata(inst->cache, c);
-       }
+       *out = NULL;
 
-       /*
-        *      Is there an entry for this key?
-        */
-       my_c.key = key;
-       c = rbtree_finddata(inst->cache, &my_c);
-       if (!c) return NULL;
+       for (;;) {
+               ret = inst->module->find(&c, inst, request, handle, key);
+               switch (ret) {
+               case CACHE_RECONNECT:
+                       RDEBUG("Reconnecting...");
+                       if (cache_reconnect(inst, request, handle) == 0) continue;
+                       return RLM_MODULE_FAIL;
+
+               case CACHE_OK:
+                       break;
+
+               case CACHE_MISS:
+                       RDEBUG("No cache entry found for \"%s\"", key);
+                       return RLM_MODULE_NOTFOUND;
+
+               /* FALL-THROUGH */
+               default:
+                       return RLM_MODULE_FAIL;
+
+               }
+
+               break;
+       }
 
        /*
         *      Yes, but it expired, OR the "forget all" epoch has
         *      passed.  Delete it, and pretend it doesn't exist.
         */
-       if ((c->expires < request->timestamp) ||
-           (c->created < inst->epoch)) {
-       delete:
-               RDEBUG("Entry has expired, removing");
+       if ((c->expires < request->timestamp) || (c->created < inst->epoch)) {
+               RDEBUG("Removing expired entry");
 
-               fr_heap_extract(inst->heap, c);
-               rbtree_deletebydata(inst->cache, c);
-
-               return NULL;
+               inst->module->expire(inst, request, handle, c);
+               cache_free(inst, &c);
+               return RLM_MODULE_NOTFOUND;     /* Couldn't find a non-expired entry */
        }
 
        RDEBUG("Found entry for \"%s\"", key);
 
-       /*
-        *      Update the expiry time based on the TTL.
-        *      A TTL of 0 means "delete from the cache".
-        */
-       vp = pairfind(request->config_items, PW_CACHE_TTL, 0, TAG_ANY);
-       if (vp) {
-               if (vp->vp_integer == 0) goto delete;
-
-               ttl = vp->vp_integer;
-               c->expires = request->timestamp + ttl;
-               RDEBUG("Adding %d to the TTL", ttl);
-       }
        c->hits++;
+       *out = c;
 
-       return c;
+       return RLM_MODULE_OK;
 }
 
-
-/*
- *     Add an entry to the cache.
+/** Expire a cache entry (removing it from the datastore)
+ *
  */
-static rlm_cache_entry_t *cache_add(rlm_cache_t *inst, REQUEST *request,
-                                   char const *key)
+static void cache_expire(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle, rlm_cache_entry_t **c)
 {
-       int ttl;
-       VALUE_PAIR *vp, *found, **to_req, **to_cache, **from;
-       DICT_ATTR const *da;
+       rad_assert(*c);
 
-       int merge = true;
-       REQUEST *context;
+       for (;;) switch (inst->module->expire(inst, request, handle, *c)) {
+       case CACHE_RECONNECT:
+               if (cache_reconnect(inst, request, handle) == 0) continue;
 
-       value_pair_map_t const *map;
+       /* FALL-THROUGH */
+       default:
+               cache_free(inst, c);
+               *c = NULL;
+               return;
+       }
+}
 
+/** Create and insert a cache entry.
+ *
+ * @return RLM_MODULE_OK on success, RLM_MODULE_UPDATED if we merged the cache entry and RLM_MODULE_FAIL on failure.
+ */
+static rlm_rcode_t cache_insert(rlm_cache_t *inst, REQUEST *request, rlm_cache_handle_t **handle,
+                               char const *key, int ttl)
+{
+       VALUE_PAIR *vp, *to_cache;
+       vp_cursor_t src_list, packet, reply, control, state;
+
+       vp_map_t const *map;
+
+       bool merge = true;
        rlm_cache_entry_t *c;
-       char buffer[1024];
 
-       if (rbtree_num_elements(inst->cache) >= inst->max_entries) {
-               RDEBUG("Cache is full: %d entries", inst->max_entries);
-               return NULL;
+       if ((inst->max_entries > 0) && inst->module->count &&
+           (inst->module->count(inst, request, handle) > inst->max_entries)) {
+               RWDEBUG("Cache is full: %d entries", inst->max_entries);
+               return RLM_MODULE_FAIL;
        }
 
-       /*
-        *      TTL of 0 means "don't cache this entry"
-        */
-       vp = pairfind(request->config_items, PW_CACHE_TTL, 0, TAG_ANY);
-       if (vp && (vp->vp_integer == 0)) return NULL;
+       c = cache_alloc(inst, request);
+       if (!c) return RLM_MODULE_FAIL;
 
-       c = talloc_zero(inst, rlm_cache_entry_t);
-       c->key = talloc_strdup(c, key);
+       c->key = talloc_typed_strdup(c, key);
        c->created = c->expires = request->timestamp;
-
-       /*
-        *      Use per-entry TTL, or globally defined one.
-        */
-       if (vp) {
-               ttl = vp->vp_integer;
-       } else {
-               ttl = inst->ttl;
-       }
        c->expires += ttl;
 
-       RDEBUG("Creating entry for \"%s\"", key);
+       RDEBUG("Creating new cache entry");
 
-       /*
-        *      Check to see if we need to merge the entry into the request
-        */
-       vp = pairfind(request->config_items, PW_CACHE_MERGE, 0, TAG_ANY);
-       if (vp && (vp->vp_integer == 0)) {
-               merge = false;
-               RDEBUG2("Told not to merge new entry into request");
-       }
+       fr_cursor_init(&packet, &c->packet);
+       fr_cursor_init(&reply, &c->reply);
+       fr_cursor_init(&control, &c->control);
+       fr_cursor_init(&state, &c->state);
 
        for (map = inst->maps; map != NULL; map = map->next) {
-               rad_assert(map->dst && map->src);
-
-               /*
-                *      Specifying inner/outer request doesn't work here
-                *      but there's no easy fix...
-                */
-               switch (map->dst->list) {
-               case PAIR_LIST_REQUEST:
-                       to_cache = &c->packet;
-                       break;
-
-               case PAIR_LIST_REPLY:
-                       to_cache = &c->reply;
-                       break;
-
-               case PAIR_LIST_CONTROL:
-                       to_cache = &c->control;
-                       break;
+               rad_assert(map->lhs && map->rhs);
 
-               default:
-                       rad_assert(0);
-                       return NULL;
+               if (map_to_vp(c, &to_cache, request, map, NULL) < 0) {
+                       RDEBUG("Skipping %s", map->rhs->name);
+                       continue;
                }
 
-               /*
-                *      Resolve the destination in the current request.
-                *      We need to add the to_cache there too if any of these
-                *      are.
-                *      true :
-                *        - Map specifies an xlat'd string.
-                *        - Map specifies a literal string.
-                *        - Map src and dst lists differ.
-                *        - Map src and dst attributes differ
-                */
-               to_req = NULL;
-               if (merge && ( !map->src->da ||
-                   (map->src->list != map->dst->list) ||
-                   (map->src->da != map->dst->da))) {
-                       context = request;
+               for (vp = fr_cursor_init(&src_list, &to_cache);
+                    vp;
+                    vp = fr_cursor_next(&src_list)) {
+                       VERIFY_VP(vp);
+
                        /*
-                        *      It's ok if the list isn't valid here...
-                        *      It might be valid later when we merge
-                        *      the cache entry.
+                        *      Prevent people from accidentally caching
+                        *      cache control attributes.
                         */
-                       if (radius_request(&context, map->dst->request) == 0) {
-                               to_req = radius_list(context, map->dst->list);
-                       }
-               }
+                       if (map->rhs->type == TMPL_TYPE_LIST) switch (vp->da->attr) {
+                       case PW_CACHE_TTL:
+                       case PW_CACHE_STATUS_ONLY:
+                       case PW_CACHE_READ_ONLY:
+                       case PW_CACHE_MERGE:
+                       case PW_CACHE_ENTRY_HITS:
+                               RDEBUG2("Skipping %s", vp->da->name);
+                               continue;
 
-               /*
-                *      We infer that src was an attribute ref from the fact
-                *      it contains a da.
-                */
-               RDEBUG4(":: dst is \"%s\" src is \"%s\"",
-                       fr_int2str(vpt_types, map->dst->type, "<INVALID>"),
-                       fr_int2str(vpt_types, map->src->type, "<INVALID>"));
-
-               switch (map->src->type) {
-               case VPT_TYPE_ATTR:
-                       {
-                               vp_cursor_t cursor;
-
-                               from = NULL;
-                               da = map->src->da;
-                               context = request;
-                               if (radius_request(&context, map->src->request) == 0) {
-                                       from = radius_list(context, map->src->list);
-                               }
-
-                               /*
-                                *      Can't add the attribute if the list isn't
-                                *      valid.
-                                */
-                               if (!from) continue;
-
-                               paircursor(&cursor, from);
-                               found = pairfindnext(&cursor, da->attr, da->vendor, TAG_ANY);
-                               if (!found) {
-                                       RWDEBUG("\"%s\" not found, skipping",
-                                              map->src->name);
-                                       continue;
-                               }
-
-                               RDEBUG("\t%s %s %s", map->dst->name,
-                                      fr_int2str(fr_tokens, map->op, "<INVALID>"),
-                                      map->src->name);
-
-                               switch (map->op) {
-                               case T_OP_SET:
-                               case T_OP_EQ:
-                               case T_OP_SUB:
-                                       vp = map->dst->type == VPT_TYPE_LIST ?
-                                               paircopyvp(c, found) :
-                                               paircopyvpdata(c, map->dst->da, found);
-
-                                       if (!vp) continue;
-
-                                       pairadd(to_cache, vp);
-
-                                       if (to_req) {
-                                               vp = paircopyvp(request, vp);
-                                               radius_pairmove(request, to_req, vp);
-                                       }
-
-                                       break;
-                               case T_OP_ADD:
-                                       do {
-                                               vp = map->dst->type == VPT_TYPE_LIST ?
-                                                       paircopyvp(c, found) :
-                                                       paircopyvpdata(c, map->dst->da, found);
-                                               if (!vp) continue;
-
-                                               vp->op = map->op;
-                                               pairadd(to_cache, vp);
-
-                                               if (to_req) {
-                                                       vp = paircopyvp(request, vp);
-                                                       radius_pairmove(request, to_req, vp);
-
-                                               }
-                                       } while ((found = pairfindnext(&cursor, da->attr, da->vendor, TAG_ANY)));
-                                       break;
-
-                               default:
-                                       rad_assert(0);
-                                       return NULL;
-                               }
+                       default:
                                break;
                        }
-               case VPT_TYPE_LIST:
-                       {
-                               vp_cursor_t cursor;
-                               VALUE_PAIR *i;
-
-                               rad_assert(map->src->type == VPT_TYPE_LIST);
-
-                               from = NULL;
-                               context = request;
-                               if (radius_request(&context, map->src->request) == 0) {
-                                       from = radius_list(context, map->src->list);
-                               }
-                               if (!from) continue;
-
-                               found = paircopy(c, *from);
-                               if (!found) continue;
-
-                               for (i = paircursor(&cursor, &vp);
-                                    i != NULL;
-                                    i = pairnext(&cursor)) {
-                                       RDEBUG("\t%s %s %s (%s)", map->dst->name,
-                                              fr_int2str(fr_tokens, map->op, "<INVALID>"),
-                                              map->src->name, i->da->name);
-                                       i->op = map->op;
-                               }
-
-                               pairadd(to_cache, found);
-
-                               if (to_req) {
-                                       vp = paircopy(request, found);
-                                       radius_pairmove(request, to_req, vp);
-                               }
 
-                               break;
-                       }
-               /*
-                *      It was most likely a double quoted string that now
-                *      needs to be expanded.
-                */
-               case VPT_TYPE_XLAT:
-                       if (radius_xlat(buffer, sizeof(buffer), request, map->src->name, NULL, NULL) <= 0) {
-                               continue;
-                       }
-
-                       RDEBUG("\t%s %s \"%s\"", map->dst->name,
-                              fr_int2str(fr_tokens, map->op, "<INVALID>"),
-                              buffer);
-
-                       vp = pairalloc(NULL, map->dst->da);
-                       if (!vp) continue;
+                       RINDENT();
+                       if (RDEBUG_ENABLED2) map_debug_log(request, map, vp);
+                       REXDENT();
 
                        vp->op = map->op;
-                       if (!pairparsevalue(vp, buffer)) {
-                               pairfree(&vp);
-                               continue;
-                       }
 
-                       pairadd(to_cache, vp);
+                       switch (map->lhs->tmpl_list) {
+                       case PAIR_LIST_REQUEST:
+                               fr_cursor_insert(&packet, vp);
+                               break;
 
-                       if (to_req) {
-                               vp = paircopyvp(request, vp);
-                               radius_pairmove(request, to_req, vp);
-                       }
+                       case PAIR_LIST_REPLY:
+                               fr_cursor_insert(&reply, vp);
+                               break;
 
-                       break;
-               /*
-                *      Literal string.
-                */
-               case VPT_TYPE_LITERAL:
-                       RDEBUG("\t%s %s '%s'", map->dst->name,
-                              fr_int2str(fr_tokens, map->op, "<INVALID>"),
-                              map->src->name);
+                       case PAIR_LIST_CONTROL:
+                               fr_cursor_insert(&control, vp);
+                               break;
 
-                       vp = pairalloc(NULL, map->dst->da);
-                       if (!vp) continue;
+                       case PAIR_LIST_STATE:
+                               fr_cursor_insert(&state, vp);
+                               break;
 
-                       vp->op = map->op;
-                       if (!pairparsevalue(vp, map->src->name)) {
-                               pairfree(&vp);
-                               continue;
+                       default:
+                               rad_assert(0);  /* should have been caught by validation */
                        }
+               }
+       }
 
-                       pairadd(to_cache, vp);
+       /*
+        *      Check to see if we need to merge the entry into the request
+        */
+       vp = fr_pair_find_by_num(request->config, PW_CACHE_MERGE, 0, TAG_ANY);
+       if (vp && (vp->vp_integer == 0)) merge = false;
 
-                       if (to_req) {
-                               vp = paircopyvp(request, vp);
-                               radius_pairmove(request, to_req, vp);
-                       }
+       if (merge) cache_merge(inst, request, c);
 
-                       break;
+       for (;;) {
+               cache_status_t ret;
+
+               ret = inst->module->insert(inst, request, handle, c);
+               switch (ret) {
+               case CACHE_RECONNECT:
+                       if (cache_reconnect(inst, request, handle) == 0) continue;
+                       return RLM_MODULE_FAIL;
+
+               case CACHE_OK:
+                       RDEBUG("Committed entry, TTL %d seconds", ttl);
+                       cache_free(inst, &c);
+                       return RLM_MODULE_UPDATED;
 
                default:
-                       rad_assert(0);
-                       return NULL;
+                       talloc_free(c); /* Failed insertion - use talloc_free not the driver free */
+                       return RLM_MODULE_FAIL;
                }
        }
+}
 
-       if (!rbtree_insert(inst->cache, c)) {
-               REDEBUG("FAILED adding entry for key %s", key);
-               cache_entry_free(c);
-               return NULL;
+/** Verify that a map in the cache section makes sense
+ *
+ */
+static int cache_verify(vp_map_t *map, void *ctx)
+{
+       if (modcall_fixup_update(map, ctx) < 0) return -1;
+
+       if ((map->lhs->type != TMPL_TYPE_ATTR) &&
+           (map->lhs->type != TMPL_TYPE_LIST)) {
+               cf_log_err(map->ci, "Destination must be an attribute ref or a list");
+               return -1;
        }
 
-       if (!fr_heap_insert(inst->heap, c)) {
-               REDEBUG("FAILED adding entry for key %s", key);
-               rbtree_deletebydata(inst->cache, c);
-               return NULL;
+       switch (map->lhs->tmpl_list) {
+       case PAIR_LIST_REQUEST:
+       case PAIR_LIST_REPLY:
+       case PAIR_LIST_CONTROL:
+       case PAIR_LIST_STATE:
+               break;
+
+       default:
+               cf_log_err(map->ci, "Destination list must be one of request, reply, control or session-state");
+               return -1;
        }
 
-       RDEBUG("Inserted entry, TTL %d seconds", ttl);
+       if (map->lhs->tmpl_request != REQUEST_CURRENT) {
+               cf_log_err(map->ci, "Cached attributes can only be inserted into the current request");
+               return -1;
+       }
 
-       return c;
+       switch (map->rhs->type) {
+       case TMPL_TYPE_EXEC:
+               cf_log_err(map->ci, "Exec values are not allowed");
+               return -1;
+       /*
+        *      Only =, :=, += and -= operators are supported for
+        *      cache entries.
+        */
+       case TMPL_TYPE_LITERAL:
+       case TMPL_TYPE_XLAT:
+       case TMPL_TYPE_ATTR:
+               switch (map->op) {
+               case T_OP_SET:
+               case T_OP_EQ:
+               case T_OP_SUB:
+               case T_OP_ADD:
+                       break;
+
+               default:
+                       cf_log_err(map->ci, "Operator \"%s\" not allowed for %s values",
+                                  fr_int2str(fr_tokens, map->op, "<INVALID>"),
+                                  fr_int2str(tmpl_names, map->rhs->type, "<INVALID>"));
+                       return -1;
+               }
+       default:
+               break;
+       }
+
+       return 0;
 }
 
 /*
- *     Verify that the cache section makes sense.
+ *     Do caching checks.  Since we can update ANY VP list, we do
+ *     exactly the same thing for all sections (autz / auth / etc.)
+ *
+ *     If you want to cache something different in different sections,
+ *     configure another cache module.
  */
-static int cache_verify(rlm_cache_t *inst, value_pair_map_t **head)
+static rlm_rcode_t CC_HINT(nonnull) mod_cache_it(void *instance, REQUEST *request)
 {
-       value_pair_map_t *map;
+       rlm_cache_entry_t *c;
+       rlm_cache_t *inst = instance;
 
-       if (radius_attrmap(cf_section_sub_find(inst->cs, "update"),
-                          head, PAIR_LIST_REQUEST,
-                          PAIR_LIST_REQUEST, MAX_ATTRMAP) < 0) {
-               return -1;
+       rlm_cache_handle_t *handle;
+
+       vp_cursor_t cursor;
+       VALUE_PAIR *vp;
+       char buffer[1024];
+       rlm_rcode_t rcode;
+
+       int ttl = inst->ttl;
+
+       if (radius_xlat(buffer, sizeof(buffer), request, inst->key, NULL, NULL) < 0) return RLM_MODULE_FAIL;
+
+       if (buffer[0] == '\0') {
+               REDEBUG("Zero length key string is invalid");
+               return RLM_MODULE_INVALID;
        }
 
-       if (!*head) {
-               cf_log_err_cs(inst->cs,
-                          "Cache config must contain an update section, and "
-                          "that section must not be empty");
+       if (cache_acquire(&handle, inst, request) < 0) return RLM_MODULE_FAIL;
 
-               return -1;
+       rcode = cache_find(&c, inst, request, &handle, buffer);
+       if (rcode == RLM_MODULE_FAIL) goto finish;
+       rad_assert(handle);
+
+       /*
+        *      If Cache-Status-Only == yes, only return whether we found a
+        *      valid cache entry
+        */
+       vp = fr_pair_find_by_num(request->config, PW_CACHE_STATUS_ONLY, 0, TAG_ANY);
+       if (vp && vp->vp_integer) {
+               rcode = c ? RLM_MODULE_OK:
+                           RLM_MODULE_NOTFOUND;
+               goto finish;
        }
 
-       for (map = *head; map != NULL; map = map->next) {
-               if ((map->dst->type != VPT_TYPE_ATTR) &&
-                   (map->dst->type != VPT_TYPE_LIST)) {
-                       cf_log_err(map->ci, "Left operand must be an attribute "
-                                  "ref or a list");
+       /*
+        *      Update the expiry time based on the TTL.
+        *      A TTL of 0 means "delete from the cache".
+        *      A TTL < 0 means "delete from the cache and recreate the entry".
+        */
+       vp = fr_pair_find_by_num(request->config, PW_CACHE_TTL, 0, TAG_ANY);
+       if (vp) ttl = vp->vp_signed;
 
-                       return -1;
+       /*
+        *      If there's no existing cache entry, go and create a new one.
+        */
+       if (!c) {
+               if (ttl <= 0) ttl = inst->ttl;
+               goto insert;
+       }
+
+       /*
+        *      Expire the entry if requested to do so
+        */
+       if (vp) {
+               if (ttl == 0) {
+                       cache_expire(inst, request, &handle, &c);
+                       RDEBUG("Forcing expiry of entry");
+                       rcode = RLM_MODULE_OK;
+                       goto finish;
                }
 
-               switch (map->src->type) {
-               case VPT_TYPE_EXEC:
-                       cf_log_err(map->ci, "Exec values are not allowed");
+               if (ttl < 0) {
+                       RDEBUG("Forcing expiry of existing entry");
+                       cache_expire(inst, request, &handle, &c);
+                       ttl *= -1;
+                       goto insert;
+               }
+               c->expires = request->timestamp + ttl;
+               RDEBUG("Setting TTL to %d", ttl);
+       }
 
-                       return -1;
+       /*
+        *      Cache entry was still valid, so we merge it into the request
+        *      and return. No need to add a new entry.
+        */
+       cache_merge(inst, request, c);
+       rcode = RLM_MODULE_OK;
 
-               /*
-                *      Only =, :=, += and -= operators are supported for
-                *      cache entries.
-                */
-               case VPT_TYPE_LITERAL:
-               case VPT_TYPE_XLAT:
-               case VPT_TYPE_ATTR:
-                       switch (map->op) {
-                       case T_OP_SET:
-                       case T_OP_EQ:
-                       case T_OP_SUB:
-                       case T_OP_ADD:
-                               break;
+       goto finish;
 
-                       default:
-                               cf_log_err(map->ci, "Operator \"%s\" not "
-                                          "allowed for %s values",
-                                          fr_int2str(fr_tokens, map->op,
-                                                     "<INVALID>"),
-                                          fr_int2str(vpt_types, map->src->type,
-                                                     "<INVALID>"));
-                               return -1;
-                       }
-               default:
+insert:
+       /*
+        *      If Cache-Read-Only == yes, then we only allow already cached entries
+        *      to be merged into the request
+        */
+       vp = fr_pair_find_by_num(request->config, PW_CACHE_READ_ONLY, 0, TAG_ANY);
+       if (vp && vp->vp_integer) {
+               rcode = RLM_MODULE_NOTFOUND;
+               goto finish;
+       }
+
+       /*
+        *      Create a new entry.
+        */
+       rcode = cache_insert(inst, request, &handle, buffer, ttl);
+       rad_assert(handle);
+
+finish:
+       cache_free(inst, &c);
+       cache_release(inst, request, &handle);
+
+       /*
+        *      Clear control attributes
+        */
+       for (vp = fr_cursor_init(&cursor, &request->config);
+            vp;
+            vp = fr_cursor_next(&cursor)) {
+               if (vp->da->vendor == 0) switch (vp->da->attr) {
+               case PW_CACHE_TTL:
+               case PW_CACHE_STATUS_ONLY:
+               case PW_CACHE_READ_ONLY:
+               case PW_CACHE_MERGE:
+                       vp = fr_cursor_remove(&cursor);
+                       talloc_free(vp);
                        break;
                }
        }
-       return 0;
+
+       return rcode;
 }
 
+static ssize_t CC_HINT(nonnull) cache_xlat(void *instance, REQUEST *request,
+                                          char const *fmt, char *out, size_t freespace);
+
 /*
  *     Allow single attribute values to be retrieved from the cache.
  */
 static ssize_t cache_xlat(void *instance, REQUEST *request,
                          char const *fmt, char *out, size_t freespace)
 {
-       rlm_cache_entry_t *c;
-       rlm_cache_t *inst = instance;
-       VALUE_PAIR *vp, *vps;
-       pair_lists_t list;
-       DICT_ATTR const *target;
-       char const *p = fmt;
-       int ret = 0;
-
-       list = radius_list_name(&p, PAIR_LIST_REQUEST);
+       rlm_cache_entry_t       *c = NULL;
+       rlm_cache_t             *inst = instance;
+       rlm_cache_handle_t      *handle = NULL;
+
+       VALUE_PAIR              *vp, *vps;
+       pair_lists_t            list;
+       DICT_ATTR const         *target;
+       char const              *p = fmt;
+       size_t                  len;
+       int                     ret = 0;
+
+       p += radius_list_name(&list, p, PAIR_LIST_REQUEST);
+       if (list == PAIR_LIST_UNKNOWN) {
+               REDEBUG("Unknown list qualifier in \"%s\"", fmt);
+               ret = -1;
+               goto finish;
+       }
 
        target = dict_attrbyname(p);
        if (!target) {
@@ -622,13 +584,18 @@ static ssize_t cache_xlat(void *instance, REQUEST *request,
                return -1;
        }
 
-       PTHREAD_MUTEX_LOCK(&inst->cache_mutex);
-       c = cache_find(inst, request, fmt);
+       if (cache_acquire(&handle, inst, request) < 0) return -1;
 
-       if (!c) {
-               RDEBUG("No cache entry for key \"%s\"", fmt);
+       switch (cache_find(&c, inst, request, handle, fmt)) {
+       case RLM_MODULE_OK:             /* found */
+               break;
+
+       case RLM_MODULE_NOTFOUND:       /* not found */
                *out = '\0';
-               goto done;
+               return 0;
+
+       default:
+               return -1;
        }
 
        switch (list) {
@@ -644,58 +611,38 @@ static ssize_t cache_xlat(void *instance, REQUEST *request,
                vps = c->control;
                break;
 
-       case PAIR_LIST_UNKNOWN:
-               PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
-               REDEBUG("Unknown list qualifier in \"%s\"", fmt);
-               return -1;
+       case PAIR_LIST_STATE:
+               vps = c->state;
+               break;
 
        default:
-               PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
-               REDEBUG("Unsupported list \"%s\"",
-                       fr_int2str(pair_lists, list, "¿Unknown?"));
-               return -1;
+               REDEBUG("Unsupported list \"%s\"", fr_int2str(pair_lists, list, "<UNKNOWN>"));
+               ret = -1;
+               goto finish;
        }
 
-       vp = pairfind(vps, target->attr, target->vendor, TAG_ANY);
+       vp = fr_pair_find_by_num(vps, target->attr, target->vendor, TAG_ANY);
        if (!vp) {
                RDEBUG("No instance of this attribute has been cached");
                *out = '\0';
-               goto done;
+               goto finish;
        }
 
-       ret = vp_prints_value(out, freespace, vp, 0);
-done:
-       PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
+       len = vp_prints_value(out, freespace, vp, 0);
+       if (is_truncated(len, freespace)) {
+               REDEBUG("Insufficient buffer space to write cached value");
+               ret = -1;
+               goto finish;
+       }
+
+finish:
+       cache_free(inst, &c);
+       cache_release(inst, request, &handle);
 
        return ret;
 }
 
 /*
- *     A mapping of configuration file names to internal variables.
- *
- *     Note that the string is dynamically allocated, so it MUST
- *     be freed.  When the configuration file parse re-reads the string,
- *     it free's the old one, and strdup's the new one, placing the pointer
- *     to the strdup'd string into 'config.string'.  This gets around
- *     buffer over-flows.
- */
-static const CONF_PARSER module_config[] = {
-       { "key",  PW_TYPE_STRING_PTR | PW_TYPE_REQUIRED,
-         offsetof(rlm_cache_t, key), NULL, NULL},
-       { "ttl", PW_TYPE_INTEGER,
-         offsetof(rlm_cache_t, ttl), NULL, "500" },
-       { "max_entries", PW_TYPE_INTEGER,
-         offsetof(rlm_cache_t, max_entries), NULL, "16384" },
-       { "epoch", PW_TYPE_INTEGER,
-         offsetof(rlm_cache_t, epoch), NULL, "0" },
-       { "add_stats", PW_TYPE_BOOLEAN,
-         offsetof(rlm_cache_t, stats), NULL, "no" },
-
-       { NULL, -1, 0, NULL, NULL }             /* end the list */
-};
-
-
-/*
  *     Only free memory we allocated.  The strings allocated via
  *     cf_section_parse() do not need to be freed.
  */
@@ -705,137 +652,151 @@ static int mod_detach(void *instance)
 
        talloc_free(inst->maps);
 
-       fr_heap_delete(inst->heap);
-       rbtree_free(inst->cache);
+       /*
+        *  We need to explicitly free all children, so if the driver
+        *  parented any memory off the instance, their destructors
+        *  run before we unload the bytecode for them.
+        *
+        *  If we don't do this, we get a SEGV deep inside the talloc code
+        *  when it tries to call a destructor that no longer exists.
+        */
+       talloc_free_children(inst);
+
+       /*
+        *  Decrements the reference count. The driver object won't be unloaded
+        *  until all instances of rlm_cache that use it have been destroyed.
+        */
+       if (inst->handle) dlclose(inst->handle);
 
-#ifdef HAVE_PTHREAD_H
-       pthread_mutex_destroy(&inst->cache_mutex);
-#endif
        return 0;
 }
 
 
-/*
- *     Instantiate the module.
- */
-static int mod_instantiate(CONF_SECTION *conf, void *instance)
+static int mod_bootstrap(CONF_SECTION *conf, void *instance)
 {
        rlm_cache_t *inst = instance;
 
        inst->cs = conf;
 
-       inst->xlat_name = cf_section_name2(conf);
-       if (!inst->xlat_name) {
-               inst->xlat_name = cf_section_name1(conf);
-       }
+       inst->name = cf_section_name2(conf);
+       if (!inst->name) inst->name = cf_section_name1(conf);
 
        /*
         *      Register the cache xlat function
         */
-       xlat_register(inst->xlat_name, cache_xlat, NULL, inst);
+       xlat_register(inst->name, cache_xlat, NULL, inst);
 
-       rad_assert(inst->key && *inst->key);
+       return 0;
+}
 
-       if (inst->ttl == 0) {
-               cf_log_err_cs(conf, "Must set 'ttl' to non-zero");
-               return -1;
-       }
 
-       if (inst->epoch != 0) {
-               cf_log_err_cs(conf, "Must not set 'epoch' in the configuration files");
-               return -1;
-       }
+/*
+ *     Instantiate the module.
+ */
+static int mod_instantiate(CONF_SECTION *conf, void *instance)
+{
+       rlm_cache_t *inst = instance;
+       CONF_SECTION *update;
 
-#ifdef HAVE_PTHREAD_H
-       if (pthread_mutex_init(&inst->cache_mutex, NULL) < 0) {
-               EDEBUG("Failed initializing mutex: %s",
-                      strerror(errno));
-               return -1;
-       }
-#endif
+       inst->cs = conf;
 
        /*
-        *      The cache.
+        *      Sanity check for crazy people.
         */
-       inst->cache = rbtree_create(cache_entry_cmp, cache_entry_free, 0);
-       if (!inst->cache) {
-               EDEBUG("Failed to create cache");
+       if (strncmp(inst->driver_name, "rlm_cache_", 8) != 0) {
+               cf_log_err_cs(conf, "\"%s\" is NOT an Cache driver!", inst->driver_name);
                return -1;
        }
 
        /*
-        *      The heap of entries to expire.
+        *      Load the appropriate driver for our database
         */
-       inst->heap = fr_heap_create(cache_heap_cmp,
-                                   offsetof(rlm_cache_entry_t, offset));
-       if (!inst->heap) {
-               EDEBUG("Failed to create heap for the cache");
+       inst->handle = lt_dlopenext(inst->driver_name);
+       if (!inst->handle) {
+               cf_log_err_cs(conf, "Could not link driver %s: %s", inst->driver_name, dlerror());
+               cf_log_err_cs(conf, "Make sure it (and all its dependent libraries!) are in the search path"
+                             " of your system's ld");
                return -1;
        }
 
-       /*
-        *      Make sure the users don't screw up too badly.
-        */
-       if (cache_verify(inst, &inst->maps) < 0) {
+       inst->module = (cache_module_t *) dlsym(inst->handle, inst->driver_name);
+       if (!inst->module) {
+               cf_log_err_cs(conf, "Could not link symbol %s: %s", inst->driver_name, dlerror());
                return -1;
        }
 
-       return 0;
-}
+       DEBUG("rlm_cache (%s): Driver %s (module %s) loaded and linked", inst->name,
+             inst->driver_name, inst->module->name);
 
-/*
- *     Do caching checks.  Since we can update ANY VP list, we do
- *     exactly the same thing for all sections (autz / auth / etc.)
- *
- *     If you want to cache something different in different sections,
- *     configure another cache module.
- */
-static rlm_rcode_t cache_it(void *instance, REQUEST *request)
-{
-       rlm_cache_entry_t *c;
-       rlm_cache_t *inst = instance;
-       VALUE_PAIR *vp;
-       char buffer[1024];
-       rlm_rcode_t rcode;
+       /*
+        *      Non optional fields and callbacks
+        */
+       rad_assert(inst->module->name);
+       rad_assert(inst->module->find);
+       rad_assert(inst->module->insert);
+       rad_assert(inst->module->expire);
+
+       if (inst->module->instantiate) {
+               CONF_SECTION *cs;
+               char const *name;
+
+               name = strrchr(inst->driver_name, '_');
+               if (!name) {
+                       name = inst->driver_name;
+               } else {
+                       name++;
+               }
 
-       if (radius_xlat(buffer, sizeof(buffer), request, inst->key, NULL, NULL) < 0) {
-               return RLM_MODULE_FAIL;
+               cs = cf_section_sub_find(conf, name);
+               if (!cs) {
+                       cs = cf_section_alloc(conf, name, NULL);
+                       if (!cs) return -1;
+               }
+
+               /*
+                *      It's up to the driver to register a destructor (using talloc)
+                *
+                *      Should write its instance data in inst->driver,
+                *      and parent it off of inst.
+                */
+               if (inst->module->instantiate(cs, inst) < 0) return -1;
        }
 
-       PTHREAD_MUTEX_LOCK(&inst->cache_mutex);
-       c = cache_find(inst, request, buffer);
+       rad_assert(inst->key && *inst->key);
 
-       /*
-        *      If yes, only return whether we found a valid cache entry
-        */
-       vp = pairfind(request->config_items, PW_CACHE_STATUS_ONLY, 0, TAG_ANY);
-       if (vp && vp->vp_integer) {
-               rcode = c ? RLM_MODULE_OK:
-                           RLM_MODULE_NOTFOUND;
-               goto done;
+       if (inst->ttl == 0) {
+               cf_log_err_cs(conf, "Must set 'ttl' to non-zero");
+               return -1;
        }
 
-       if (c) {
-               cache_merge(inst, request, c);
+       if (inst->epoch != 0) {
+               cf_log_err_cs(conf, "Must not set 'epoch' in the configuration files");
+               return -1;
+       }
 
-               rcode = RLM_MODULE_OK;
-               goto done;
+       update = cf_section_sub_find(inst->cs, "update");
+       if (!update) {
+               cf_log_err_cs(conf, "Must have an 'update' section in order to cache anything.");
+               return -1;
        }
 
-       c = cache_add(inst, request, buffer);
-       if (!c) {
-               rcode = RLM_MODULE_NOOP;
-               goto done;
+       /*
+        *      Make sure the users don't screw up too badly.
+        */
+       if (map_afrom_cs(&inst->maps, update,
+                        PAIR_LIST_REQUEST, PAIR_LIST_REQUEST, cache_verify, NULL, MAX_ATTRMAP) < 0) {
+               return -1;
        }
 
-       rcode = RLM_MODULE_UPDATED;
+       if (!inst->maps) {
+               cf_log_err_cs(inst->cs, "Cache config must contain an update section, and "
+                             "that section must not be empty");
 
-done:
-       PTHREAD_MUTEX_UNLOCK(&inst->cache_mutex);
-       return rcode;
+               return -1;
+       }
+       return 0;
 }
 
-
 /*
  *     The module name should be the only globally exported symbol.
  *     That is, everything else should be 'static'.
@@ -845,22 +806,21 @@ done:
  *     The server will then take care of ensuring that the module
  *     is single-threaded.
  */
+extern module_t rlm_cache;
 module_t rlm_cache = {
-       RLM_MODULE_INIT,
-       "cache",
-       0,                              /* type */
-       sizeof(rlm_cache_t),
-       module_config,
-       mod_instantiate,                /* instantiation */
-       mod_detach,                     /* detach */
-       {
-               NULL,                   /* authentication */
-               cache_it,               /* authorization */
-               cache_it,               /* preaccounting */
-               cache_it,               /* accounting */
-               NULL,                   /* checksimul */
-               cache_it,               /* pre-proxy */
-               cache_it,               /* post-proxy */
-               cache_it,               /* post-auth */
+       .magic          = RLM_MODULE_INIT,
+       .name           = "cache",
+       .inst_size      = sizeof(rlm_cache_t),
+       .config         = module_config,
+       .bootstrap      = mod_bootstrap,
+       .instantiate    = mod_instantiate,
+       .detach         = mod_detach,
+       .methods = {
+               [MOD_AUTHORIZE]         = mod_cache_it,
+               [MOD_PREACCT]           = mod_cache_it,
+               [MOD_ACCOUNTING]        = mod_cache_it,
+               [MOD_PRE_PROXY]         = mod_cache_it,
+               [MOD_POST_PROXY]        = mod_cache_it,
+               [MOD_POST_AUTH]         = mod_cache_it
        },
 };