opr: Introduce opr_cache 84/13884/11
authorAndrew Deason <adeason@sinenomine.net>
Fri, 20 Sep 2019 19:19:23 +0000 (14:19 -0500)
committerBenjamin Kaduk <kaduk@mit.edu>
Fri, 6 Mar 2020 16:57:51 +0000 (11:57 -0500)
Add a simple general-purpose in-memory cache implementation, called
opr_cache. Keys and values are simple flat opaque buffers (no complex
nested structures allowed), hashing is done with jhash, and cache
eviction is mostly random with some LRU bias.

Partly based off a different implementation by
mbarbosa@sinenomine.net.

Change-Id: I16b5988947ff603dfe31613cd7be3908a69264e5
Reviewed-on: https://gerrit.openafs.org/13884
Tested-by: BuildBot <buildbot@rampaginggeek.com>
Reviewed-by: Benjamin Kaduk <kaduk@mit.edu>

src/opr/Makefile.in
src/opr/NTMakefile
src/opr/cache.c [new file with mode: 0644]
src/opr/opr.h
tests/TESTS
tests/opr/Makefile.in
tests/opr/cache-t.c [new file with mode: 0644]

index 968f6e5..a6cc4af 100644 (file)
@@ -3,7 +3,7 @@ include @TOP_OBJDIR@/src/config/Makefile.config
 include @TOP_OBJDIR@/src/config/Makefile.pthread
 include @TOP_OBJDIR@/src/config/Makefile.libtool
 
-LT_objs = assert.lo casestrcpy.lo dict.lo fmt.lo proc.lo rbtree.lo \
+LT_objs = assert.lo cache.lo casestrcpy.lo dict.lo fmt.lo proc.lo rbtree.lo \
          softsig.lo threadname.lo uuid.lo
 LT_libs = $(LIB_hcrypto) $(LIB_roken)
 
index d2866e0..98c9d3d 100644 (file)
@@ -36,6 +36,7 @@ LIBFILE = $(DESTDIR)\lib\opr.lib
 
 LIBOBJS = \
        $(OUT)\assert.obj \
+       $(OUT)\cache.obj \
        $(OUT)\casestrcpy.obj \
        $(OUT)\dict.obj \
        $(OUT)\fmt.obj \
