Table of Contents
ex_disk_cache.c
a bounded on-disk cache keyed by content Makes a scratch directory, stores and reads entries, checks the path a key maps to, then gives three entries known last-used times and checks the size prune removes the least recently used first, the age expiry removes only the old, and neither ever touches a file the cache did not make. Last, two caches pruned as a group share one budget and one order. ```c
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <time.h> #include <unistd.h> #include <sys/stat.h>
#include "nilorea/n_common.h" #include "nilorea/n_log.h" #include "nilorea/n_disk_cache.h"
#ifdef windows #include <direct.h> #include <sys/utime.h> #define ex_mkdir(p) _mkdir(p) #else #include <utime.h> #define ex_mkdir(p) mkdir(p, 0700) #endif
#define EX_DIR "ex_disk_cache.dir"
static int log_level = LOG_ERR; static int failures = 0;
static void expect_true(const char* what, int ok) { if (!ok) { n_log(LOG_ERR, "FAIL: %s", what); failures++; } else { n_log(LOG_INFO, "ok: %s", what); } }
static int process_args(int argc, char** argv) { int opt = 0;
while ((opt = getopt(argc, argv, "V:")) != -1) {
if (opt == 'V') {
log_level = atoi(optarg);
} else {
return -1;
}
}
return 0;
}
static int exists(const char* path) { struct stat st;
return stat(path, &st) == 0;
}
static void write_foreign(const char* name) { char path[512]; FILE* f = NULL;
snprintf(path, sizeof(path), "%s/%s", EX_DIR, name);
f = fopen(path, "wb");
if (f) {
fputs("not the cache's", f);
fclose(f);
}
}
static void put_aged(const N_DISK_CACHE* cache, const char* key, long ago) { char data[100]; char path[512]; struct utimbuf when;
memset(data, key[0], sizeof(data));
n_disk_cache_put(cache, key, strlen(key), ".bin", data, sizeof(data));
n_disk_cache_path(cache, key, strlen(key), ".bin", path, sizeof(path));
when.actime = time(NULL) - ago;
when.modtime = when.actime;
utime(path, &when);
}
static int has(const N_DISK_CACHE* cache, const char* key) { char path[512];
return n_disk_cache_path(cache, key, strlen(key), ".bin", path, sizeof(path)) && exists(path);
}
int main(int argc, char** argv) { N_DISK_CACHE* cache = NULL; unsigned char* data = NULL; size_t len = 0; uint64_t total = 0; char p1[512], p2[512], p3[512]; const char* upper = "0123456789ABCDEF0123456789ABCDEF.bin";
set_log_level(LOG_ERR);
if (process_args(argc, argv) != 0) {
fprintf(stderr, "Usage: %s [-V LOG_LEVEL]\n", argv[0]);
return 2;
}
set_log_level(log_level);
expect_true("a directory that is not there is refused", n_disk_cache_open(EX_DIR "/nope") == NULL);
ex_mkdir(EX_DIR);
cache = n_disk_cache_open(EX_DIR "/");
expect_true("an existing directory opens", cache != NULL);
if (!cache) return 1;
expect_true("its name loses the trailing separator", strcmp(n_disk_cache_dir(cache), EX_DIR) == 0);
/* keys to paths */
expect_true("a key has a path", n_disk_cache_path(cache, "k", 1, ".png", p1, sizeof(p1)));
n_disk_cache_path(cache, "k", 1, ".png", p2, sizeof(p2));
n_disk_cache_path(cache, "k", 1, ".jpg", p3, sizeof(p3));
expect_true("the same key has the same path", strcmp(p1, p2) == 0);
expect_true("another extension, another file", strcmp(p1, p3) != 0);
expect_true("the path is the directory, 32 digits and the extension",
strlen(p1) == strlen(EX_DIR) + 1 + N_DISK_CACHE_NAME_LEN + 4 && strcmp(p1 + strlen(p1) - 4, ".png") == 0);
expect_true("an extension with a separator is refused", n_disk_cache_path(cache, "k", 1, "/x", p1, sizeof(p1)) == FALSE);
expect_true("a path that does not fit is refused", n_disk_cache_path(cache, "k", 1, ".png", p1, 8) == FALSE);
/* put and get */
expect_true("put stores", n_disk_cache_put(cache, "hello", 5, NULL, "world", 5));
expect_true("get finds it", n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len) && len == 5 && memcmp(data, "world", 5) == 0 && data[5] == '\0');
free(data);
data = NULL;
expect_true("an entry over the limit is a miss", n_disk_cache_get(cache, "hello", 5, NULL, 4, &data, &len) == FALSE && data == NULL);
expect_true("another key is a miss", n_disk_cache_get(cache, "hellO", 5, NULL, 1024, &data, &len) == FALSE);
expect_true("put replaces", n_disk_cache_put(cache, "hello", 5, NULL, "again!", 6) &&
n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len) && len == 6);
free(data);
data = NULL;
expect_true("remove removes", n_disk_cache_remove(cache, "hello", 5, NULL) && !n_disk_cache_get(cache, "hello", 5, NULL, 1024, &data, &len));
expect_true("removing what is not there is fine", n_disk_cache_remove(cache, "hello", 5, NULL));
/* files the cache did not make */
write_foreign("notes.txt");
write_foreign(upper);
/* least recently used first */
put_aged(cache, "a", 3000);
put_aged(cache, "b", 2000);
put_aged(cache, "c", 1000);
expect_true("size counts only the entries", n_disk_cache_size(cache, &total) == 3 && total == 300);
expect_true("prune to 250 bytes removes one", n_disk_cache_prune(cache, 250) == 1);
expect_true("the least recently used", !has(cache, "a") && has(cache, "b") && has(cache, "c"));
expect_true("prune under budget removes none", n_disk_cache_prune(cache, 1000) == 0);
expect_true("prune to 0 removes the rest", n_disk_cache_prune(cache, 0) == 2 && !has(cache, "b") && !has(cache, "c"));
/* by age */
put_aged(cache, "d", 1000);
put_aged(cache, "e", 0);
expect_true("expire at 500 s removes the old one", n_disk_cache_expire(cache, 500) == 1 && !has(cache, "d") && has(cache, "e"));
expect_true("expire at 0 removes nothing", n_disk_cache_expire(cache, 0) == 0 && has(cache, "e"));
n_disk_cache_prune(cache, 0);
snprintf(p1, sizeof(p1), "%s/notes.txt", EX_DIR);
snprintf(p2, sizeof(p2), "%s/%s", EX_DIR, upper);
expect_true("a file that is not an entry is never removed", exists(p1) && exists(p2));
remove(p1);
remove(p2);
/* two caches, one budget: the oldest entries go whichever directory they
* are in */
{
N_DISK_CACHE* other = NULL;
const N_DISK_CACHE* both[2];
ex_mkdir(EX_DIR "/other");
other = n_disk_cache_open(EX_DIR "/other");
expect_true("a second cache opens", other != NULL);
if (other) {
both[0] = cache;
both[1] = other;
put_aged(cache, "f", 4000);
put_aged(other, "g", 3000);
put_aged(cache, "h", 2000);
put_aged(other, "i", 1000);
expect_true("a group prune to 250 bytes removes two", n_disk_cache_prune_group(both, 2, 250) == 2);
expect_true("the two oldest, one from each", !has(cache, "f") && !has(other, "g") && has(cache, "h") && has(other, "i"));
expect_true("under budget removes none", n_disk_cache_prune_group(both, 2, 1000) == 0);
expect_true("a NULL cache is skipped", n_disk_cache_prune_group((const N_DISK_CACHE* const[]){NULL, other}, 2, 0) == 1 && !has(other, "i") && has(cache, "h"));
expect_true("no caches is an error", n_disk_cache_prune_group(both, 0, 0) == -1);
n_disk_cache_prune(cache, 0);
n_disk_cache_close(&other);
}
expect_true("the second scratch directory is empty again", rmdir(EX_DIR "/other") == 0);
}
n_disk_cache_close(&cache);
expect_true("close sets the handle to NULL", cache == NULL);
n_disk_cache_close(&cache);
n_disk_cache_close(NULL);
expect_true("the scratch directory is empty again", rmdir(EX_DIR) == 0);
if (failures) {
fprintf(stderr, "ex_disk_cache: %d failure(s)\n", failures);
return 1;
}
printf("ex_disk_cache: ok\n");
return 0;
}
_Filename: ex_disk_cache.c_
Modules
- AABB3D
- ACCEPT_POOL
- ALLEGRO4
- ALLEGRO5
- ANIM_LIB
- ASTAR
- ATLAS_LIB
- AVRO
- CLOCK_SYNC
- COMMONS
- CONFIGFILE
- CYPHER_BASE64
- CYPHER_VIGENERE
- DEAD_RECKONING
- ENUMS
- EXCEPTIONS
- GAME_ENV
- GIT
- GUI
- HASH_TABLE
- ISOMETRIC_ENGINE
- LIST
- LOG
- LOGNODUP
- LZ4
- NETWORKING
- NETWORK_MSG
- N_COOKIES
- N_DIFF
- N_DIGEST
- N_DISK_CACHE
- N_DNS
- N_ECC
- N_ENTROPY
- N_FILES
- N_FLUIDS
- N_GUI_COLOR
- N_GUI_COLORWHEEL
- N_GUI_CURVE
- N_GUI_GRID
- N_GUI_POPUP
- N_HEX
- N_HTTP3
- N_HTTP_COOKIE
- N_HTTP_FORM
- N_HTTP_MIME
- N_HTTP_REQ
- N_HTTP_RESP
- N_HTTP_ROUTER
- N_HTTP_SERVER
- N_ICC
- N_JSON
- N_KAFKA
- N_LINALG
- N_MODULE
- N_OPENSSL_COMPAT
- N_PRETTY
- N_PTH
- N_QUERY
- N_RANDOM
- N_SHA256
- N_STR
- N_TIFF
- N_TIME
- N_USER
- N_X509
- N_ZIP
- PARTICLES
- PCRE
- PHYSICS
- SIGNALS
- STACK
- THREADS
- TRAJECTORY
- TREE
- ZLIB