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)
LIBOBJS = \
$(OUT)\assert.obj \
+ $(OUT)\cache.obj \
$(OUT)\casestrcpy.obj \
$(OUT)\dict.obj \
$(OUT)\fmt.obj \
--- /dev/null
+/*
+ * 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);
+}
}
#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
auth/authcon
auth/realms
cmd/command
+opr/cache
opr/dict
opr/fmt
opr/jhash
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)
--- /dev/null
+/*
+ * 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;
+}