diff --git a/src/opr/cache.c b/src/opr/cache.c
new file mode 100644 (file)
index 0000000..82b759d
--- /dev/null
@@ -0,0 +1,432 @@
+/*
+ * Copyright (c) 2019 Sine Nomine Associates. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR `AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*
+ * opr_cache - A simple general-purpose in-memory cache implementation. Keys
+ * and values are simple flat memory buffers (keys are compared with memcmp),
+ * and currently the only cache eviction strategy is a semi-random approach. We
+ * hash values using opr_jhash_opaque.
+ */
+
+#include <afsconfig.h>
+#include <afs/param.h>
+
+#include <roken.h>
+#include <afs/opr.h>
+
+#include <opr/dict.h>
+#include <opr/queue.h>
+#include <opr/jhash.h>
+#ifdef AFS_PTHREAD_ENV
+# include <opr/lock.h>
+#else
+# include <opr/lockstub.h>
+#endif
+
+#define MIN_BUCKETS (4)
+#define MAX_BUCKETS (1024*1024)
+
+#define MIN_ENTRIES (4)
+#define MAX_ENTRIES (1024*1024*1024)
+
+struct opr_cache {
+    /*
+     * This lock protects everything in the opr_cache structure. This lock must
+     * be held before touching anything in opr_cache, and before calling any of
+     * our internal functions (unless we're allocating or freeing the cache; in
+     * which case, we had better be the only one touching it!).
+     */
+    opr_mutex_t lock;
+
+    /* How many items are present in the cache? */
+    afs_uint32 n_entries;
+
+    /* The max number of items we can hold in the cache. */
+    afs_uint32 max_entries;
+
+    /* Our dictionary holding the cached items. Contains cache_entry structs,
+     * linked together via 'link'. */
+    struct opr_dict *dict;
+};
+
+struct cache_entry {
+    struct opr_queue link;
+
+    size_t key_len;
+    void *key_buf;
+
+    size_t val_len;
+    void *val_buf;
+};
+
+static void
+free_entry_contents(struct cache_entry *entry)
+{
+    opr_queue_Remove(&entry->link);
+
+    free(entry->key_buf);
+    free(entry->val_buf);
+
+    entry->key_buf = entry->val_buf = NULL;
+    entry->key_len = entry->val_len = 0;
+}
+
+/**
+ * Evict an entry from the cache.
+ *
+ * @pre cache is full.
+ * @pre cache->lock is held.
+ *
+ * @param[in] cache    The opr_cache to use.
+ * @param[out] a_entry Set to the cache entry that was evicted. Its contents
+ *                     have been freed; it can be treated like a
+ *                     newly-allocated cache entry.
+ */
+static void
+evict_entry(struct opr_cache *cache, struct cache_entry **a_entry)
+{
+    unsigned int safety, bucket;
+    struct cache_entry *entry = NULL;
+
+    opr_Assert(cache->dict->size > 0);
+    opr_Assert(cache->n_entries > 0);
+
+    /*
+     * For cache eviction, we pick an entry somewhat randomly, since random
+     * eviction performs fairly well, and is very simple to implement.
+     *
+     * Our approach slightly differs from pure random eviction: we pick a
+     * random hash bucket, and evict the least-recently-used entry in that
+     * bucket. If the bucket we picked is empty, just use the next bucket.
+     */
+    bucket = rand() % cache->dict->size;
+
+    for (safety = 0; safety < cache->dict->size; safety++) {
+       struct opr_queue *chain;
+       chain = &cache->dict->table[bucket];
+
+       if (!opr_queue_IsEmpty(chain)) {
+           entry = opr_queue_Last(chain, struct cache_entry, link);
+           break;
+       }
+
+       bucket++;
+       bucket = bucket % cache->dict->size;
+    }
+    opr_Assert(entry != NULL);
+
+    free_entry_contents(entry);
+
+    *a_entry = entry;
+}
+
+/**
+ * Alloc a new entry in the cache.
+ *
+ * @pre cache->lock is held.
+ * @pre The given key does not already exist in the cache.
+ *
+ * @param[in] cache    The opr_cache to use.
+ * @param[inout] a_key_buf  The key for the new cache entry. Set to NULL on
+ *                         success; the memory is used in the new cache entry,
+ *                         and must not be freed or otherwise used by the
+ *                         caller.
+ * @param[in] key_len  The size of *a_key_buf.
+ * @param[out] a_entry Set to the new cache entry on success. The entry has
+ *                     been inserted into the cache, and the key has been
+ *                     populated; it looks identical to a normal entry in the
+ *                     cache, except the value is set to NULL.
+ *
+ * @return errno status codes
+ */
+static int
+alloc_entry(struct opr_cache *cache, void **a_key_buf, size_t key_len,
+           struct cache_entry **a_entry)
+{
+    struct cache_entry *entry = NULL;
+    unsigned int hash;
+
+    if (cache->n_entries >= cache->max_entries) {
+       /*
+        * The cache is full. "Alloc" an entry by picking an existing entry to
+        * evict, and just re-use that entry.
+        */
+       evict_entry(cache, &entry);
+
+    } else {
+       entry = calloc(1, sizeof(*entry));
+       if (entry == NULL) {
+           return ENOMEM;
+       }
+       cache->n_entries++;
+    }
+
+    entry->key_len = key_len;
+    entry->key_buf = *a_key_buf;
+    *a_key_buf = NULL;
+
+    hash = opr_jhash_opaque(entry->key_buf, entry->key_len, 0);
+    opr_dict_Prepend(cache->dict, hash, &entry->link);
+
+    *a_entry = entry;
+    return 0;
+}
+
+/**
+ * Find a cache entry in the cache.
+ *
+ * @pre cache->lock is held.
+ *
+ * @param[in] cache    The opr_cache to use.
+ * @param[in] key_buf  The key of the entry to find.
+ * @param[in] key_len  The size of 'key_buf'.
+ * @param[out] a_entry The found cache entry.
+ *
+ * @return errno status codes
+ * @retval ENOENT The given key does not exist in the cache.
+ */
+static int
+find_entry(struct opr_cache *cache, void *key_buf, size_t key_len,
+          struct cache_entry **a_entry)
+{
+    struct opr_queue *cursor;
+    unsigned int hash;
+
+    hash = opr_jhash_opaque(key_buf, key_len, 0);
+
+    for (opr_dict_ScanBucket(cache->dict, hash, cursor)) {
+       struct cache_entry *entry;
+       entry = opr_queue_Entry(cursor, struct cache_entry, link);
+       if (key_len != entry->key_len) {
+           continue;
+       }
+       if (memcmp(key_buf, entry->key_buf, key_len) != 0) {
+           continue;
+       }
+       opr_dict_Promote(cache->dict, hash, &entry->link);
+       *a_entry = entry;
+       return 0;
+    }
+
+    return ENOENT;
+}
+
+/**
+ * Fetch an item from the cache.
+ *
+ * If NULL is given for 'cache' or 'key_buf', or if 0 is given for key_len, we
+ * always return ENOENT. Conceptually, we treat a NULL cache as an empty cache,
+ * and NULL/0-length keys are not allowed in an opr_cache, so returning ENOENT
+ * in these cases is consistent.
+ *
+ * @param[in] cache    The opr_cache to use.
+ * @param[in] key_buf  The key to lookup.
+ * @param[in] key_len  The size of 'key_buf'.
+ * @param[out] val_buf On success, where to store the looked-up value.
+ * @param[inout] a_val_len  On entry, set to the max size available in
+ *                         'val_buf'. On successful return, set to the number
+ *                         of bytes copied into 'val_buf'.
+ * @return errno status codes
+ * @retval 0 success
+ * @retval ENOENT key not found
+ * @retval ENOSPC a_val_len is too small to store the retrieved value
+ */
+int
+opr_cache_get(struct opr_cache *cache, void *key_buf, size_t key_len,
+             void *val_buf, size_t *a_val_len)
+{
+    struct cache_entry *entry;
+    int code;
+
+    if (cache == NULL || key_buf == NULL || key_len < 1) {
+       return ENOENT;
+    }
+
+    opr_mutex_enter(&cache->lock);
+
+    code = find_entry(cache, key_buf, key_len, &entry);
+    if (code != 0) {
+       goto done;
+    }
+
+    if (entry->val_len > *a_val_len) {
+       code = ENOSPC;
+       goto done;
+    }
+
+    memcpy(val_buf, entry->val_buf, entry->val_len);
+    *a_val_len = entry->val_len;
+
+ done:
+    opr_mutex_exit(&cache->lock);
+    return code;
+}
+
+static void*
+memdup(void *src, size_t len)
+{
+    void *buf = malloc(len);
+    if (buf == NULL) {
+       return NULL;
+    }
+    memcpy(buf, src, len);
+    return buf;
+}
+
+/**
+ * Store an item in the cache.
+ *
+ * This operation is a no-op if NULL is given for 'cache', 'key_buf', or
+ * 'val_buf', or if 'key_len' or 'val_len' are 0. Conceptually, a NULL
+ * opr_cache represents an empty cache, and NULL/0-length keys and values are
+ * not allowed in an opr_cache.
+ *
+ * @param[in] cache    The opr_cache to use.
+ * @param[in] key_buf  The key for the stored value.
+ * @param[in] key_len  The size of 'key_buf'.
+ * @param[in] val_buf  The value to store.
+ * @param[in] val_len  The size of 'val_buf'.
+ */
+void
+opr_cache_put(struct opr_cache *cache, void *key_buf, size_t key_len,
+             void *val_buf, size_t val_len)
+{
+    int code;
+    struct cache_entry *entry;
+    void *key_dup = NULL;
+    void *val_dup = NULL;
+
+    if (cache == NULL || key_buf == NULL || val_buf == NULL || key_len < 1 ||
+       val_len < 1) {
+       goto done;
+    }
+
+    key_dup = memdup(key_buf, key_len);
+    val_dup = memdup(val_buf, val_len);
+    if (key_dup == NULL || val_dup == NULL) {
+       goto done;
+    }
+
+    opr_mutex_enter(&cache->lock);
+
+    code = find_entry(cache, key_buf, key_len, &entry);
+    if (code == ENOENT) {
+       code = alloc_entry(cache, &key_dup, key_len, &entry);
+    }
+    if (code != 0) {
+       goto done_locked;
+    }
+
+    free(entry->val_buf);
+    entry->val_buf = val_dup;
+    entry->val_len = val_len;
+    val_dup = NULL;
+
+ done_locked:
+    opr_mutex_exit(&cache->lock);
+ done:
+    free(key_dup);
+    free(val_dup);
+}
+
+static_inline int
+isPowerOf2(int value)
+{
+    return (value > 0) && (value & (value - 1)) == 0;
+}
+
+/**
+ * Create a new opr_cache.
+ *
+ * @param[in] opts  What options to use for the cache.
+ * @param[out] a_cache  The newly-allocated cache.
+ *
+ * @return errno status codes
+ * @retval EINVAL   invalid option values
+ */
+int
+opr_cache_init(struct opr_cache_opts *opts, struct opr_cache **a_cache)
+{
+    struct opr_cache *cache;
+
+    if (opts->n_buckets < MIN_BUCKETS || opts->n_buckets > MAX_BUCKETS) {
+       return EINVAL;
+    }
+    if (!isPowerOf2(opts->n_buckets)) {
+       return EINVAL;
+    }
+    if (opts->max_entries < MIN_ENTRIES || opts->max_entries > MAX_ENTRIES) {
+       return EINVAL;
+    }
+
+    cache = calloc(1, sizeof(*cache));
+    if (cache == NULL) {
+       return ENOMEM;
+    }
+
+    opr_mutex_init(&cache->lock);
+    cache->max_entries = opts->max_entries;
+
+    cache->dict = opr_dict_Init(opts->n_buckets);
+    if (cache->dict == NULL) {
+       opr_cache_free(&cache);
+       return ENOMEM;
+    }
+
+    *a_cache = cache;
+    return 0;
+}
+
+/**
+ * Free an opr_cache.
+ *
+ * @param[inout] a_cache    The cache to free. Set to NULL on return.
+ */
+void
+opr_cache_free(struct opr_cache **a_cache)
+{
+    struct opr_cache *cache = *a_cache;
+    if (cache == NULL) {
+       return;
+    }
+    *a_cache = NULL;
+
+    if (cache->dict != NULL) {
+       int bucket;
+       for (bucket = 0; bucket < cache->dict->size; bucket++) {
+           struct opr_queue *cursor, *tmp;
+           for (opr_dict_ScanBucketSafe(cache->dict, bucket, cursor, tmp)) {
+               struct cache_entry *entry;
+               entry = opr_queue_Entry(cursor, struct cache_entry, link);
+               free_entry_contents(entry);
+               free(entry);
+           }
+       }
+       opr_dict_Free(&cache->dict);
+    }
+
+    opr_mutex_destroy(&cache->lock);
+
+    free(cache);
+}
index 784ccb5..1130b33 100644 (file)
@@ -86,4 +86,22 @@ opr_threadname_set(const char *threadname)
 }
 #endif
 
