diff --git a/CMakeLists.txt b/CMakeLists.txt index d093160..6c82fa0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -36,6 +36,11 @@ if(KVSPACE_BUILD_TESTS) target_link_libraries(test_shm_split PRIVATE kvspace-c) add_test(NAME shm_split COMMAND test_shm_split) + add_executable(test_shm_resize tests/test_shm_resize.c) + target_include_directories(test_shm_resize PRIVATE src) + target_link_libraries(test_shm_resize PRIVATE kvspace-c) + add_test(NAME shm_resize COMMAND test_shm_resize) + add_executable(test_art_scan tests/test_art_scan.c) target_include_directories(test_art_scan PRIVATE src) target_link_libraries(test_art_scan PRIVATE kvspace-c) diff --git a/src/kvspace.c b/src/kvspace.c index b62b3f5..cfe26cc 100644 --- a/src/kvspace.c +++ b/src/kvspace.c @@ -21,8 +21,15 @@ #define ART_PREFIX_MAX 10 #define ART_NODE_MAX_SZ 2112 #define ART_SLAB_INIT (256UL * 1024 * 1024) -/* Reserved VA per region: grow in place, base never moves. */ +/* blocks_init picks 30-bit ids only if the initial pool holds > 8191 blocks; + a smaller pool would be capped at 16384 blocks forever. */ +#define SBO_HEAD_POOL_INIT (4UL * 1024 * 1024) +/* Reserved VA per region: grow in place, base never moves. + data grows x64 per step; 2^40 fits 8 * 64^6 = 512GB. */ #define REGION_RESERVE (1ULL << 38) +#define DATA_RESERVE (1ULL << 40) +/* Values >= 64KB are level-3+ objects (32KB aligned): punch pages on delete. */ +#define SBO_PUNCH_MIN (64UL * 1024) #define WATCH_TABLE_SZ 256 enum { ART_N4 = 0, @@ -80,6 +87,27 @@ _Static_assert(ART_SLAB_INIT / (ART_NODE_MAX_SZ + 2) > 32767ULL / 4, "ART_SLAB_INIT too small: blockmalloc would pick 14-bit ids"); _Static_assert(REGION_RESERVE / (ART_NODE_MAX_SZ + 4) <= (1ULL << 30), "REGION_RESERVE too large for 30-bit block ids"); +_Static_assert( + SBO_HEAD_POOL_INIT / (sizeof(sbo_box_t) + 2) > 32767ULL / 4, + "SBO_HEAD_POOL_INIT too small: blockmalloc would pick 14-bit ids"); + +/* Head layout with root_slots == 1 (checked at open): + [sbo_meta_t][blocks_meta_t][sbo_lock_t][pool]. Public structs only. */ +#define SBO_HEAD_FIXED \ + (sizeof(sbo_meta_t) + sizeof(blocks_meta_t) + sizeof(sbo_lock_t)) +static blocks_meta_t *sbo_pool(sbo_meta_t *m) { + return (blocks_meta_t *)(m + 1); +} +static sbo_lock_t *sbo_plock(sbo_meta_t *m) { + return (sbo_lock_t *)(sbo_pool(m) + 1); +} +static uint8_t *sbo_pmem(sbo_meta_t *m) { + return (uint8_t *)(sbo_plock(m) + 1); +} +static sbo_box_t *sbo_box(sbo_meta_t *m, int32_t id) { + return (sbo_box_t *)(sbo_pmem(m) + (size_t)id * m->block_stride + + m->sizeof_block_head); +} typedef struct { int fd; @@ -102,7 +130,8 @@ struct kvspace { kvspace_hdr_t *hdr; blocks_meta_t *art_meta; uint8_t *art_data; - uint8_t *sbo_meta, *sbo_data; + sbo_meta_t *sbo_meta; + uint8_t *sbo_data; watch_t watches[WATCH_TABLE_SZ]; pthread_mutex_t wlock; }; @@ -121,12 +150,13 @@ static int region_map(shm_region_t *r, size_t size) { } /* create: size the file; open: check it. Then reserve VA and map. */ -static int region_attach(shm_region_t *r, bool create, size_t size) { +static int region_attach(shm_region_t *r, bool create, size_t size, + size_t reserve) { struct stat st; if (create ? ftruncate(r->fd, (off_t)size) != 0 : fstat(r->fd, &st) != 0 || st.st_size < (off_t)size) return -1; - r->reserve = size > REGION_RESERVE ? size : REGION_RESERVE; + r->reserve = size > reserve ? size : reserve; void *p = mmap(NULL, r->reserve, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); if (p == MAP_FAILED) @@ -163,6 +193,131 @@ static int art_slab_grow(kvspace_t *kv) { return 0; } +/* ---- sbo growth (docs/shm-resize.md). Both grow functions run under + sbo_plock, which excludes sbo_alloc/free/allocated_size in every process. */ + +/* Free list empty and no room to append. Unlocked calls are a hint only. */ +static bool sbo_pool_full(sbo_meta_t *m) { + blocks_meta_t *p = sbo_pool(m); + if (p->free_next_id != -1) + return false; + uint64_t next_end = (uint64_t)block_offset(p, p->malloc_blocks) + + p->sizeof_block_head + p->block_size; + return next_end > p->total_size; +} + +/* Cap at what the block-head width can address: 14-bit or 30-bit ids. */ +static int sbo_head_grow(kvspace_t *kv) { + sbo_meta_t *m = kv->sbo_meta; + blocks_meta_t *p = sbo_pool(m); + uint64_t stride = p->sizeof_block_head + p->block_size; + uint64_t cap = (1ULL << (p->sizeof_block_head == 2 ? 14 : 30)) * stride; + uint64_t cur = p->total_size, want = cur * 2; + if (want > cap) + want = cap; + if (SBO_HEAD_FIXED + want > kv->r_head.reserve) + want = kv->r_head.reserve - SBO_HEAD_FIXED; + if (want <= cur || + ftruncate(kv->r_head.fd, (off_t)(SBO_HEAD_FIXED + want)) != 0 || + region_map(&kv->r_head, SBO_HEAD_FIXED + (size_t)want) != 0) + return -1; + p->total_size = want; + m->per_slot_meta = want; + m->head_size = SBO_HEAD_FIXED + want; + kv->hdr->sbo_head_size = m->head_size; + return 0; +} + +/* x64: move the root into a new block, make block 0 the level above with + slot 0 pointing at it. Existing offsets stay valid (docs 2.2). */ +static int sbo_data_grow(kvspace_t *kv) { + sbo_meta_t *m = kv->sbo_meta; + uint64_t cur = m->data_size, want = cur * 64; + if (want > kv->r_data.reserve) + return -1; + if (sbo_pool_full(m) && sbo_head_grow(kv) != 0) + return -1; + if (ftruncate(kv->r_data.fd, (off_t)want) != 0 || + region_map(&kv->r_data, (size_t)want) != 0) + return -1; + int64_t nid = blocks_alloc(sbo_pool(m), sbo_pmem(m)); + if (nid < 0) + return -1; + sbo_box_t *root = sbo_box(m, 0), *b = sbo_box(m, (int32_t)nid); + memcpy(b, root, sizeof *b); + b->parent = 0; + for (int i = 0; i < SBO_N; i++) + if (root->slots[i].state == SBO_BOX) + sbo_box(m, root->children[i])->parent = (int32_t)nid; + uint8_t lvl = (uint8_t)(root->objlevel + 1); + root->objlevel = lvl; + root->box_boundary = 1; + root->obj_boundary = SBO_N - 1; + memset(root->free_bitmap, 0xFF, SBO_BITMAP_B); + root->free_bitmap[0] &= 0xFE; + for (int i = 0; i < SBO_N; i++) { + root->slots[i].state = SBO_FREE; + root->children[i] = -1; + } + root->slots[0].state = SBO_BOX; + root->children[0] = (int32_t)nid; + root->max_obj_cap = SBO_N - 1; + root->child_max_cap = (sbo_usage_t){(uint8_t)(lvl + 1), 1}; + m->data_size = want; + m->slot_bytes = want; + kv->hdr->sbo_data_size = want; + return 0; +} + +/* Pool exhaustion is exact. Data exhaustion is not (sbo_alloc also fails on + trylock contention), so re-root only after two failures at one capacity. */ +static int kv_sbo_grow(kvspace_t *kv, bool failed, uint64_t *seen_data) { + sbo_meta_t *m = kv->sbo_meta; + sbo_lock(sbo_plock(m)); + int rc = 0; + if (sbo_pool_full(m)) + rc = sbo_head_grow(kv); + else if (failed && m->data_size == *seen_data) + rc = sbo_data_grow(kv); + else if (failed) + *seen_data = m->data_size; + sbo_unlock(sbo_plock(m)); + return rc; +} + +/* A failed sbo_alloc burns one slot per level (box_boundary is not rolled + back), so check the pool before allocating. */ +static uint64_t kv_sbo_alloc(kvspace_t *kv, size_t n) { + sbo_meta_t *m = kv->sbo_meta; + uint64_t seen_data = 0; + for (int attempt = 0; attempt < 8; attempt++) { + if (attempt && kv_sync(kv) != 0) + return (uint64_t)-1; + if (sbo_pool_full(m) && kv_sbo_grow(kv, false, &seen_data) != 0) + return (uint64_t)-1; + uint64_t off = sbo_alloc(m, n); + if (off != (uint64_t)-1) + return off; + if (kv_sbo_grow(kv, true, &seen_data) != 0) + return (uint64_t)-1; + } + return (uint64_t)-1; +} + +/* Punch before free: once freed, another process may reuse the range. */ +static void kv_sbo_free(kvspace_t *kv, uint64_t off) { + uint64_t sz = sbo_allocated_size(kv->sbo_meta, off); + if (sz >= SBO_PUNCH_MIN) { + uint64_t pg = (uint64_t)sysconf(_SC_PAGESIZE); + uint64_t a = (off + pg - 1) & ~(pg - 1), b = (off + sz) & ~(pg - 1); + if (b > a) + (void)fallocate(kv->r_data.fd, + FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, + (off_t)a, (off_t)(b - a)); + } + sbo_free(kv->sbo_meta, off); +} + /* ---- helpers ---- */ static void *art_blk(kvspace_t *kv, int32_t id) { if (id < 0) @@ -620,7 +775,7 @@ static int32_t art_del(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, if (!h->has_value) return nid; h->has_value = 0; - sbo_free(kv->sbo_meta, h->box_offset); + kv_sbo_free(kv, h->box_offset); h->box_offset = 0; *del = true; } else { @@ -834,7 +989,7 @@ kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { size_t art_slab, sbo_head; if (created) { art_slab = ART_SLAB_INIT; - sbo_head = sbo_meta_size(data_size, 256 * 1024); + sbo_head = sbo_meta_size(data_size, SBO_HEAD_POOL_INIT); } else { kvspace_hdr_t tmp; if (pread(kv->r_art.fd, &tmp, sizeof tmp, 0) != (ssize_t)sizeof tmp || @@ -847,17 +1002,18 @@ kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { /* O_TRUNC: sbo_init rejects a stale magic */ int fl = created ? O_RDWR | O_CREAT | O_TRUNC : O_RDWR; - if (region_attach(&kv->r_art, created, ART_OFF + art_slab) != 0 || + if (region_attach(&kv->r_art, created, ART_OFF + art_slab, + REGION_RESERVE) != 0 || (kv->r_head.fd = open(head_path, fl, 0644)) < 0 || - region_attach(&kv->r_head, created, sbo_head) != 0 || + region_attach(&kv->r_head, created, sbo_head, REGION_RESERVE) != 0 || (kv->r_data.fd = open(data_path, fl, 0644)) < 0 || - region_attach(&kv->r_data, created, data_size) != 0) + region_attach(&kv->r_data, created, data_size, DATA_RESERVE) != 0) goto fail; kv->hdr = (kvspace_hdr_t *)kv->r_art.base; kv->art_meta = (blocks_meta_t *)(kv->r_art.base + sizeof(kvspace_hdr_t)); kv->art_data = kv->r_art.base + ART_OFF; - kv->sbo_meta = kv->r_head.base; + kv->sbo_meta = (sbo_meta_t *)kv->r_head.base; kv->sbo_data = kv->r_data.base; if (created) { @@ -872,6 +1028,9 @@ kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { /* magic last: half-initialized file fails reopen */ memcpy(kv->hdr->magic, KVS_MAGIC, sizeof(KVS_MAGIC) - 1); } + /* growth code assumes the single-root pool layout (SBO_HEAD_FIXED) */ + if (kv->sbo_meta->root_slots != 1) + goto fail; pthread_mutex_init(&kv->wlock, NULL); for (int i = 0; i < WATCH_TABLE_SZ; i++) { @@ -1156,9 +1315,9 @@ static int shm_set_raw(kvspace_t *kv, const char *key, const uint8_t *val, memcpy(kv->sbo_data + old->box_offset, val, (size_t)val_len); return 0; } - sbo_free(kv->sbo_meta, old->box_offset); + kv_sbo_free(kv, old->box_offset); } - uint64_t off = sbo_alloc(kv->sbo_meta, (size_t)val_len); + uint64_t off = kv_sbo_alloc(kv, (size_t)val_len); if (off == (uint64_t)-1) return -1; memcpy(kv->sbo_data + off, val, val_len); @@ -1492,8 +1651,8 @@ static int shm_alloc_head(kvspace_t *kv, const char *key, uint8_t ref, art_hdr_t *old = art_search(kv, kv->hdr->art_root, (const uint8_t *)key, (int)strlen(key)); if (old && old->has_value) - sbo_free(kv->sbo_meta, old->box_offset); - uint64_t off = sbo_alloc(kv->sbo_meta, (size_t)total); + kv_sbo_free(kv, old->box_offset); + uint64_t off = kv_sbo_alloc(kv, (size_t)total); if (off == (uint64_t)-1) return -1; int32_t dims[X_MAX_NDIM]; diff --git a/src/kvspace_shm.h b/src/kvspace_shm.h index 5385750..66e0fd1 100644 --- a/src/kvspace_shm.h +++ b/src/kvspace_shm.h @@ -19,7 +19,8 @@ typedef struct kvspace kvspace_t; * 生命周期 * ================================================================ */ -// Files: (ART, grows), .sbo.head, .sbo.data. data_size = 8*64^k, create only. +// Files: , .sbo.head, .sbo.data; all grow on demand. +// data_size = 8*64^k, create only, initial (not maximum) data size. kvspace_t *kvspaceShmOpen(const char *path, size_t data_size); void kvspaceShmClose(kvspace_t *kv); diff --git a/tests/test_shm_resize.c b/tests/test_shm_resize.c new file mode 100644 index 0000000..57f14b9 --- /dev/null +++ b/tests/test_shm_resize.c @@ -0,0 +1,343 @@ +/* test_shm_resize [dir] — sbo head pool doubling, data x64 re-root, PUNCH_HOLE + * (#15) */ + +#define _GNU_SOURCE +#include "kvspace_shm.h" +#include "test_util.h" +#include +#include +#include +#include +#include +#include +#include + +#define DATA_2MB (8UL * 64 * 64 * 64) +#define DATA_128MB (8UL * 64 * 64 * 64 * 64) +#define HEAD_POOL_INIT (4UL * 1024 * 1024) +/* 4MB pool / 347B stride ~= 12085 blocks. A TLV of 257..504B occupies one L1 + box alone (> 32 of 64 slots, still level 1), so 14000 values overflow the + pool. Head is 28B today. */ +#define HEAD_KEYS 14000 +#define HEAD_VAL_RAW 440 +/* 900KB = 29 slots of 32KB; the 2MB root has 64, so the third one re-roots. */ +#define BIG_RAW (900 * 1024) + +static double now_s(void) { + struct timespec t; + clock_gettime(CLOCK_MONOTONIC, &t); + return (double)t.tv_sec + (double)t.tv_nsec * 1e-9; +} + +/* uint8 array, raw[j] = seed + j */ +static int set_bytes(kvspace_t *kv, const char *key, int32_t n, int seed) { + uint8_t *raw = malloc((size_t)n); + for (int32_t j = 0; j < n; j++) + raw[j] = (uint8_t)(seed + j); + uint8_t *tlv; + int32_t dims[1] = {n}; + int32_t len = kvspaceXvalueEncode(KVSPACE_KIND_UINT8, raw, n, dims, 1, &tlv); + int rc = kvspaceShmSet(kv, key, tlv, len); + free(tlv); + free(raw); + return rc; +} + +/* 0 if present with the exact length and content */ +static int check_bytes(kvspace_t *kv, const char *key, int32_t n, int seed) { + int32_t len = 0; + uint8_t *d = kvspaceShmGet(kv, key, 1, &len); + if (!d || len <= 0) + return 1; + xvalue_head_t h = kvspaceXvalueDecodeHead(d, len); + if (h.raw_len != n) + return 2; + for (int32_t j = 0; j < n; j++) + if (h.raw[j] != (uint8_t)(seed + j)) + return 3; + return 0; +} + +static void head_key(char *buf, size_t n, int i) { + snprintf(buf, n, "/h/%05d", i); +} + +/* returns failure count */ +static int fill_head(kvspace_t *kv) { + char key[64]; + int fail = 0; + for (int i = 0; i < HEAD_KEYS; i++) { + head_key(key, sizeof key, i); + fail += set_bytes(kv, key, HEAD_VAL_RAW, i) != 0; + } + return fail; +} + +/* returns mismatch count over every step-th key */ +static int check_head(kvspace_t *kv, int step) { + char key[64]; + int bad = 0; + for (int i = 0; i < HEAD_KEYS; i += step) { + head_key(key, sizeof key, i); + bad += check_bytes(kv, key, HEAD_VAL_RAW, i) != 0; + } + return bad; +} + +static void t_head_grow(const char *dir) { + printf("[head] %d values fill the 4MB box pool, grow, verify across reopen\n", + HEAD_KEYS); + char db[512], head[600], data[600]; + snprintf(db, sizeof db, "%s/hd", dir); + snprintf(head, sizeof head, "%s.sbo.head", db); + snprintf(data, sizeof data, "%s.sbo.data", db); + kvspace_t *kv = kvspaceShmOpen(db, DATA_128MB); + REQUIRE(kv != NULL, "open failed"); + off_t before = fsize(head); + CHECK(before > (off_t)HEAD_POOL_INIT && before < (off_t)HEAD_POOL_INIT + 4096, + "initial head size %ld", (long)before); + + double t0 = now_s(); + int fail = fill_head(kv); + CHECK(fail == 0, "%d sets failed", fail); + off_t after = fsize(head); + CHECK(after >= 2 * before - 4096, "head file did not grow: %ld -> %ld", + (long)before, (long)after); + CHECK(fsize(data) == (off_t)DATA_128MB, "data grew spuriously: %ld", + (long)fsize(data)); + printf(" head file %ld -> %ld bytes, %.2fs\n", (long)before, (long)after, + now_s() - t0); + int bad = check_head(kv, 3); + CHECK(bad == 0, "%d values unreadable after head grow", bad); + kvspaceShmClose(kv); + + kv = kvspaceShmOpen(db, 8); + REQUIRE(kv != NULL, "reopen failed"); + CHECK(fsize(head) == after, "reopen changed head size"); + bad = check_head(kv, 5); + CHECK(bad == 0, "%d values unreadable after reopen", bad); + CHECK(set_bytes(kv, "/h/after", HEAD_VAL_RAW, 7) == 0, "set after reopen"); + CHECK(check_bytes(kv, "/h/after", HEAD_VAL_RAW, 7) == 0, "get after reopen"); + kvspaceShmClose(kv); +} + +static void t_data_grow(const char *dir) { + printf("[data] third 900KB value in a 2MB store re-roots to 128MB, old " + "values intact, verify across reopen\n"); + char db[512], data[600], head[600]; + snprintf(db, sizeof db, "%s/dg", dir); + snprintf(data, sizeof data, "%s.sbo.data", db); + snprintf(head, sizeof head, "%s.sbo.head", db); + kvspace_t *kv = kvspaceShmOpen(db, DATA_2MB); + REQUIRE(kv != NULL, "open failed"); + CHECK(fsize(data) == (off_t)DATA_2MB, "initial data size %ld", + (long)fsize(data)); + off_t head_before = fsize(head); + + CHECK(set_bytes(kv, "/small", 100, 1) == 0, "set /small"); + CHECK(set_bytes(kv, "/big/1", BIG_RAW, 11) == 0, "set /big/1"); + CHECK(set_bytes(kv, "/big/2", BIG_RAW, 22) == 0, "set /big/2"); + CHECK(fsize(data) == (off_t)DATA_2MB, "grew too early: %ld", + (long)fsize(data)); + + CHECK(set_bytes(kv, "/big/3", BIG_RAW, 33) == 0, "set /big/3 (grow)"); + CHECK(fsize(data) == (off_t)DATA_128MB, "data did not grow to 128MB: %ld", + (long)fsize(data)); + CHECK(fsize(head) == head_before, "head grew spuriously: %ld -> %ld", + (long)head_before, (long)fsize(head)); + printf(" data file %lu -> %ld bytes\n", DATA_2MB, (long)fsize(data)); + + CHECK(check_bytes(kv, "/small", 100, 1) == 0, "/small after grow"); + CHECK(check_bytes(kv, "/big/1", BIG_RAW, 11) == 0, "/big/1 after grow"); + CHECK(check_bytes(kv, "/big/2", BIG_RAW, 22) == 0, "/big/2 after grow"); + CHECK(check_bytes(kv, "/big/3", BIG_RAW, 33) == 0, "/big/3 after grow"); + + /* freed space in the old subtree is reused; 3MB fits the new root */ + CHECK(kvspaceShmDel(kv, "/big/1") == 0, "del /big/1"); + CHECK(set_bytes(kv, "/big/1b", BIG_RAW, 44) == 0, "set /big/1b"); + CHECK(set_bytes(kv, "/huge", 3 * 1024 * 1024, 55) == 0, "set /huge 3MB"); + CHECK(fsize(data) == (off_t)DATA_128MB, "unexpected second grow"); + CHECK(check_bytes(kv, "/huge", 3 * 1024 * 1024, 55) == 0, "/huge"); + kvspaceShmClose(kv); + + kv = kvspaceShmOpen(db, 8); + REQUIRE(kv != NULL, "reopen failed"); + CHECK(fsize(data) == (off_t)DATA_128MB, "reopen changed data size"); + CHECK(check_bytes(kv, "/small", 100, 1) == 0, "/small after reopen"); + CHECK(check_bytes(kv, "/big/2", BIG_RAW, 22) == 0, "/big/2 after reopen"); + CHECK(check_bytes(kv, "/big/3", BIG_RAW, 33) == 0, "/big/3 after reopen"); + CHECK(check_bytes(kv, "/big/1b", BIG_RAW, 44) == 0, "/big/1b after reopen"); + CHECK(check_bytes(kv, "/huge", 3 * 1024 * 1024, 55) == 0, + "/huge after reopen"); + CHECK(set_bytes(kv, "/big/4", BIG_RAW, 66) == 0, "set after reopen"); + CHECK(check_bytes(kv, "/big/4", BIG_RAW, 66) == 0, "get after reopen"); + kvspaceShmClose(kv); +} + +/* child attaches at 2MB / 4MB pool; parent re-roots and grows the pool; + child then reads the new area and allocates from the grown pool */ +static void t_cross_process(const char *dir) { + printf("[xproc] one process re-roots and grows the pool, another syncs and " + "reads/writes\n"); + char db[512], head[600]; + snprintf(db, sizeof db, "%s/xp", dir); + snprintf(head, sizeof head, "%s.sbo.head", db); + kvspace_t *kv = kvspaceShmOpen(db, DATA_2MB); + REQUIRE(kv != NULL, "open failed"); + CHECK(set_bytes(kv, "/seed", 16, 1) == 0, "seed"); + + int to_child[2], to_parent[2]; + REQUIRE(pipe(to_child) == 0 && pipe(to_parent) == 0, "pipe"); + pid_t pid = fork(); + REQUIRE(pid >= 0, "fork"); + if (pid == 0) { + close(to_child[1]); + close(to_parent[0]); + kvspace_t *ckv = kvspaceShmOpen(db, DATA_2MB); + if (!ckv) + _exit(10); + if (check_bytes(ckv, "/seed", 16, 1) != 0) + _exit(11); + char c = 'r'; + if (write(to_parent[1], &c, 1) != 1) + _exit(12); + if (read(to_child[0], &c, 1) != 1) + _exit(13); + if (check_bytes(ckv, "/big/3", BIG_RAW, 33) != 0) + _exit(14); + if (check_bytes(ckv, "/big/1", BIG_RAW, 11) != 0) + _exit(15); + char key[64]; + head_key(key, sizeof key, HEAD_KEYS - 1); + if (check_bytes(ckv, key, HEAD_VAL_RAW, HEAD_KEYS - 1) != 0) + _exit(16); + if (set_bytes(ckv, "/from-child", BIG_RAW, 99) != 0) + _exit(17); + for (int i = 0; i < 100; i++) { + snprintf(key, sizeof key, "/c/%03d", i); + if (set_bytes(ckv, key, HEAD_VAL_RAW, i) != 0) + _exit(18); + } + kvspaceShmClose(ckv); + _exit(0); + } + close(to_child[0]); + close(to_parent[1]); + char c; + CHECK(read(to_parent[0], &c, 1) == 1 && c == 'r', "child ready"); + + CHECK(set_bytes(kv, "/big/1", BIG_RAW, 11) == 0, "set /big/1"); + CHECK(set_bytes(kv, "/big/2", BIG_RAW, 22) == 0, "set /big/2"); + CHECK(set_bytes(kv, "/big/3", BIG_RAW, 33) == 0, "set /big/3"); + off_t head_before = fsize(head); + int fail = fill_head(kv); + CHECK(fail == 0, "%d sets failed", fail); + CHECK(fsize(head) > head_before, "parent did not grow head pool"); + c = 'g'; + CHECK(write(to_child[1], &c, 1) == 1, "signal child"); + + int status = 0; + CHECK(waitpid(pid, &status, 0) == pid, "waitpid"); + CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0, + "child exit status %d (signal %d)", + WIFEXITED(status) ? WEXITSTATUS(status) : -1, + WIFSIGNALED(status) ? WTERMSIG(status) : 0); + CHECK(check_bytes(kv, "/from-child", BIG_RAW, 99) == 0, + "parent sees child's write"); + CHECK(check_bytes(kv, "/c/099", HEAD_VAL_RAW, 99) == 0, + "parent sees child's small writes"); + kvspaceShmClose(kv); +} + +static int punch_supported(const char *dir) { + char p[600]; + snprintf(p, sizeof p, "%s/punch-probe", dir); + int fd = open(p, O_RDWR | O_CREAT | O_TRUNC, 0644); + if (fd < 0) + return 0; + int ok = ftruncate(fd, 1 << 20) == 0 && + fallocate(fd, FALLOC_FL_PUNCH_HOLE | FALLOC_FL_KEEP_SIZE, 0, + 1 << 20) == 0; + close(fd); + unlink(p); + return ok; +} + +static void t_punch(const char *dir) { + printf("[punch] deleting a 1MB value releases the data file's pages\n"); + if (!punch_supported(dir)) { + printf(" (filesystem lacks PUNCH_HOLE, skipped)\n"); + return; + } + char db[512], data[600]; + snprintf(db, sizeof db, "%s/pn", dir); + snprintf(data, sizeof data, "%s.sbo.data", db); + kvspace_t *kv = kvspaceShmOpen(db, DATA_2MB); + REQUIRE(kv != NULL, "open failed"); + off_t base = fphys(data); + CHECK(set_bytes(kv, "/v", BIG_RAW, 5) == 0, "set /v"); + off_t used = fphys(data); + CHECK(used - base >= (off_t)BIG_RAW - 65536, "phys after write %ld -> %ld", + (long)base, (long)used); + CHECK(kvspaceShmDel(kv, "/v") == 0, "del /v"); + off_t freed = fphys(data); + CHECK(freed <= base + 65536, "phys after del %ld (base %ld)", (long)freed, + (long)base); + printf(" phys %ld -> %ld -> %ld bytes\n", (long)base, (long)used, + (long)freed); + /* growing overwrite goes through free+alloc: old box punched, new readable */ + CHECK(set_bytes(kv, "/w", BIG_RAW, 6) == 0, "set /w"); + CHECK(set_bytes(kv, "/w", BIG_RAW + 65536, 7) == 0, "overwrite /w bigger"); + CHECK(check_bytes(kv, "/w", BIG_RAW + 65536, 7) == 0, "/w after overwrite"); + kvspaceShmClose(kv); +} + +/* growth code addresses the single-root pool layout; anything else is refused + */ +static void t_bad_root_slots(const char *dir) { + printf("[root_slots] head file with root_slots != 1 is rejected\n"); + char db[512], head[600]; + snprintf(db, sizeof db, "%s/rs", dir); + snprintf(head, sizeof head, "%s.sbo.head", db); + kvspace_t *kv = kvspaceShmOpen(db, DATA_2MB); + REQUIRE(kv != NULL, "open failed"); + kvspaceShmClose(kv); + /* sbo_meta_t: magic[16] head_size data_size slot_bytes per_slot_meta -> 48 */ + int fd = open(head, O_RDWR); + REQUIRE(fd >= 0, "open head"); + uint8_t one = 0; + REQUIRE(pread(fd, &one, 1, 48) == 1 && one == 1, "root_slots byte is %u", + one); + uint8_t two = 2; + CHECK(pwrite(fd, &two, 1, 48) == 1, "pwrite"); + CHECK(kvspaceShmOpen(db, DATA_2MB) == NULL, "root_slots=2 accepted"); + CHECK(pwrite(fd, &one, 1, 48) == 1, "restore"); + close(fd); + kv = kvspaceShmOpen(db, DATA_2MB); + CHECK(kv != NULL, "reopen after restore"); + kvspaceShmClose(kv); +} + +int main(int argc, char **argv) { + char tmpl[] = "/tmp/kvspace-resize-XXXXXX"; + const char *dir = argc > 1 ? argv[1] : mkdtemp(tmpl); + if (!dir) { + perror("mkdtemp"); + return 2; + } + + t_data_grow(dir); + t_cross_process(dir); + t_punch(dir); + t_bad_root_slots(dir); + t_head_grow(dir); + + if (argc <= 1) { + char cmd[700]; + snprintf(cmd, sizeof cmd, "rm -rf '%s'", dir); + if (system(cmd) != 0) + fprintf(stderr, "cleanup failed: %s\n", dir); + } + printf(failures ? "FAILED (%d)\n" : "OK\n", failures); + return failures ? 1 : 0; +} diff --git a/tests/test_shm_split.c b/tests/test_shm_split.c index d8f4a57..7a166b5 100644 --- a/tests/test_shm_split.c +++ b/tests/test_shm_split.c @@ -2,11 +2,11 @@ #define _GNU_SOURCE #include "kvspace_shm.h" +#include "test_util.h" #include #include #include #include -#include #include #include @@ -16,28 +16,6 @@ #define GROW_KEYS 3000 #define GROW_KEY_LEN 600 -static int failures = 0; -#define CHECK(cond, ...) \ - do { \ - if (!(cond)) { \ - failures++; \ - fprintf(stderr, " FAIL %s:%d: ", __FILE__, __LINE__); \ - fprintf(stderr, __VA_ARGS__); \ - fprintf(stderr, "\n"); \ - } \ - } while (0) -#define REQUIRE(cond, ...) \ - do { \ - CHECK(cond, __VA_ARGS__); \ - if (!(cond)) \ - return; \ - } while (0) - -static off_t fsize(const char *p) { - struct stat st; - return stat(p, &st) == 0 ? st.st_size : -1; -} - /* unique prefix + random tail, no shared prefix between keys */ static void key_of(char *buf, size_t n, int i) { unsigned x = (unsigned)i * 2654435761u + 12345u; diff --git a/tests/test_util.h b/tests/test_util.h new file mode 100644 index 0000000..4b12884 --- /dev/null +++ b/tests/test_util.h @@ -0,0 +1,37 @@ +/* Shared test helpers: failure counter, CHECK/REQUIRE, file size probes. */ + +#ifndef TEST_UTIL_H +#define TEST_UTIL_H + +#include +#include + +static int failures = 0; +#define CHECK(cond, ...) \ + do { \ + if (!(cond)) { \ + failures++; \ + fprintf(stderr, " FAIL %s:%d: ", __FILE__, __LINE__); \ + fprintf(stderr, __VA_ARGS__); \ + fprintf(stderr, "\n"); \ + } \ + } while (0) +#define REQUIRE(cond, ...) \ + do { \ + CHECK(cond, __VA_ARGS__); \ + if (!(cond)) \ + return; \ + } while (0) + +static inline off_t fsize(const char *p) { + struct stat st; + return stat(p, &st) == 0 ? st.st_size : -1; +} + +/* allocated bytes, not apparent size */ +static inline off_t fphys(const char *p) { + struct stat st; + return stat(p, &st) == 0 ? (off_t)st.st_blocks * 512 : -1; +} + +#endif