+/* cache.c */
+
+struct opr_cache_opts {
+    afs_uint32 max_entries;
+    afs_uint32 n_buckets;
+};
+struct opr_cache;
+
+extern int opr_cache_init(struct opr_cache_opts *opts,
+                         struct opr_cache **a_cache) AFS_NONNULL();
+extern void opr_cache_free(struct opr_cache **a_cache) AFS_NONNULL();
+
+extern int opr_cache_get(struct opr_cache *cache, void *key_buf,
+                        size_t key_len, void *val_buf, size_t *a_val_len)
+                        AFS_NONNULL((4,5));
+extern void opr_cache_put(struct opr_cache *cache, void *key_buf,
+                         size_t key_len, void *val_buf, size_t val_len);
+
 #endif
index 29ed02d..8285d1c 100644 (file)
@@ -6,6 +6,7 @@ auth/superuser
 auth/authcon
 auth/realms
 cmd/command
+opr/cache
 opr/dict
 opr/fmt
 opr/jhash
index d9a37f6..52916fb 100644 (file)
@@ -7,10 +7,14 @@ MODULE_CFLAGS = -I$(TOP_OBJDIR)
 
 LIBS=../tap/libtap.a $(abs_top_builddir)/src/opr/liboafs_opr.la
 
-tests = dict-t fmt-t jhash-t queues-t rbtree-t softsig-helper time-t uuid-t
+tests = cache-t dict-t fmt-t jhash-t queues-t rbtree-t softsig-helper time-t \
+       uuid-t
 
 all check test tests: $(tests)
 
+cache-t: cache-t.o
+       $(LT_LDRULE_static) cache-t.o $(LIBS) $(XLIBS)
+
 dict-t: dict-t.o
        $(LT_LDRULE_static) dict-t.o $(LIBS) $(XLIBS)
 
diff --git a/tests/opr/cache-t.c b/tests/opr/cache-t.c
new file mode 100644 (file)
index 0000000..522bfaf
--- /dev/null
@@ -0,0 +1,247 @@
+/*
+ * Copyright (c) 2019 Sine Nomine Associates. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR `AS IS'' AND ANY EXPRESS OR
+ * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
+ * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
+ * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
+ * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
+ * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+ * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#include <afsconfig.h>
+#include <afs/param.h>
+
+#include <errno.h>
+#include <string.h>
+
+#include <tests/tap/basic.h>
+#include <afs/opr.h>
+
+#include <stdlib.h>
+#include <time.h>
+
+struct {
+    char *key;
+    int key_len;
+
+    char *val;
+    int val_len;
+} items[] = {
+
+#define TCASE(key, len, val) { (key), (len), (val), sizeof(val)-1 }
+
+    TCASE("foo\0\0", 6, "one"),
+    TCASE("bar\0\0", 6, "two"),
+    TCASE("baz\0\0", 6, "three"),
+    TCASE("quux\0",  6, "four"),
+    TCASE("pants",   6, "five"),
+
+    TCASE("foo\0\0", 4, "six"),
+    TCASE("bar\0\0", 4, "seven"),
+    TCASE("baz\0\0", 4, "eight"),
+    TCASE("quux\0",  5, "nine"),
+
+    TCASE("foo1\0", 6, "ten"),
+    TCASE("bar1\0", 6, "eleven"),
+    TCASE("baz1\0", 6, "twelve"),
+    TCASE("quux1",  6, "thirteen"),
+
+    TCASE("f\xf3\x0a", 5, "value \x01"),
+    TCASE("ba\xffr", 5, "\x01\x02\x03"),
+    TCASE("ba\xffz", 5, "\0\0\0\0"),
+
+#undef TCASE
+
+};
+static const int n_items = sizeof(items)/sizeof(items[0]);
+
+static void
+run_seed(int seed)
+{
+    struct opr_cache_opts opts;
+    struct opr_cache *cache = NULL;
+    int item_i;
+    int missing;
+    int code;
+    char val[1024];
+    size_t val_len;
+
+    srand(seed);
+
+    val_len = sizeof(val);
+    code = opr_cache_get(cache, NULL, 0, val, &val_len);
+    is_int(ENOENT, code,
+       "Looking up in a NULL cache fails with ENOENT");
+
+    opr_cache_put(cache, NULL, 0, NULL, 0);
+    ok(1,
+       "Storing in a NULL cache does nothing");
+
+    memset(&opts, 0, sizeof(opts));
+
+    opts.n_buckets = 2;
+    opts.max_entries = 100;
+    ok(opr_cache_init(&opts, &cache) != 0,
+       "Initializing a cache with a tiny n_buckets fails");
+
+    opts.n_buckets = 0x40000000;
+    ok(opr_cache_init(&opts, &cache) != 0,
+       "Initializing a cache with a huge n_buckets fails");
+
+    opts.n_buckets = 23;
+    ok(opr_cache_init(&opts, &cache) != 0,
+       "Initializing a cache with non-power-of-2 n_buckets fails");
+
+    opts.n_buckets = 64;
+    opts.max_entries = 1;
+    ok(opr_cache_init(&opts, &cache) != 0,
+       "Initializing a cache with a tiny max_entries fails");
+
+    opts.max_entries = 0x7fffffff;
+    ok(opr_cache_init(&opts, &cache) != 0,
+       "Initializing a cache with a huge max_entries fails");
+
+    opts.n_buckets = 8;
+    opts.max_entries = 12;
+
+    code = opr_cache_init(&opts, &cache);
+    is_int(0, code,
+       "Initializing a reasonably-sized cache succeeds");
+
+    ok(cache != NULL,
+       "Initializing a cache gives us a cache");
+
+    for (item_i = 0; item_i < n_items; item_i++) {
+       val_len = sizeof(val);
+       code = opr_cache_get(cache, items[item_i].key,
+                            items[item_i].key_len,
+                            val, &val_len);
+       is_int(ENOENT, code,
+          "[item %d] Looking up in an empty cache fails with ENOENT", item_i);
+    }
+
+    for (item_i = 0; item_i < 12; item_i++) {
+       opr_cache_put(cache, items[item_i].key, items[item_i].key_len,
+                     items[item_i].val, items[item_i].val_len);
+    }
+    ok(1, "Cache filled successfully");
+
+    for (item_i = 0; item_i < 12; item_i++) {
+       val_len = sizeof(val);
+       code = opr_cache_get(cache, items[item_i].key, items[item_i].key_len,
+                            val, &val_len);
+       is_int(0, code, "[item %d] Lookup succeeds", item_i);
+       is_int(items[item_i].val_len, val_len,
+          "[item %d] Lookup returns correct val_len %d",
+          item_i, items[item_i].val_len);
+
+       ok(memcmp(val, items[item_i].val, val_len) == 0,
+          "[item %d] Lookup returns correct val", item_i);
+    }
+
+    val_len = sizeof(val);
+    code = opr_cache_get(cache, NULL, 5, val, &val_len);
+    is_int(ENOENT, code,
+       "Looking up NULL key fails with ENOENT");
+
+    code = opr_cache_get(cache, val, 0, val, &val_len);
+    is_int(ENOENT, code,
+       "Looking up 0-length key fails with ENOENT");
+
+    opr_cache_put(cache, NULL, 0, val, val_len);
+    opr_cache_put(cache, NULL, 5, val, val_len);
+    opr_cache_put(cache, val, 0, val, val_len);
+    opr_cache_put(cache, val, val_len, NULL, 0);
+    opr_cache_put(cache, val, val_len, NULL, 5);
+    opr_cache_put(cache, val, val_len, val, 0);
+    opr_cache_put(cache, NULL, 0, NULL, 0);
+    opr_cache_put(cache, NULL, 5, NULL, 5);
+    opr_cache_put(cache, val, 0, val, 0);
+    ok(1, "Storing NULL/0-length entries does nothing");
+
+    code = opr_cache_get(cache, "123", 3, val, &val_len);
+    is_int(ENOENT, code, "Cache lookup fails for nonexistent item");
+
+    memcpy(val, "replace", 7);
+    val_len = 7;
+    opr_cache_put(cache, items[0].key, items[0].key_len, val, val_len);
+    ok(1, "Replacement store succeeds");
+
+    val_len = 1;
+    code = opr_cache_get(cache, items[0].key, items[0].key_len, val, &val_len);
+    is_int(ENOSPC, code, "Small lookup returns ENOSPC");
+
+    val_len = sizeof(val);
+    code = opr_cache_get(cache, items[0].key, items[0].key_len, val, &val_len);
+    is_int(0, code, "Replacement lookup succeeds");
+    is_int(7, val_len, "Lookup trims val_len");
+    ok(memcmp(val, "replace", 7) == 0,
+       "Replacement lookup returns correct value");
+
+    /* Set items[0] back to the original value. */
+    opr_cache_put(cache, items[0].key, items[0].key_len, items[0].val,
+                 items[0].val_len);
+
+    for (item_i = 12; item_i < n_items; item_i++) {
+       opr_cache_put(cache, items[item_i].key, items[item_i].key_len,
+                     items[item_i].val, items[item_i].val_len);
+    }
+    ok(1, "[seed %d] Cache over-filled successfully", seed);
+
+    missing = 0;
+    for (item_i = 0; item_i < n_items; item_i++) {
+       val_len = sizeof(val);
+       code = opr_cache_get(cache, items[item_i].key, items[item_i].key_len,
+                            val, &val_len);
+       if (code == ENOENT) {
+           missing++;
+           continue;
+       }
+       is_int(0, code, "[item %d] Lookup succeeds", item_i);
+       is_int(items[item_i].val_len, val_len,
+          "[item %d] Lookup returns correct val_len %d",
+          item_i, items[item_i].val_len);
+
+       ok(memcmp(val, items[item_i].val, val_len) == 0,
+           "[item %d] Lookup returns correct val", item_i);
+    }
+
+    is_int(4, missing,
+       "[seed %d] Cache lookup fails for %d items", seed, missing);
+
+    opr_cache_free(&cache);
+    ok(1, "Cache free succeeds");
+    ok(cache == NULL, "Cache free NULLs arg");
+
+    opr_cache_free(&cache);
+    ok(1, "Double-free is noop");
+    ok(cache == NULL, "Cache is still NULL after double-free");
+}
+
+int
+main(void)
+{
+    int seed;
+
+    plan(113 * 32);
+
+    for (seed = 0; seed < 32; seed++) {
+       run_seed(seed);
+    }
+
+    return 0;
+}