From 1fcbec653f24ca964915308f7f695a8607051e01 Mon Sep 17 00:00:00 2001 From: miaobyte Date: Sun, 1 Nov 2026 23:23:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20v0.3.23=20ABI=20=E5=A5=91=E7=BA=A6?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8E=E7=BB=91=E5=AE=9A=E5=90=8C=E6=AD=A5?= =?UTF-8?q?=EF=BC=88review=20PR=20#67=EF=BC=8C=E5=86=B2=E7=AA=81=E5=8F=96?= =?UTF-8?q?=20PR=20=E6=96=B9=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按用户裁决把 review PR #67 落到当前 main:冲突一律取 PR 方。 - shm/ABI 契约修复、List 遍历缓存、DelTree、Watch(None) - xvalue_head 有界 langtype 读取与构造失败契约 - Python/Rust 绑定对齐 #571 wire 格式 - make test + CI 跑测试套件,deps.json pin kvspace v0.3.23 已知后果(提交历史留档):body-only 守卫让位给 PR 口径后,kvspace-c 的 SetPart/SetPartByPart 仅受整值范围与物理容量约束(仅 ViewWrite 仍等长), 没有任何入口阻止写 head/attr —— 见 ci-followups ⑪。 Co-Authored-By: Claude Code --- .github/workflows/ci-macos.yml | 6 + .github/workflows/ci.yml | 4 + .github/workflows/release.yml | 6 +- .gitignore | 1 + CMakeLists.txt | 13 +- Makefile | 10 +- README.md | 29 +- build.log | 48 + build2.log | 40 + build3.log | 20 + cfg.log | 15 + deps.json | 1 + durable_abi.c | 486 +++++ kvspace.c | 3754 ++++++++++++++++++++++++++++++++ kvspace_shm.h | 166 ++ py/kvspace/__init__.py | 140 +- py/tests/test_wire.py | 152 +- rust/src/lib.rs | 60 +- rust/tests/wire.rs | 183 +- src/durable_abi.c | 147 +- src/kvspace.c | 930 +++++--- src/kvspace_shm.h | 37 +- src/xvalue_head.c | 83 +- src/xvalue_head.h | 39 +- tests/test_abi_contract.c | 522 +++++ tests/test_art_alignment.c | 36 +- tests/test_codec_abi.c | 3 + tests/test_inplace_body_only.c | 18 +- tests/test_xvalue_head.c | 39 + 29 files changed, 6226 insertions(+), 762 deletions(-) create mode 100644 build.log create mode 100644 build2.log create mode 100644 build3.log create mode 100644 cfg.log create mode 100644 durable_abi.c create mode 100644 kvspace.c create mode 100644 kvspace_shm.h create mode 100644 tests/test_abi_contract.c diff --git a/.github/workflows/ci-macos.yml b/.github/workflows/ci-macos.yml index a551409..e58ea13 100644 --- a/.github/workflows/ci-macos.yml +++ b/.github/workflows/ci-macos.yml @@ -20,6 +20,12 @@ jobs: -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local cmake --build /tmp/slotsboxmalloc/build -j sudo cmake --install /tmp/slotsboxmalloc/build + - name: Install kvspace ABI headers (version from deps.json) + run: | + ver=$(jq -r '.kvspace' deps.json) + git clone --depth 1 --branch "$ver" https://github.com/array2d/kvspace.git /tmp/kvspace + sudo mkdir -p /usr/local/include + sudo cp -R /tmp/kvspace/include/. /usr/local/include/ - name: 构建 run: | cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=/usr/local \ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3cf5d78..748124e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,3 +10,7 @@ jobs: run: ./ci/deps.sh - name: 构建 run: make + - name: Run C tests + run: make test + - name: Run Python wrapper tests + run: KVSPACE_C_SO=build/libkvspace-c.so python3 py/tests/test_wire.py diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 59de6f7..b320db0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,8 +30,10 @@ jobs: v="${RELEASE_TAG:-${GITHUB_REF_NAME}}" d="kvspace-c-abi-$v-linux-x86_64" mkdir -p "$d/include" "$d/lib" - # 头:本仓无公共头(内部头在 src/),权威 ABI 头在 kvspace 仓 → 递归整目录 - git clone --depth 1 https://github.com/array2d/kvspace.git /tmp/kvspace-hdr + # Copy authoritative ABI headers from the pinned kvspace tag in deps.json. + kvspace_ver=$(jq -r '.kvspace' deps.json) + test -n "$kvspace_ver" && test "$kvspace_ver" != null + git clone --depth 1 --branch "$kvspace_ver" https://github.com/array2d/kvspace.git /tmp/kvspace-hdr cp -R /tmp/kvspace-hdr/include/. "$d/include/" # 库:扫构建目录的所有 .so*/.dylib(-P 保符号链接) find build -maxdepth 3 \( -name 'lib*.so*' -o -name 'lib*.dylib' \) -exec cp -P {} "$d/lib/" \; diff --git a/.gitignore b/.gitignore index 5569c0b..8f48f55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +build-test/ cmake-build-*/ *.o *.a diff --git a/CMakeLists.txt b/CMakeLists.txt index fa35485..9bcfbff 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -65,6 +65,14 @@ if(KVSPACE_BUILD_TESTS) target_include_directories(test_codec_abi PRIVATE src) target_link_libraries(test_codec_abi PRIVATE kvspace-c) add_test(NAME codec_abi COMMAND test_codec_abi) + add_executable(test_abi_contract tests/test_abi_contract.c) + target_include_directories(test_abi_contract PRIVATE src) + target_link_libraries(test_abi_contract PRIVATE kvspace-c) + add_test(NAME abi_contract COMMAND test_abi_contract) + add_executable(test_inplace_body_only tests/test_inplace_body_only.c) + target_include_directories(test_inplace_body_only PRIVATE src) + target_link_libraries(test_inplace_body_only PRIVATE kvspace-c) + add_test(NAME inplace_body_only COMMAND test_inplace_body_only) add_executable(test_set_part_fastpath tests/test_set_part_fastpath.c) target_include_directories(test_set_part_fastpath PRIVATE src) target_link_libraries(test_set_part_fastpath PRIVATE kvspace-c) @@ -80,11 +88,6 @@ if(KVSPACE_BUILD_TESTS) target_link_libraries(test_slot_view PRIVATE kvspace-c) add_test(NAME slot_view COMMAND test_slot_view) - add_executable(test_inplace_body_only tests/test_inplace_body_only.c) - target_include_directories(test_inplace_body_only PRIVATE src) - target_link_libraries(test_inplace_body_only PRIVATE kvspace-c) - add_test(NAME inplace_body_only COMMAND test_inplace_body_only) - add_executable(test_art_alignment tests/test_art_alignment.c) target_include_directories(test_art_alignment PRIVATE src) target_compile_definitions(test_art_alignment PRIVATE P2_TEST_HOOKS) diff --git a/Makefile b/Makefile index bbf59ee..59ef6f6 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,13 @@ -.PHONY: build clean +.PHONY: build test clean build: @mkdir -p build && cd build && cmake -DCMAKE_BUILD_TYPE=Release .. && make -j$$(nproc) +# Tests need the kvspace ABI headers () on the include path. +test: + @mkdir -p build-test && cd build-test && \ + cmake -DCMAKE_BUILD_TYPE=Debug -DKVSPACE_BUILD_TESTS=ON .. && \ + make -j$$(nproc) && ctest --output-on-failure + clean: - rm -rf build + rm -rf build build-test diff --git a/README.md b/README.md index 8d70d79..6dd8b87 100644 --- a/README.md +++ b/README.md @@ -12,15 +12,18 @@ Backend: `shm://` — single file-backed mmap block (ART-tree key index + slotsb ```bash make # → build/libkvspace-c.so +make test # → build-test/, runs ctest (needs the kvspace ABI headers) ``` -Dependencies: [`blockmalloc`](../blockmalloc), [`slotsboxmalloc`](../slotsboxmalloc) (header-only + `.so` dual-mode libraries). +Dependencies: [`blockmalloc`](../blockmalloc), [`slotsboxmalloc`](../slotsboxmalloc) (header-only, compiled into this library). +Tests additionally include the authoritative ABI header `` from [`kvspace`](../kvspace) (`include/`). +`ci/deps.sh` installs all three at the versions pinned in `deps.json`. ## API Two surfaces: -1. **Native C API** (`kvspaceShm*`) — `include/kvspace/kvspace.h`: +1. **Native C API** (`kvspaceShm*`) — `src/kvspace_shm.h` (internal header): `kvspaceShmOpen/Close`, `kvspaceShmGet/Set`, `kvspaceShmList`, `kvspaceShmDel/Deltree`, `kvspaceShmMkindex`, `kvspaceShmExtindex/Delextindex`, `kvspaceShmNotify/Watch`. 2. **durable C ABI** — byte-compatible with `kvspace-durable` (`src/durable_abi.c`): @@ -33,24 +36,30 @@ Two surfaces: ## XValue -Fixed 8-byte head wire format (`#566`), read/written by direct `memcpy` of the -`kvspaceXvalueHead` struct: +Fixed 8-byte head (`#566`) followed by the body and an attr region (`#571`), stored as one value under one key: ``` -[len:u48 le @0-5][storetype:u8 @6][ptr:u8 @7][body @8] +[len:u48 le @0-5][storetype:u8 @6][ptr:u8 @7][body @8][attr @8+len] +attr = [attr_len:u16 le][records] attr_len includes itself; empty attr = 2 +record = [tag:u8][len:u16 le][data] tag 1 = langtype (kindexpr); 2+ reserved +total = 8 + len + attr_len ``` - storetype selects short, slack, large, or extension storage; ptr marks pointer/ext. -- langtype (kindexpr) is not in the head — it lives in the `x‥langtype` attr key. -- The head is built with `kvspaceXvalueHeadNew*` constructors. +- langtype (kindexpr) is the tag-1 record of the attr region; no record = untyped. +- Values are built with the `kvspaceXvalueHeadNew*` constructors. `kvspaceSetValue` + rejects values whose attr region is missing or runs past the value. +- Store format version 6 (this wire). Stores written by older versions are refused at open; rebuild them. -## Tutorial +## Tests ```bash -python3 tutorial/test.py +make test # C tests (ctest) +KVSPACE_C_SO=build/libkvspace-c.so python3 py/tests/test_wire.py +(cd rust && cargo test) # links ../build ``` -Cases: `01_basic.c`, `02_cpp.cpp`, `03_python.py`, `04_rust.rs`, `05_integrity.c`, `06_multiprocess.c`. +Cross-language tutorials live in the [`kvspace`](../kvspace) repo (`tutorial-c/`). ## Language wrappers diff --git a/build.log b/build.log new file mode 100644 index 0000000..0215022 --- /dev/null +++ b/build.log @@ -0,0 +1,48 @@ +[ 3%] Building C object CMakeFiles/test_xvalue_head.dir/tests/test_xvalue_head.c.o +[ 6%] Building C object CMakeFiles/test_xvalue_head.dir/src/xvalue_head.c.o +[ 13%] Building C object CMakeFiles/kvspace-c.dir/src/xvalue_head.c.o +[ 13%] Building C object CMakeFiles/kvspace-c.dir/src/coord.c.o +[ 16%] Building C object CMakeFiles/kvspace-c.dir/src/kvspace.c.o +[ 20%] Building C object CMakeFiles/kvspace-c.dir/src/durable_abi.c.o +[ 23%] Linking C executable test_xvalue_head +[ 23%] Built target test_xvalue_head +[ 26%] Linking C shared library libkvspace-c.so +[ 26%] Built target kvspace-c +[ 30%] Building C object CMakeFiles/test_shm_split.dir/tests/test_shm_split.c.o +[ 33%] Building C object CMakeFiles/test_shm_resize.dir/tests/test_shm_resize.c.o +[ 43%] Building C object CMakeFiles/test_codec_abi.dir/tests/test_codec_abi.c.o +[ 43%] Building C object CMakeFiles/test_handle_cache.dir/tests/test_handle_cache.c.o +[ 43%] Building C object CMakeFiles/test_art_scan.dir/tests/test_art_scan.c.o +[ 46%] Building C object CMakeFiles/test_abi_contract.dir/tests/test_abi_contract.c.o +[ 50%] Building C object CMakeFiles/test_inplace_body_only.dir/tests/test_inplace_body_only.c.o +[ 56%] Building C object CMakeFiles/test_set_part_fastpath.dir/tests/test_set_part_fastpath.c.o +[ 56%] Building C object CMakeFiles/test_new_wire_shm.dir/tests/test_new_wire_shm.c.o +[ 63%] Building C object CMakeFiles/test_art_alignment.dir/tests/test_art_alignment.c.o +[ 63%] Building C object CMakeFiles/test_slot_view.dir/tests/test_slot_view.c.o +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c: In function 'run': +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:46:18: warning: implicit declaration of function 'mkdtemp' [-Wimplicit-function-declaration] + 46 | char *made = mkdtemp(dir); + | ^~~~~~~ +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:46:18: warning: initialization of 'char *' from 'int' makes pointer from integer without a cast [-Wint-conversion] +[ 66%] Linking C executable test_art_alignment +[ 70%] Linking C executable test_slot_view +[ 73%] Linking C executable test_new_wire_shm +[ 76%] Linking C executable test_codec_abi +[ 80%] Linking C executable test_inplace_body_only +[ 83%] Linking C executable test_handle_cache +[ 86%] Linking C executable test_shm_split +[ 90%] Linking C executable test_set_part_fastpath +[ 93%] Linking C executable test_shm_resize +[ 96%] Linking C executable test_art_scan +[100%] Linking C executable test_abi_contract +[100%] Built target test_art_alignment +[100%] Built target test_codec_abi +[100%] Built target test_new_wire_shm +[100%] Built target test_slot_view +[100%] Built target test_inplace_body_only +[100%] Built target test_handle_cache +[100%] Built target test_set_part_fastpath +[100%] Built target test_shm_split +[100%] Built target test_shm_resize +[100%] Built target test_art_scan +[100%] Built target test_abi_contract diff --git a/build2.log b/build2.log new file mode 100644 index 0000000..0adca38 --- /dev/null +++ b/build2.log @@ -0,0 +1,40 @@ +[ 10%] Built target test_xvalue_head +[ 26%] Built target kvspace-c +[ 36%] Built target test_shm_resize +[ 33%] Building C object CMakeFiles/test_inplace_body_only.dir/tests/test_inplace_body_only.c.o +[ 43%] Built target test_art_scan +[ 50%] Built target test_handle_cache +[ 63%] Built target test_codec_abi +[ 63%] Built target test_shm_split +[ 70%] Built target test_abi_contract +[ 76%] Built target test_set_part_fastpath +[ 83%] Built target test_new_wire_shm +[ 90%] Built target test_slot_view +[ 96%] Built target test_art_alignment +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c: In function 'run': +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:46:18: warning: implicit declaration of function 'mkdtemp' [-Wimplicit-function-declaration] + 46 | char *made = mkdtemp(dir); + | ^~~~~~~ +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:46:18: warning: initialization of 'char *' from 'int' makes pointer from integer without a cast [-Wint-conversion] +In file included from /tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:5: +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:122:47: error: 'sp0' undeclared (first use in this function); did you mean 'sp2'? + 122 | REQUIRE(sp2.raw != NULL && sp2.len == sp0.len, "body write geometry"); + | ^~~ +/tmp/resolve_kvc/prside/tests/test_util.h:12:11: note: in definition of macro 'CHECK' + 12 | if (!(cond)) { \ + | ^~~~ +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:122:9: note: in expansion of macro 'REQUIRE' + 122 | REQUIRE(sp2.raw != NULL && sp2.len == sp0.len, "body write geometry"); + | ^~~~~~~ +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:122:47: note: each undeclared identifier is reported only once for each function it appears in + 122 | REQUIRE(sp2.raw != NULL && sp2.len == sp0.len, "body write geometry"); + | ^~~ +/tmp/resolve_kvc/prside/tests/test_util.h:12:11: note: in definition of macro 'CHECK' + 12 | if (!(cond)) { \ + | ^~~~ +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:122:9: note: in expansion of macro 'REQUIRE' + 122 | REQUIRE(sp2.raw != NULL && sp2.len == sp0.len, "body write geometry"); + | ^~~~~~~ +gmake[2]: *** [CMakeFiles/test_inplace_body_only.dir/build.make:76: CMakeFiles/test_inplace_body_only.dir/tests/test_inplace_body_only.c.o] Error 1 +gmake[1]: *** [CMakeFiles/Makefile2:315: CMakeFiles/test_inplace_body_only.dir/all] Error 2 +gmake: *** [Makefile:146: all] Error 2 diff --git a/build3.log b/build3.log new file mode 100644 index 0000000..b598a14 --- /dev/null +++ b/build3.log @@ -0,0 +1,20 @@ +[ 10%] Built target test_xvalue_head +[ 26%] Built target kvspace-c +[ 33%] Built target test_shm_split +[ 40%] Built target test_handle_cache +[ 43%] Building C object CMakeFiles/test_inplace_body_only.dir/tests/test_inplace_body_only.c.o +[ 50%] Built target test_art_scan +[ 56%] Built target test_abi_contract +[ 63%] Built target test_set_part_fastpath +[ 70%] Built target test_codec_abi +[ 76%] Built target test_slot_view +[ 86%] Built target test_art_alignment +[ 90%] Built target test_new_wire_shm +[ 96%] Built target test_shm_resize +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c: In function 'run': +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:46:18: warning: implicit declaration of function 'mkdtemp' [-Wimplicit-function-declaration] + 46 | char *made = mkdtemp(dir); + | ^~~~~~~ +/tmp/resolve_kvc/prside/tests/test_inplace_body_only.c:46:18: warning: initialization of 'char *' from 'int' makes pointer from integer without a cast [-Wint-conversion] +[100%] Linking C executable test_inplace_body_only +[100%] Built target test_inplace_body_only diff --git a/cfg.log b/cfg.log new file mode 100644 index 0000000..f9839f1 --- /dev/null +++ b/cfg.log @@ -0,0 +1,15 @@ +-- The C compiler identification is GNU 13.3.0 +-- The CXX compiler identification is GNU 13.3.0 +-- Detecting C compiler ABI info +-- Detecting C compiler ABI info - done +-- Check for working C compiler: /usr/bin/cc - skipped +-- Detecting C compile features +-- Detecting C compile features - done +-- Detecting CXX compiler ABI info +-- Detecting CXX compiler ABI info - done +-- Check for working CXX compiler: /usr/bin/c++ - skipped +-- Detecting CXX compile features +-- Detecting CXX compile features - done +-- Configuring done (0.5s) +-- Generating done (0.0s) +-- Build files have been written to: /tmp/resolve_kvc/prside/build diff --git a/deps.json b/deps.json index 3a2eaaf..df07c4b 100644 --- a/deps.json +++ b/deps.json @@ -1,4 +1,5 @@ { "blockmalloc": "v0.1.5", + "kvspace": "v0.3.23", "slotsboxmalloc": "v0.1.7" } diff --git a/durable_abi.c b/durable_abi.c new file mode 100644 index 0000000..575277f --- /dev/null +++ b/durable_abi.c @@ -0,0 +1,486 @@ +/* KVSpace SHM ABI. */ + +/* macOS 需 _DARWIN_C_SOURCE 才暴露 usleep 等完整 Darwin API;_POSIX_C_SOURCE ≥200112 + * 才在 macOS 暴露 C99 的 snprintf(Linux 走 glibc 的 _DEFAULT_SOURCE)。 */ +#if defined(__APPLE__) +#define _DARWIN_C_SOURCE +#endif +#define _POSIX_C_SOURCE 200809L +#define _DEFAULT_SOURCE + +#include "kvspace_shm.h" +#include "xvalue_head.h" +#include +#include +#include +#include +#include + +#include + +#define SHM_DEFAULT_SIZE (8ULL * 64 * 64 * 64 * 64) + +/* Mirrors the public ABI. */ +typedef struct { + uint16_t headlen; + uint8_t ref; + uint8_t storetype; + int32_t body_len; + int32_t ndim; + int32_t dims[8]; + char langtype[256]; + int32_t langtype_len; + int32_t body_offset; + uint64_t body_cap; +} kvspaceHead_t; + +/* 固定 8 字节 head → 元数据(langtype 在 body 尾 @8+len,见 kvspaceGetHead)。 */ +static int from_wire_head(const uint8_t *d, uint32_t total, kvspaceHead_t *out) { + if (!out || !d || total < KVSPACE_XVALUE_HEAD_LEN) + return 1; + memset(out, 0, sizeof *out); + kvspaceXvalueHead h; + memcpy(&h, d, sizeof h); + uint64_t l = kvspaceXvalueHeadLen(&h); + if (h.storetype > KVSPACE_XVALUE_ST_EXT || h.ptr > KVSPACE_XVALUE_PTR_EXT || + (uint64_t)KVSPACE_XVALUE_HEAD_LEN + l > total) + return 1; + out->headlen = KVSPACE_XVALUE_HEAD_LEN; + out->ref = h.ptr == KVSPACE_XVALUE_PTR_PTR ? 1 : + h.ptr == KVSPACE_XVALUE_PTR_EXT ? 2 : 0; + out->storetype = h.storetype; + out->body_len = l > INT32_MAX ? INT32_MAX : (int32_t)l; + out->body_offset = KVSPACE_XVALUE_HEAD_LEN; + out->body_cap = l; + return 0; +} + +static int parse_shm_path(const char *dsn, char *out, size_t osz) { + const char *sep = strstr(dsn, "://"); + if (!sep || strncmp(dsn, "shm", (size_t)(sep - dsn)) != 0) + return -1; + snprintf(out, osz, "%s", sep + 3); + return 0; +} + +void *kvspaceConnect(const char *dsn) { + char path[1024]; + if (parse_shm_path(dsn, path, sizeof path) != 0) + return NULL; + return (void *)kvspaceShmOpen(path, SHM_DEFAULT_SIZE); +} + +void kvspaceClose(void *h) { + if (h) + kvspaceShmClose((kvspace_t *)h); +} + +/* 借用读:*out 指向 SHM 常驻映射(生命周期同该槽),调用方不得 free。 */ +int kvspaceGet(void *h, const char *key, int resolve, uint8_t **out, + uint32_t *out_len) { + int32_t len = 0; + uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, resolve, &len); + if (!d || len <= 0) { + *out = NULL; + *out_len = 0; + return 0; + } + *out = d; + *out_len = (uint32_t)len; + return 0; +} + +int kvspaceSetValue(void *h, const char *key, const uint8_t *value, + uint32_t value_len, char *err, uint32_t err_cap) { + int rc = value_len > INT32_MAX ? -2 : + kvspaceShmSetValue((kvspace_t *)h, key, value, + (int32_t)value_len); + if (rc == -2) { + if (err && err_cap) + snprintf(err, err_cap, "invalid XValue"); + return 1; + } + if (rc != 0) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: set-value failed at %s", key ? key : ""); + return 1; + } + return 0; +} + +int kvspaceSetValueByRef(void *h, kvspaceRef_t *ref, const char *key_fallback, + const uint8_t *value, uint32_t value_len, + char *err, uint32_t err_cap) { + int rc = value_len > INT32_MAX + ? -2 + : kvspaceShmSetValueByRef((kvspace_t *)h, ref, key_fallback, + value, (int32_t)value_len); + if (rc == -2) { + if (err && err_cap) + snprintf(err, err_cap, "invalid XValue"); + return 1; + } + if (rc != 0) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: set-value-by-ref failed at %s", + key_fallback ? key_fallback : ""); + return 1; + } + return 0; +} + +int kvspaceResolveRef(void *h, const char *key, kvspaceRef_t *ref) { + return kvspaceShmResolveRef((kvspace_t *)h, key, ref); +} + +int kvspaceGetByRef(void *h, kvspaceRef_t *ref, const char *key_fallback, + uint8_t **out, uint32_t *out_len) { + int32_t len = 0; + uint8_t *d = kvspaceShmGetByRef((kvspace_t *)h, ref, key_fallback, &len); + if (!d || len <= 0) { + *out = NULL; + *out_len = 0; + return 0; + } + *out = d; + *out_len = (uint32_t)len; + return 0; +} + +int kvspaceXvalueBodyView(void *h, const kvspaceRef_t *ref, const char *key_fallback, + kvspaceXvalueBodyView_t *out) { + return kvspaceShmXvalueBodyView((kvspace_t *)h, ref, key_fallback, out); +} + +int kvspaceSetPartByRef(void *h, kvspaceRef_t *ref, const char *key_fallback, + uint32_t offset, const uint8_t *buf, uint32_t buf_len, + char *err, uint32_t err_cap) { + if (kvspaceShmSetPartByRef((kvspace_t *)h, ref, key_fallback, offset, buf, + buf_len) != 0) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: set-part-by-ref failed"); + return 1; + } + return 0; +} + +/* 定位读:*out 指向 SHM 常驻映射内 [offset, offset+len)(借用,不得 free)。 + * 越界/空 → *out=NULL、out_len=0。 */ +int kvspaceGetPart(void *h, const char *key, uint32_t offset, uint32_t len, + uint8_t **out, uint32_t *out_len) { + int32_t total = 0; + uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &total); + if (!d || total <= 0 || offset >= (uint32_t)total) { + *out = NULL; + *out_len = 0; + return 0; + } + uint32_t avail = (uint32_t)total - offset; + *out = d + offset; + *out_len = len < avail ? len : avail; + return 0; +} + +/* 定位写:就地 memcpy buf 到 SHM 映射内 [offset, offset+buf_len)(key 须已存在、不改结构)。 */ +int kvspaceSetPart(void *h, const char *key, uint32_t offset, + const uint8_t *buf, uint32_t buf_len, char *err, + uint32_t err_cap) { + int32_t total = 0; + uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &total); + if (!d || total < 0 || !kvspaceShmPartInBody(d, total, offset, buf_len)) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: set-part out of range at %s", key); + return 1; + } + memcpy(d + offset, buf, buf_len); + return 0; +} + +/* 读 head:取值前缀的三正交轴 head(不取 body)。空/不存在 → 返回 1。 */ +int kvspaceGetHead(void *h, const char *key, kvspaceHead_t *out) { + int32_t total = 0; + uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &total); + if (!d || total <= 0) + return 1; + if (from_wire_head(d, (uint32_t)total, out) != 0) + return 1; + /* langtype 在 attr 区 tag=1 记录(@8+len 自描述);无记录 = 无类型。 */ + uint64_t blen = (uint64_t)out->body_len; + const uint8_t *lt = NULL; + uint16_t ltl = kvspaceXvalueHeadLangtype(d, blen, <); + if (lt && ltl > 0) { + size_t take = (size_t)ltl < (uint64_t)sizeof out->langtype - 1 + ? (size_t)ltl + : (size_t)sizeof out->langtype - 1; + memcpy(out->langtype, lt, take); + out->langtype[take] = 0; + out->langtype_len = (int32_t)take; + } else { + out->langtype_len = 0; + } + return 0; +} + +/* 就地写:返回原 box body 偏移指针;前置条件不满足 → 非 0 + err。 */ +int kvspaceWriteInPlace(void *h, const char *key, int resolve, + uint32_t body_len, uint8_t **body, char *err, + uint32_t err_cap) { + if (kvspaceShmWriteInPlace((kvspace_t *)h, key, resolve, (int32_t)body_len, + body) != 0) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: write-in-place rejected at %s", key); + return 1; + } + return 0; +} + +/* 新位置写:按 (ref, storetype, body_len, body_cap, langtype) 分配 box、写固定头 + + * langtype 尾部(整体单写),返回 body 偏移指针。 */ +int kvspaceWriteNewPlace(void *h, const char *key, uint8_t ref, + uint8_t storetype, uint32_t body_len, uint64_t body_cap, + const char *langtype, + uint8_t **body, char *err, uint32_t err_cap) { + if (kvspaceShmWriteNewPlace((kvspace_t *)h, key, ref, storetype, + (int32_t)body_len, body_cap, langtype, + body) != 0) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: write-new-place failed at %s", key); + return 1; + } + return 0; +} + +/* 只返回前缀下子项计数,无缓冲、无需释放。 */ +int kvspaceListLen(void *h, const char *prefix, int expand_ext, int resolve, + int32_t *out_count) { + return kvspaceShmListLen((kvspace_t *)h, prefix, expand_ext != 0, resolve, + out_count); +} + +/* 索引取项:把前缀下第 idx 个直接子项名写进调用方自备缓冲 buf(容量 + buf_cap),*out_len 置该名长度(不含 NUL)。库侧零状态。idx 越界或缓冲不足 → + 返回非 0(缓冲不足时 *out_len 仍为所需长度,不静默截断)。配合 kvspaceListLen + 遍历。 */ +int kvspaceListAt(void *h, const char *prefix, int expand_ext, int resolve, + int32_t idx, uint8_t *buf, uint32_t buf_cap, + uint32_t *out_len) { + *out_len = 0; + char **names = NULL; + int32_t count = 0; + if (kvspaceShmList((kvspace_t *)h, prefix, expand_ext != 0, resolve, &names, + &count) != 0) + return -1; + int rc = -1; + if (idx >= 0 && idx < count) { + size_t l = strlen(names[idx]); + *out_len = (uint32_t)l; + if (buf && l <= buf_cap) { + memcpy(buf, names[idx], l); + rc = 0; + } + } + for (int32_t i = 0; i < count; i++) + free(names[i]); + free(names); + return rc; +} + +int kvspaceDel(void *h, const char *const *keys, uint32_t nkeys, char *err, + uint32_t err_cap) { + (void)err; + (void)err_cap; + for (uint32_t i = 0; i < nkeys; i++) + kvspaceShmDel((kvspace_t *)h, keys[i]); + return 0; +} + +int kvspaceDelTree(void *h, const char *prefix, char *err, uint32_t err_cap) { + (void)err; + (void)err_cap; + return kvspaceShmDeltree((kvspace_t *)h, prefix); +} + +int kvspaceCp(void *h, const char *src, const char *dst, char *err, + uint32_t err_cap) { + (void)err; + (void)err_cap; + return kvspaceShmCp((kvspace_t *)h, src, dst); +} + +int kvspaceCpTree(void *h, const char *src, const char *dst, char *err, + uint32_t err_cap) { + (void)err; + (void)err_cap; + return kvspaceShmCptree((kvspace_t *)h, src, dst); +} + +int kvspaceCpList(void *h, const char *src, const char *dst, char *err, + uint32_t err_cap) { + (void)err; + (void)err_cap; + return kvspaceShmCplist((kvspace_t *)h, src, dst); +} + +int kvspaceMkindex(void *h, const char *path, uint32_t capacity, char *err, + uint32_t err_cap) { + (void)err; + (void)err_cap; + return kvspaceShmMkindex((kvspace_t *)h, path, capacity); +} + +int kvspaceMkindexExt(void *h, const char *path, const char *ext_path, + char *err, uint32_t err_cap) { + return kvspaceShmExtindex((kvspace_t *)h, path, ext_path, err, err_cap); +} + +int kvspaceRmindexExt(void *h, const char *path, char *err, uint32_t err_cap) { + (void)err; + (void)err_cap; + return kvspaceShmDelextindex((kvspace_t *)h, path); +} + +int kvspaceClear(void *h, char *err, uint32_t err_cap) { + (void)err; + (void)err_cap; + return kvspaceShmDeltree((kvspace_t *)h, "/"); +} + +/* New* 统一收口:New* 构造 head+body(malloc),total 超 u32 上限即失败。 */ +static int finish(uint8_t *value, uint64_t total, uint8_t **out, uint32_t *out_len) { + if (!value || total > UINT32_MAX) { + free(value); + return 1; + } + *out = value; + *out_len = (uint32_t)total; + return 0; +} + +int kvspaceNewPtr(const char *target_kindexpr, const char *target, + uint8_t **out, uint32_t *out_len) { + if (!target_kindexpr || !target || !out || !out_len) + return 1; + /* `any` 不是类型名(空 kindexpr = 不约束)。 */ + if (strcmp(target_kindexpr, "any") == 0) + return 1; + uint8_t *value = NULL; + uint64_t total = 0; + if (kvspaceXvalueHeadNewPtr((const uint8_t *)target, + (uint32_t)strlen(target), target_kindexpr, + &value, &total) != 0) + return 1; + return finish(value, total, out, out_len); +} + +int kvspaceNewChar(const uint8_t *bytes, uint32_t len, const char *langtype, + uint8_t **out, uint32_t *out_len) { + uint8_t *value = NULL; + uint64_t total = 0; + int rc = kvspaceXvalueHeadNewSlack(bytes, len, len, langtype, &value, &total); + return rc == 0 ? finish(value, total, out, out_len) : 1; +} + +int kvspaceNewBool(uint8_t v, const char *langtype, + uint8_t **out, uint32_t *out_len) { + uint8_t b = v ? 1 : 0; + uint8_t *value = NULL; + uint64_t total = 0; + int rc = kvspaceXvalueHeadNewScalar(KVSPACE_XVALUE_ST_SHORT, &b, 1, langtype, + &value, &total); + return rc == 0 ? finish(value, total, out, out_len) : 1; +} + +int kvspaceNewInt64(int64_t v, const char *langtype, + uint8_t **out, uint32_t *out_len) { + uint8_t raw[8]; + for (int i = 0; i < 8; i++) + raw[i] = (uint8_t)((uint64_t)v >> (8 * i)); + uint8_t *value = NULL; + uint64_t total = 0; + int rc = kvspaceXvalueHeadNewScalar(KVSPACE_XVALUE_ST_SHORT, raw, 8, langtype, + &value, &total); + return rc == 0 ? finish(value, total, out, out_len) : 1; +} + +int kvspaceNewFloat64(double v, const char *langtype, + uint8_t **out, uint32_t *out_len) { + uint64_t bits; + memcpy(&bits, &v, sizeof bits); + uint8_t raw[8]; + for (int i = 0; i < 8; i++) + raw[i] = (uint8_t)(bits >> (8 * i)); + uint8_t *value = NULL; + uint64_t total = 0; + int rc = kvspaceXvalueHeadNewScalar(KVSPACE_XVALUE_ST_SHORT, raw, 8, langtype, + &value, &total); + return rc == 0 ? finish(value, total, out, out_len) : 1; +} + +int kvspaceWatch(void *h, const char *key, const uint8_t *target, + uint32_t target_len, uint64_t tick_ns, uint8_t **out, + uint32_t *out_len) { + if (!out || !out_len) + return 1; + *out = NULL; + *out_len = 0; + struct timespec t0, tn; + clock_gettime(CLOCK_MONOTONIC, &t0); + for (;;) { + int32_t len = 0; + uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &len); + if (d && (uint32_t)len == target_len && + memcmp(d, target, target_len) == 0) { + *out = d; + *out_len = (uint32_t)len; + return 0; + } + clock_gettime(CLOCK_MONOTONIC, &tn); + uint64_t elapsed = (uint64_t)(tn.tv_sec - t0.tv_sec) * 1000000000ULL + + (uint64_t)(tn.tv_nsec - t0.tv_nsec); + if (elapsed >= tick_ns) + return 0; + usleep(1000); + } +} + + +/* ── P2 token ABI wrappers(durable 后端后续给 ENOTSUP)──────────── */ +int kvspaceViewResolvePrefix(void *h, const char *path, kvspaceRef_t *ref) { + return kvspaceShmResolvePrefix((kvspace_t *)h, path, ref) == 0 ? 0 : 1; +} + +int kvspaceViewAck(void *h, uint32_t *epoch) { + return kvspaceShmViewAck((kvspace_t *)h, epoch); +} + +int kvspaceViewQuiesce(void *h, int on) { + return kvspaceShmViewQuiesce((kvspace_t *)h, on); +} + +int kvspaceViewSweep(void *h) { + return kvspaceShmViewSweep((kvspace_t *)h); +} + +int kvspaceViewResolve(void *h, const char *key, kvspaceViewToken_t *tok) { + return kvspaceShmViewResolve((kvspace_t *)h, key, tok) == 0 ? 0 : 1; +} +int kvspaceViewFromRef(void *h, const kvspaceRef_t *ref, const char *k, + kvspaceViewToken_t *tok) { + return kvspaceShmViewFromRef((kvspace_t *)h, ref, k, tok) == 0 ? 0 : 1; +} +int kvspaceViewResolveFrom(void *h, const kvspaceRef_t *parent, const char *rel, + kvspaceViewToken_t *tok) { + return kvspaceShmViewResolveFrom((kvspace_t *)h, parent, rel, tok) == 0 ? 0 : 1; +} +int kvspaceViewCheck(void *h, const kvspaceViewToken_t *tok) { + return kvspaceShmViewCheck((kvspace_t *)h, tok); +} +int kvspaceViewWrite(void *h, kvspaceViewToken_t *tok, const uint8_t *d, uint32_t n) { + return kvspaceShmViewWrite((kvspace_t *)h, tok, d, n); +} +int kvspaceViewDel(void *h, kvspaceViewToken_t *tok, const char *key) { + return kvspaceShmViewDel((kvspace_t *)h, tok, key); +} +int kvspaceViewRefresh(void *h, kvspaceViewToken_t *tok, const char *key) { + return kvspaceShmViewRefresh((kvspace_t *)h, tok, key); +} diff --git a/kvspace.c b/kvspace.c new file mode 100644 index 0000000..011f3c0 --- /dev/null +++ b/kvspace.c @@ -0,0 +1,3754 @@ +/* + * kvspace.c — KVSpace SHM 存储引擎: ART 树 + slotsboxmalloc + */ + +#define _GNU_SOURCE +/* header-only 模式:两个依赖库的实现体在本 TU 编译,不再链接它们的 .so。 + * blockmalloc 必须先于 slotsboxmalloc(后者在其实现体里调用前者)。 */ +#define BLOCKMALLOC_IMPLEMENTATION +#include +#define SBO_MIN_SHARD_BYTES (1ULL << 40) +#define SLOTSBOXMALLOC_IMPLEMENTATION +#include "slotsboxmalloc/slotsboxobj.h" +#include "kvspace_shm.h" +#include "xvalue_head.h" +#include +#include +#include +#include +#if defined(__linux__) +#include +#include +#endif +#include +#include +#include +#include +#include +#include +#include +#include + +#define KVS_MAGIC "kvspace-c.v5" +#define ART_PREFIX_MAX 10 +/* ART block = one N4/N16 node; N48/N256 take a run of consecutive blocks. + The pool only appends (nodes are never freed), so runs are bump-allocated. */ +#define ART_BLOCK_SZ 104 +#define ART_SLAB_INIT (64UL * 1024 * 1024) +/* 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) +/* ART ids are 30-bit: reserve <= 2^30 blocks. */ +#define ART_RESERVE (1ULL << 36) +#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 + +static int reserved_meta_path(const char *key) { + static const char root[] = "/.kvspace-meta"; + return strncmp(key, root, sizeof root - 1) == 0 && + (key[sizeof root - 1] == 0 || key[sizeof root - 1] == '/'); +} + +enum { ART_N4 = 0, + ART_N16 = 1, + ART_N48 = 2, + ART_N256 = 3, + ART_MOVED = 255 }; + +/* v5:{has_value, box_offset} 合并成单个 8B 对齐原子字,禁止裸位域访问。 + * val = bit0 has_value | bit1..63 box_offset(MOVED 节点用 box_offset 存新 id)。 */ +typedef struct { + uint8_t type, prefix[ART_PREFIX_MAX], prefix_len; + uint16_t count; + uint16_t _pad; + uint64_t val; +} art_hdr_t; + +#define AV_HAS(v) ((int)((v) & 1u)) +#define AV_OFF(v) ((uint64_t)((v) >> 1)) +#define AV_MAKE(has, off) ((((uint64_t)(off)) << 1) | ((has) ? 1ull : 0ull)) +static inline uint64_t art_val_load(const art_hdr_t *h) { + return __atomic_load_n(&h->val, __ATOMIC_ACQUIRE); +} +static inline void art_val_store(art_hdr_t *h, uint64_t v) { + __atomic_store_n(&h->val, v, __ATOMIC_RELEASE); +} +static inline bool art_val_cas(art_hdr_t *h, uint64_t *exp, uint64_t nv) { + return __atomic_compare_exchange_n(&h->val, exp, nv, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); +} + +typedef struct { + art_hdr_t h; + uint8_t keys[4]; + int32_t children[4]; +} art_n4_t; +typedef struct { + art_hdr_t h; + uint8_t keys[16]; + int32_t children[16]; +} art_n16_t; +typedef struct { + art_hdr_t h; + uint8_t index[256]; + int32_t children[48]; +} art_n48_t; +typedef struct { + art_hdr_t h; + int32_t children[256]; +} art_n256_t; + +_Static_assert(sizeof(art_n4_t) <= ART_BLOCK_SZ && sizeof(art_n16_t) <= ART_BLOCK_SZ, + "N4/N16 must fit one ART block"); + +static int art_node_sz(int t) { + return t == ART_N4 ? sizeof(art_n4_t) + : t == ART_N16 ? sizeof(art_n16_t) + : t == ART_N48 ? sizeof(art_n48_t) + : sizeof(art_n256_t); +} + +/* 进程注册表槽:pid + starttime 双重判活(pid 复用),recovery_ack = 已确认的代。 */ +#define P2_SLOT_N 64 +/* CLAIMING:已 CAS 占位、pid/starttime 尚未写完;扫描者视为存活,避免认领竞态误回收。 */ +enum { P2_SLOT_FREE = 0, P2_SLOT_MINE = 1, P2_SLOT_CLAIMING = 3 }; +typedef struct { + uint32_t pid; + uint32_t state; + uint64_t starttime; + uint64_t recovery_ack; + uint32_t quiesced; /* Q2 声明(第 3 步接) */ + uint32_t claim_id; /* open 时领取,1..(1<<30)-1 */ +} p2_slot_t; + +/* 统一垃圾环:每条 16B,off 恒为真实 box 偏移,状态编码在 meta。 + * meta = state:2 | claim_id:30(EMPTY=0;RESERVED/COMMITTED 带 claim)。 */ +#define GARB_CAP (1u << 21) /* 2,097,152 条 = 32MiB VA */ +#define GARB_THRESHOLD (64ull << 20) /* 触发一轮的垃圾字节 */ +#define P2_BARRIER_MS_DEFAULT 200 +#define GARB_MAX_CLAIM ((1u << 30) - 1u) +typedef struct { + uint64_t off; + uint32_t epoch; + uint32_t meta; +} garb_ent_t; +enum { GE_EMPTY = 0, GE_RESERVED = 1, GE_COMMITTED = 2 }; +#define GE_STATE(m) ((uint32_t)((m) & 3u)) +#define GE_CLAIM(m) ((uint32_t)((m) >> 2)) +#define GE_MAKE(st, id) ((((uint32_t)(id)) << 2) | (uint32_t)(st)) + +/* [kvspace_hdr_t][blocks_meta_t][ART slab ...] + * .sbo.head [sbo meta ...] + * .sbo.data [sbo data ...] */ +typedef struct { + char magic[12]; + uint64_t art_slab_size, sbo_head_size, sbo_data_size; + int32_t art_root; + /* v3: 旁路 gen 表(节点 id 索引,8B/节点)。 */ + uint64_t gens_size; + uint64_t gen_epoch; /* 节点分配 epoch(原子递增) */ + uint32_t format_version; /* = KVSPACE_FORMAT_VERSION */ + uint32_t recovery_epoch; /* 屏障轮次(u32,模比较) */ + uint32_t ack_seq; /* 跨进程 futex 字(第 3 步) */ + uint32_t barrier_state; + uint64_t barrier_pid, barrier_starttime; + uint64_t garb_cap, garb_head, garb_tail; + uint64_t committed_bytes; /* 只由写者改(单写者契约,普通 add) */ + uint64_t freed_bytes; /* 只由 leader 改 */ + uint64_t garb_freeing; + uint64_t round_ok, round_aborted, leak_boxes; + uint32_t claim_seq; /* 30 位发号;耗尽 open 硬失败 */ + p2_slot_t slots[P2_SLOT_N]; +} kvspace_hdr_t; + +#define ART_OFF (sizeof(kvspace_hdr_t) + sizeof(blocks_meta_t)) + +/* blocks_init fixes the block-head width (30-bit ids) from the initial size. */ +_Static_assert(ART_SLAB_INIT / (ART_BLOCK_SZ + 2) > 32767ULL / 4, + "ART_SLAB_INIT too small: blockmalloc would pick 14-bit ids"); +_Static_assert(ART_RESERVE / (ART_BLOCK_SZ + 8) <= (1ULL << 30), + "ART_RESERVE too large for 30-bit block ids"); +_Static_assert((ART_BLOCK_SZ + 8) % 8 == 0, "ART stride must keep 8B alignment"); +_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 sbo_pool_meta(m, 0); +} +static sbo_lock_t *sbo_plock(sbo_meta_t *m) { + return sbo_pool_lock(m, 0); +} +static uint8_t *sbo_pmem(sbo_meta_t *m) { + return sbo_pool_mem(m, 0); +} +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->box_off); +} + +typedef struct { + int fd; + uint8_t *base; + size_t mapped; + size_t reserve; +} shm_region_t; + +typedef struct { + char key[256]; + pthread_cond_t cond; + pthread_mutex_t mtx; + uint8_t *val; + int32_t val_len; + bool ready; +} watch_t; + +struct kvspace { + shm_region_t r_art, r_head, r_data, r_gen, r_garb; + garb_ent_t *garb; /* .garb 映射(GARB_CAP 条) */ + uint64_t gen_n; /* gens_at 容量:本进程已映射的节点数 */ + uint64_t art_total_seen; /* 上次同步时的 ART slab total_size */ + kvspace_hdr_t *hdr; + uint64_t store_id; /* 进程内句柄身份(P2 token 用) */ + int p2_slot; /* 注册表槽;-1 = 未注册 */ + uint32_t pid; /* open 时的 pid(冷路径判 fork) */ + uint32_t fork_gen; /* fork 世代:子进程里全局 +1 → 热路径守卫 */ + uint32_t quiesced; /* Q2 进程内镜像;claim 时 = 1 */ + pthread_t maint_thread; + uint32_t maint_seq; /* 进程内私有 futex 字 */ + uint32_t maint_started, maint_stop; /* started: 0 未建, 1 运行, 2 创建失败 */ + uint32_t reclaim_unavailable; /* 线程创建失败:环 3/4 满后写硬失败 */ + blocks_meta_t *art_meta; + uint8_t *art_data; + sbo_meta_t *sbo_meta; + uint8_t *sbo_data; + watch_t watches[WATCH_TABLE_SZ]; + pthread_mutex_t wlock; +}; + +/* ---- shm region ---- */ +static int region_map(shm_region_t *r, size_t size) { + if (size > r->reserve) + return -1; + if (size <= r->mapped) + return 0; + if (mmap(r->base, size, PROT_READ | PROT_WRITE, MAP_SHARED | MAP_FIXED, + r->fd, 0) == MAP_FAILED) + return -1; + r->mapped = size; + return 0; +} + +/* 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, + 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 > reserve ? size : reserve; + void *p = mmap(NULL, r->reserve, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (p == MAP_FAILED) + return -1; + r->base = p; + return region_map(r, size); +} + +static void region_close(shm_region_t *r); /* fwd */ + +static void region_close(shm_region_t *r) { + if (r->base) + munmap(r->base, r->reserve); + if (r->fd >= 0) + close(r->fd); +} + +/* Pick up growth done by another process (mid-op growth: kvspace#11). */ + +/* ── P2: 旁路 gen 表(node id → {struct_gen, value_gen})──────────── */ +static void kv_sbo_free(kvspace_t *kv, uint64_t off); +static void p2_setstats_dump(void); +static art_hdr_t *art_hdr(kvspace_t *kv, int32_t id); +static int32_t art_child(kvspace_t *kv, void *n, uint8_t b); +static int pfx_shared(const uint8_t *a, int al, const uint8_t *b, int bl); +static int32_t art_walk(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, + int d, int32_t *par, int *pard); +static int32_t art_follow(kvspace_t *kv, int32_t id); +static int32_t ref_leaf(kvspace_t *kv, kvspaceRef_t *ref, const char *key); + +/* 每节点 8B:{struct_gen, value_gen}(v2.2 删 pin 后不再有 refcount 位)。 */ +typedef struct { + uint32_t struct_gen; + uint32_t value_gen; +} kv_gens_t; + +static uint64_t gen_cap(const kvspace_t *kv) { + blocks_meta_t *m = kv->art_meta; + uint64_t head = (uint64_t)m->sizeof_block_head; + return m->total_size / (head + m->block_size); +} + +static int32_t art_id_of(kvspace_t *kv, const void *p) { + if (!p || !kv->art_data) + return -1; + return (int32_t)blockid_bydataoffset(kv->art_meta, + (uint64_t)((const uint8_t *)p - kv->art_data)); +} + +static kv_gens_t *gens_at(kvspace_t *kv, int32_t id) { + if (id < 0 || !kv->r_gen.base) + return NULL; + if ((uint64_t)id >= kv->gen_n) /* gen_n 由 gens_grow / kv_sync 维护 */ + return NULL; + return (kv_gens_t *)kv->r_gen.base + id; +} + +static void gen_bump_struct(kvspace_t *kv, int32_t id) { + kv_gens_t *g = gens_at(kv, id); + if (g) + __atomic_add_fetch(&g->struct_gen, 1u, __ATOMIC_ACQ_REL); +} + +/* value_gen 静止态恒为偶数:就地写前后各 +1(奇=写中),整体替换/删除 +2。 */ +static void gen_bump_value(kvspace_t *kv, int32_t id) { + kv_gens_t *g = gens_at(kv, id); + if (g) + __atomic_add_fetch(&g->value_gen, 2u, __ATOMIC_ACQ_REL); +} + +static int64_t gen_alloc_epoch(kvspace_t *kv) { + return (int64_t)__atomic_add_fetch(&kv->hdr->gen_epoch, 1ull, __ATOMIC_ACQ_REL); +} + +static int p2_maint_start(kvspace_t *kv); /* 懒启动(阈值触发) */ +static bool p2_claim_alive(kvspace_t *kv, uint32_t claim); +static int kv_sync(kvspace_t *kv); + +/* 阈值:生产固定 64MiB;测试构建可用 P2_TEST_GARB_THRESHOLD_MB 调低。 */ +static uint64_t p2_garb_threshold(void) { +#ifdef P2_TEST_HOOKS + const char *v = getenv("P2_TEST_GARB_THRESHOLD_MB"); + if (v) { + unsigned long mb = strtoul(v, NULL, 10); + if (mb) + return (uint64_t)mb << 20; + } +#endif + return GARB_THRESHOLD; +} + +/* ── 统一垃圾环:第 2 步只记不回收(drain 在第 3 步接)───────────── + * reserve 必须发生在任何键值/节点变更之前;失败 = 写路径硬错误,无半写。 + * 每个 reserve 之后必须且只能走 commit 或 cancel。 */ +static void garb_full_report(kvspace_t *kv) { + static bool once; + if (once) + return; + once = true; + uint64_t h = __atomic_load_n(&kv->hdr->garb_head, __ATOMIC_ACQUIRE); + uint64_t t = __atomic_load_n(&kv->hdr->garb_tail, __ATOMIC_ACQUIRE); + uint32_t m = __atomic_load_n(&kv->garb[h & (GARB_CAP - 1)].meta, + __ATOMIC_ACQUIRE); + fprintf(stderr, + "kvspace: garbage ring full (cap=%llu, head=%llu, tail=%llu, " + "bytes=%llu): head entry #%llu state=%u claim=%u is holding " + "the drain (no reclaim round yet)\n", + (unsigned long long)kv->hdr->garb_cap, (unsigned long long)h, + (unsigned long long)t, + (unsigned long long)(kv->hdr->committed_bytes - + kv->hdr->freed_bytes), + (unsigned long long)h, GE_STATE(m), GE_CLAIM(m)); +} + +static int garb_reserve(kvspace_t *kv, uint64_t *idx) { + uint32_t claim = kv->p2_slot >= 0 ? kv->hdr->slots[kv->p2_slot].claim_id : 0; + if (!claim) + return -1; + uint64_t h = __atomic_load_n(&kv->hdr->garb_head, __ATOMIC_ACQUIRE); + uint64_t t = __atomic_load_n(&kv->hdr->garb_tail, __ATOMIC_ACQUIRE); + if (t - h >= kv->hdr->garb_cap) { + garb_full_report(kv); + return -1; + } + if (kv->reclaim_unavailable && + kv->hdr->garb_cap - (t - h) < kv->hdr->garb_cap / 4) { + garb_full_report(kv); /* 线程建不起来:1/4 安全线硬失败 */ + return -1; + } + /* 单写者契约(kvspace-c #58):entry/tail 只有写者写,普通 release store。 + * leader 只读;唯一的写者进程若死在 store 与 tail 之间,槽位停在 RESERVED + * 且未发布(i == tail),下一个写者在这里回收死 claim(冷路径)。 + * 活 claim 占着 tail 槽 = 契约被违反 → 硬错误,不猜。 */ + garb_ent_t *e = &kv->garb[t & (GARB_CAP - 1)]; + uint32_t m = __atomic_load_n(&e->meta, __ATOMIC_ACQUIRE); + if (m != GE_EMPTY) { + if (GE_STATE(m) != GE_RESERVED || p2_claim_alive(kv, GE_CLAIM(m))) + return -1; + __atomic_store_n(&e->meta, GE_EMPTY, __ATOMIC_RELEASE); + } + __atomic_store_n(&e->meta, GE_MAKE(GE_RESERVED, claim), __ATOMIC_RELEASE); + __atomic_store_n(&kv->hdr->garb_tail, t + 1, __ATOMIC_RELEASE); + *idx = t; + return 0; +} + +/* bytes 由退役点在“不拿分配器锁”的前提下给出(ViewDel 用 token 缓存的 cap, + * realloc 用已算出的 cap);sbo_allocated_size 会取 sbo plock,不能进热路径。 */ +static void garb_commit(kvspace_t *kv, uint64_t idx, uint64_t off, + uint64_t bytes_retired) { + garb_ent_t *e = &kv->garb[idx & (GARB_CAP - 1)]; + uint32_t claim = GE_CLAIM(__atomic_load_n(&e->meta, __ATOMIC_ACQUIRE)); + e->off = off; + e->epoch = __atomic_load_n(&kv->hdr->recovery_epoch, __ATOMIC_ACQUIRE); + __atomic_store_n(&e->meta, GE_MAKE(GE_COMMITTED, claim), __ATOMIC_RELEASE); + uint64_t bytes = kv->hdr->committed_bytes + bytes_retired; + __atomic_store_n(&kv->hdr->committed_bytes, bytes, __ATOMIC_RELEASE); + if (bytes - __atomic_load_n(&kv->hdr->freed_bytes, __ATOMIC_ACQUIRE) >= + p2_garb_threshold()) + (void)p2_maint_start(kv); /* 失败已置 reclaim_unavailable */ +} + +static void garb_cancel(kvspace_t *kv, uint64_t idx) { + __atomic_store_n(&kv->garb[idx & (GARB_CAP - 1)].meta, GE_EMPTY, + __ATOMIC_RELEASE); +} + +/* 精确 key 的节点 id(即使 has_value==0 也返回;用于墓碑复活时清 DEAD)。 */ +static int32_t p2_node_at(kvspace_t *kv, const char *key) { + int klen = (int)strlen(key); + int d = 0; + int32_t nid = kv->hdr->art_root; + while (nid >= 0) { + art_hdr_t *h = art_hdr(kv, nid); + if (!h || h->type == ART_MOVED) { + if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at FAIL key=%s nid=%d type=%d d=%d/%d\n", key, nid, h ? (int)h->type : -1, d, klen); } + return -1; + } + if (h->prefix_len) { + int s = pfx_shared(h->prefix, h->prefix_len, + (const uint8_t *)key + d, klen - d); + if (s != h->prefix_len && d + s != klen) { + if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at PFX key=%s nid=%d d=%d s=%d plen=%d klen=%d\n", key, nid, d, s, (int)h->prefix_len, klen); } + return -1; + } + d += s; + } + /* 键可能正好结束在 prefix_len==0 的分支节点上(如 k1 落在根 N4 的子叶), + * 此时 d==klen,必须在此返回;否则会越界读 key[klen]。 */ + if (d == klen) + return nid; + nid = art_child(kv, h, (uint8_t)key[d]); + d++; + } + if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at EOF key=%s d=%d/%d\n", key, d, klen); } + return -1; +} + +/* ── P2: 进程注册表 + 恢复屏障 ────────────────────────────────── */ +/* /proc//stat 第 22 字段 starttime(pid 复用判据)。macOS 无 /proc。 + * 返回 0=读到;1=进程不存在(ENOENT,判死);2=不可判定(保守视为存活)。 */ +static int p2_proc_starttime(uint32_t pid, uint64_t *out) { +#if defined(__linux__) + char p[64]; + if (snprintf(p, sizeof p, "/proc/%u/stat", pid) >= (int)sizeof p) + return 2; + FILE *f = fopen(p, "r"); + if (!f) + return errno == ENOENT ? 1 : 2; + char buf[1024]; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + if (!n) + return 2; + buf[n] = 0; + char *rp = strrchr(buf, ')'); /* comm 可含空格,字段从最后一个 ')' 起数 */ + if (!rp) + return 2; + char *s = rp + 1; + int field = 3; /* ')' 之后第一个字段是 stat 第 3 字段 */ + while (*s) { + while (*s == ' ') + s++; + if (!*s) + break; + if (field == 3 && (*s == 'Z' || *s == 'X')) + return 1; /* 僵尸/已退出:判死(否则会永久占位) */ + if (field == 22) { + *out = strtoull(s, NULL, 10); + return 0; + } + while (*s && *s != ' ') + s++; + field++; + } + return 2; +#else + (void)pid; + (void)out; + return 2; /* 平台无 liveness:永不判死 */ +#endif +} + +static bool p2_slot_alive(const p2_slot_t *s) { + uint64_t st = 0; + uint32_t state = __atomic_load_n(&s->state, __ATOMIC_ACQUIRE); + if (state == P2_SLOT_FREE) + return false; + if (state == P2_SLOT_CLAIMING) + return true; + int rc = p2_proc_starttime(s->pid, &st); + if (rc == 1) + return false; /* ENOENT:进程已死 */ + if (rc != 0) + return true; /* 不可判定 → 保守视为存活 */ + return st == s->starttime; +} + +/* open:先回收死槽,再 CAS 认领;仍满才失败(反复崩溃不耗光槽)。 */ +static int p2_reg_claim(kvspace_t *kv) { + uint64_t st = 0; + if (p2_proc_starttime((uint32_t)getpid(), &st) != 0) + st = 0; + uint32_t me = (uint32_t)getpid(); + for (int i = 0; i < P2_SLOT_N; i++) { + p2_slot_t *s = &kv->hdr->slots[i]; + uint32_t v = __atomic_load_n(&s->state, __ATOMIC_ACQUIRE); + if (v == P2_SLOT_CLAIMING && s->pid == 0) { + /* 认领者死在 CAS 与写 pid 之间:pid 仍为 0,可回收 */ + __atomic_compare_exchange_n(&s->state, &v, P2_SLOT_FREE, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); + continue; + } + if (v != P2_SLOT_FREE && !p2_slot_alive(s)) + __atomic_compare_exchange_n(&s->state, &v, P2_SLOT_FREE, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); + } + if (getenv("P2_DEBUG")) { + int nf = 0, nm = 0; + for (int i = 0; i < P2_SLOT_N; i++) { + uint32_t v = __atomic_load_n(&kv->hdr->slots[i].state, __ATOMIC_ACQUIRE); + if (v == P2_SLOT_FREE) nf++; + else if (v == P2_SLOT_MINE) nm++; + } + fprintf(stderr, "DBG claim pid=%u start=%llu free=%d mine=%d\n", me, + (unsigned long long)st, nf, nm); + } + for (int i = 0; i < P2_SLOT_N; i++) { + p2_slot_t *s = &kv->hdr->slots[i]; + uint32_t expect = P2_SLOT_FREE; + if (!__atomic_compare_exchange_n(&s->state, &expect, P2_SLOT_CLAIMING, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) + continue; + uint32_t cid = __atomic_add_fetch(&kv->hdr->claim_seq, 1u, + __ATOMIC_ACQ_REL); + if (cid > GARB_MAX_CLAIM) { /* 不静默回绕 */ + __atomic_store_n(&s->state, P2_SLOT_FREE, __ATOMIC_RELEASE); + return -3; + } + s->pid = me; + s->starttime = st; + s->quiesced = 1; /* 新进程还没有 token:天然静默;runtime 用 view 前显式 Quiesce(0) */ + s->claim_id = cid; + __atomic_store_n(&s->recovery_ack, + __atomic_load_n(&kv->hdr->recovery_epoch, __ATOMIC_ACQUIRE), + __ATOMIC_RELEASE); + __atomic_store_n(&s->state, P2_SLOT_MINE, __ATOMIC_RELEASE); + return i; + } + return -2; /* 占满:调用方拒绝 open,不静默扩容 */ +} + +static void p2_reg_release(kvspace_t *kv) { + if (kv->p2_slot < 0) + return; + if (kv->pid != (uint32_t)getpid()) { /* fork 子进程:句柄无效,不动父进程的槽 */ + kv->p2_slot = -1; + return; + } + p2_slot_t *s = &kv->hdr->slots[kv->p2_slot]; + uint32_t expect = P2_SLOT_MINE; + __atomic_compare_exchange_n(&s->state, &expect, P2_SLOT_FREE, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); + kv->p2_slot = -1; +} + +/* 每次 kvspaceView* 入口:屏障代变化 → 记本地代并 ack(token 由 _pad 校验失效)。 */ +static void p2_ack_slot(kvspace_t *kv, uint32_t ep) { + if (kv->p2_slot < 0) + return; + __atomic_store_n(&kv->hdr->slots[kv->p2_slot].recovery_ack, ep, + __ATOMIC_RELEASE); + __atomic_add_fetch(&kv->hdr->ack_seq, 1u, __ATOMIC_RELEASE); +#if defined(__linux__) + /* 没有 leader 在等就不要进内核:热路径每 64 条指令一次 ack, + * 无条件 FUTEX_WAKE 是每次写系统调用。漏唤醒最坏退化为 5ms 分片。 */ + if (__atomic_load_n(&kv->hdr->barrier_state, __ATOMIC_ACQUIRE)) + syscall(SYS_futex, &kv->hdr->ack_seq, FUTEX_WAKE, INT_MAX, NULL, NULL, + 0); +#endif +} + +/* 表随 ART slab 增长(容量按当前 block 配置重算)。 */ +#define GEN_RESERVE (8ULL << 30) /* 8GiB VA: one gens entry per ART block id */ +static int gens_grow(kvspace_t *kv) { + uint64_t want = gen_cap(kv) * sizeof(kv_gens_t); + if (want > GEN_RESERVE) + return -1; + kv->gen_n = want / sizeof(kv_gens_t); + kv->art_total_seen = kv->art_meta->total_size; + if (want <= kv->r_gen.mapped) + return 0; + if (!kv->r_gen.base) { + void *p = mmap(NULL, GEN_RESERVE, PROT_NONE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_NORESERVE, -1, 0); + if (p == MAP_FAILED) + return -1; + kv->r_gen.base = p; + kv->r_gen.reserve = GEN_RESERVE; + } + if (ftruncate(kv->r_gen.fd, (off_t)want) != 0) + return -1; + if (region_map(&kv->r_gen, (size_t)want) != 0) + return -1; + kv->hdr->gens_size = want; + return 0; +} + + +/* ── P2: tombstone 延迟释放(粗粒度 epoch:队满或显式 flush 时统一 free)── */ + + +int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, + const char *key_fallback, kvspaceXvalueBodyView_t *out); +static int32_t art_id_of(kvspace_t *kv, const void *p); +static art_hdr_t *art_hdr(kvspace_t *kv, int32_t id); +static int32_t art_follow(kvspace_t *kv, int32_t id); +static int32_t ref_leaf(kvspace_t *kv, kvspaceRef_t *ref, const char *key); +static int32_t art_walk(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, + int d, int32_t *par, int *pard); +static int32_t art_child(kvspace_t *kv, void *n, uint8_t b); +static int pfx_shared(const uint8_t *a, int al, const uint8_t *b, int bl); + +/* ── P2: 版本化 view token ─────────────────────────────────────── */ +/* fork 检测不能每 op 调 getpid()(本机 glibc 不缓存,一次 ~150ns)。 + * 用 pthread_atfork 的子进程钩子把全局世代 +1;热路径只比两个 u32。 */ +static uint32_t g_fork_gen; +static pthread_once_t g_atfork_once = PTHREAD_ONCE_INIT; +static void p2_atfork_child(void) { + __atomic_add_fetch(&g_fork_gen, 1u, __ATOMIC_ACQ_REL); +} +static void p2_atfork_install(void) { + (void)pthread_atfork(NULL, NULL, p2_atfork_child); +} + +#define P2_FORK_GUARD(kv) \ + ((kv)->fork_gen != __atomic_load_n(&g_fork_gen, __ATOMIC_ACQUIRE)) +#define P2_VIEW_GUARD(kv) ((kv)->quiesced || P2_FORK_GUARD(kv)) + +/* 固定头直读辅助:校验 value 是合法 8 字节头 + body 长度自洽;产出头。 */ +static int head_of(const uint8_t *val, int32_t val_len, kvspaceXvalueHead *out) { + if (!val || val_len < KVSPACE_XVALUE_HEAD_LEN) + return -1; + memcpy(out, val, sizeof *out); + uint64_t l = kvspaceXvalueHeadLen(out); + if (out->storetype > KVSPACE_XVALUE_ST_EXT || + out->ptr > KVSPACE_XVALUE_PTR_EXT) + return -1; + if ((uint64_t)KVSPACE_XVALUE_HEAD_LEN + l > (uint64_t)val_len) + return -1; + return 0; +} + +int kvspaceShmViewFromRef(kvspace_t *kv, const kvspaceRef_t *ref, + const char *key_fallback, kvspaceViewToken_t *tok) { + (void)key_fallback; + if (!kv || !ref || !tok || ref->gen != 0 || P2_VIEW_GUARD(kv)) + return -1; + int32_t id = art_follow(kv, (int32_t)ref->block_id); + art_hdr_t *h = id >= 0 ? art_hdr(kv, id) : NULL; + if (!h) + return -1; + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return -1; + uint64_t cap = sbo_allocated_size(kv->sbo_meta, AV_OFF(hv)); + if (cap < 1) + return -1; + uint8_t *raw = kv->sbo_data + AV_OFF(hv); + kvspaceXvalueHead w; + if (head_of(raw, (int32_t)cap, &w) != 0 || + w.ptr != KVSPACE_XVALUE_PTR_INLINE) + return -1; + kv_gens_t *g = gens_at(kv, id); + if (!g) + return -1; + uint32_t gen = __atomic_load_n(&g->struct_gen, __ATOMIC_ACQUIRE); + tok->head = raw; + tok->body = raw + KVSPACE_XVALUE_HEAD_LEN; + tok->headlen = KVSPACE_XVALUE_HEAD_LEN; + tok->value_len = (uint32_t)kvspaceXvalueHeadLen(&w); + tok->cap = cap; + tok->body_cap = cap - KVSPACE_XVALUE_HEAD_LEN; + tok->struct_gen = gen; + tok->value_gen = __atomic_load_n(&g->value_gen, __ATOMIC_ACQUIRE); + tok->store_id = kv->store_id; + tok->node_id = (uint32_t)id; + tok->_pad = __atomic_load_n(&kv->hdr->recovery_epoch, __ATOMIC_ACQUIRE); + return 0; +} + +int kvspaceShmViewResolve(kvspace_t *kv, const char *key, kvspaceViewToken_t *tok) { + kvspaceRef_t ref; + if (!kv || !key || !tok || P2_VIEW_GUARD(kv) || + kvspaceShmResolveRef(kv, key, &ref) != 0) + return -1; + return kvspaceShmViewFromRef(kv, &ref, key, tok); +} + +/* 前缀解析:path 的最后一个字节落在哪个节点;depth = 该节点压缩前缀被 path 覆盖的字节数 + * (可能 < prefix_len,即边界落在前缀内部)。用于 short 键(整键就是一个节点的前缀)。 */ +int kvspaceShmResolvePrefix(kvspace_t *kv, const char *path, kvspaceRef_t *ref) { + if (!kv || !path || !ref || P2_VIEW_GUARD(kv)) + return -1; + memset(ref, 0, sizeof(*ref)); + int klen = (int)strlen(path); + int d = 0; + int32_t nid = kv->hdr->art_root; + while (nid >= 0) { + art_hdr_t *h = art_hdr(kv, nid); + if (!h || h->type == ART_MOVED) + return -1; + if (h->prefix_len) { + int s = pfx_shared(h->prefix, h->prefix_len, + (const uint8_t *)path + d, klen - d); + if (s != h->prefix_len && d + s != klen) + return -1; + d += s; + if (d == klen) { + ref->block_id = (uint32_t)nid; + ref->depth = (uint32_t)s; /* 该节点前缀里被 path 覆盖的字节数 */ + return 0; + } + } + nid = art_child(kv, h, (uint8_t)path[d]); + d++; + } + return -1; +} + +/* 前缀形态:parent_ref 的 block_id/depth 指向目录节点;只走 rel。 */ +int kvspaceShmViewResolveFrom(kvspace_t *kv, const kvspaceRef_t *parent, + const char *rel, kvspaceViewToken_t *tok) { + if (!kv || !parent || !rel || !tok || P2_VIEW_GUARD(kv)) + return -1; + int32_t pid = art_follow(kv, (int32_t)parent->block_id); + art_hdr_t *ph = pid >= 0 ? art_hdr(kv, pid) : NULL; + if (!ph) + return -1; + /* 边界落在节点压缩前缀内部时,先把该节点前缀的剩余字节与 rel 对掉。 */ + const uint8_t *p = (const uint8_t *)rel; + int rem = (int)strlen(rel); + int32_t covered = (int32_t)parent->depth; + while (covered < (int32_t)ph->prefix_len && rem > 0) { + if (p[0] != ph->prefix[covered]) + return -1; + p++; rem--; covered++; + } + int32_t id; + if (rem == 0) { + id = pid; /* 空 rel = 前缀自身 */ + } else { + int32_t cid = art_child(kv, ph, p[0]); + id = art_walk(kv, cid, p, rem, 1, NULL, NULL); + } + if (id < 0) + return -1; + kvspaceRef_t leaf = {0}; + leaf.block_id = (uint32_t)id; + leaf.gen = 0; + return kvspaceShmViewFromRef(kv, &leaf, rel, tok); +} + +int kvspaceShmViewCheck(kvspace_t *kv, const kvspaceViewToken_t *tok) { + if (!kv || !tok || P2_VIEW_GUARD(kv)) + return -1; + if (tok->store_id != kv->store_id) + return 1; + if (tok->_pad != __atomic_load_n(&kv->hdr->recovery_epoch, __ATOMIC_ACQUIRE)) + return 1; + kv_gens_t *g = gens_at(kv, (int32_t)tok->node_id); + if (!g) + return 1; + if (__atomic_load_n(&g->struct_gen, __ATOMIC_ACQUIRE) != tok->struct_gen) + return 1; + art_hdr_t *h = art_hdr(kv, (int32_t)tok->node_id); + if (!h) + return 1; + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return 1; + if (tok->head != kv->sbo_data + AV_OFF(hv)) + return 1; + return 0; +} + +/* 热路径写:同尺寸就地写(8B inline 情形)。0=成功;1=需要回落(尺寸/版本不符)。 */ +int kvspaceShmViewWrite(kvspace_t *kv, kvspaceViewToken_t *tok, + const uint8_t *data, uint32_t len) { + if (!kv || P2_VIEW_GUARD(kv)) + return -1; + if (kvspaceShmViewCheck(kv, tok) != 0) + return 1; + if (len > tok->body_cap || len != tok->value_len) + return 1; + kv_gens_t *g = gens_at(kv, (int32_t)tok->node_id); + if (!g) + return 1; + __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); /* 奇:写中 */ + memcpy(tok->body, data, len); + __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); /* 偶:写完 */ + tok->value_len = len; + return 0; +} + +/* 热路径删:tombstone + value_gen++(box 释放走延迟队列)。 */ +int kvspaceShmViewDel(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key) { + if (!kv || P2_VIEW_GUARD(kv)) + return -1; + if (kvspaceShmViewCheck(kv, tok) != 0) + return 1; + kv_gens_t *g = gens_at(kv, (int32_t)tok->node_id); + art_hdr_t *h = art_hdr(kv, (int32_t)tok->node_id); + if (!g || !h) + return 1; + /* 叶子级逻辑墓碑:CAS 一次原子清零 has_value+box;不 bump struct_gen。 */ + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return 1; + uint64_t gidx; + if (garb_reserve(kv, &gidx) != 0) + return -1; /* 表满:硬错误,未变更 */ +#ifdef P2_TEST_HOOKS + { + const char *v = getenv("P2_TEST_HOLD_DELCAS_MS"); + if (v && atoi(v) > 0) + usleep((useconds_t)atoi(v) * 1000); + } +#endif + if (!art_val_cas(h, &hv, 0)) { + garb_cancel(kv, gidx); + return 1; + } + __atomic_add_fetch(&g->value_gen, 2u, __ATOMIC_ACQ_REL); /* 静止态保持偶数 */ + garb_commit(kv, gidx, AV_OFF(hv), tok->cap); + (void)key; + return 0; +} + +/* ── 第 3 步:屏障轮 / 维护线程 / Q1-Q2 ─────────────────────────── */ + +static long long p2_now_ms(void) { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (long long)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; +} + +static int p2_barrier_ms(void) { + const char *v = getenv("P2_BARRIER_MS"); + int ms = v ? atoi(v) : P2_BARRIER_MS_DEFAULT; + return ms < 1 ? 1 : (ms > 5000 ? 5000 : ms); +} + +#if defined(__linux__) +static void p2_futex_wait(uint32_t *addr, uint32_t val, int ms) { + struct timespec ts = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000000L}; + syscall(SYS_futex, addr, FUTEX_WAIT, val, &ts, NULL, 0); +} +static void p2_futex_wake(uint32_t *addr) { + syscall(SYS_futex, addr, FUTEX_WAKE, INT_MAX, NULL, NULL, 0); +} +static void p2_futex_wait_priv(uint32_t *addr, uint32_t val, int ms) { + struct timespec ts = {(time_t)(ms / 1000), (long)(ms % 1000) * 1000000L}; + syscall(SYS_futex, addr, FUTEX_WAIT_PRIVATE, val, &ts, NULL, 0); +} +static void p2_futex_wake_priv(uint32_t *addr) { + syscall(SYS_futex, addr, FUTEX_WAKE_PRIVATE, INT_MAX, NULL, NULL, 0); +} +#else +static void p2_futex_wait(uint32_t *a, uint32_t v, int ms) { (void)a; (void)v; usleep((useconds_t)ms * 1000); } +static void p2_futex_wake(uint32_t *a) { (void)a; } +static void p2_futex_wait_priv(uint32_t *a, uint32_t v, int ms) { (void)a; (void)v; usleep((useconds_t)ms * 1000); } +static void p2_futex_wake_priv(uint32_t *a) { (void)a; } +#endif + +/* 选举字 = state:2 | claim_id:30(与环条目同一编码),单词 CAS,无 CLAIMING 过渡态。 */ +enum { P2_BARRIER_FREE = 0, P2_BARRIER_OWNED = 1 }; + +static uint32_t p2_my_claim(kvspace_t *kv) { + return kv->p2_slot >= 0 ? kv->hdr->slots[kv->p2_slot].claim_id : 0; +} + +static bool p2_claim_alive(kvspace_t *kv, uint32_t claim) { + if (!claim) + return false; + for (int i = 0; i < P2_SLOT_N; i++) { + p2_slot_t *s = &kv->hdr->slots[i]; + if (__atomic_load_n(&s->state, __ATOMIC_ACQUIRE) != P2_SLOT_MINE) + continue; + if (__atomic_load_n(&s->claim_id, __ATOMIC_ACQUIRE) != claim) + continue; + return p2_slot_alive(s); + } + return false; +} + +static bool p2_elect(kvspace_t *kv) { + uint32_t mine = p2_my_claim(kv); + if (!mine) + return false; + uint32_t own = GE_MAKE(P2_BARRIER_OWNED, mine); + for (;;) { + uint32_t cur = __atomic_load_n(&kv->hdr->barrier_state, __ATOMIC_ACQUIRE); + if (cur == GE_MAKE(P2_BARRIER_FREE, 0)) { + if (__atomic_compare_exchange_n(&kv->hdr->barrier_state, &cur, own, + false, __ATOMIC_ACQ_REL, + __ATOMIC_ACQUIRE)) + return true; + continue; + } + if (GE_STATE(cur) != P2_BARRIER_OWNED) + return false; /* 非法状态:不猜 */ + if (GE_CLAIM(cur) == mine) + return false; /* 本进程已有 leader 线程在跑 */ + if (p2_claim_alive(kv, GE_CLAIM(cur))) + return false; /* 活着的 owner:跳过,不等待 */ + if (__atomic_compare_exchange_n(&kv->hdr->barrier_state, &cur, own, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) + return true; /* 死者接管 */ + } +} + +static void p2_release_busy(kvspace_t *kv) { + uint32_t mine = GE_MAKE(P2_BARRIER_OWNED, p2_my_claim(kv)); + __atomic_compare_exchange_n(&kv->hdr->barrier_state, &mine, + GE_MAKE(P2_BARRIER_FREE, 0), false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); +} + +/* 每槽满足:state != MINE || recovery_ack == r || quiesced。 + * 只在未满足且未判死的槽上读 /proc;判死结果本轮缓存。 */ +static bool p2_wait_acks(kvspace_t *kv, uint32_t r, int deadline_ms) { + uint8_t dead[P2_SLOT_N] = {0}; + long long t0 = p2_now_ms(); + for (;;) { + uint32_t seq = __atomic_load_n(&kv->hdr->ack_seq, __ATOMIC_ACQUIRE); + bool missing = false; + int first = -1; + for (int i = 0; i < P2_SLOT_N; i++) { + p2_slot_t *s = &kv->hdr->slots[i]; + if (__atomic_load_n(&s->state, __ATOMIC_ACQUIRE) != P2_SLOT_MINE) + continue; + if (__atomic_load_n(&s->recovery_ack, __ATOMIC_ACQUIRE) == r) + continue; + if (__atomic_load_n(&s->quiesced, __ATOMIC_ACQUIRE)) + continue; + if (!dead[i]) { + uint64_t ts = 0; + int rc = p2_proc_starttime(s->pid, &ts); + if (rc == 1 || (rc == 0 && ts != s->starttime)) + dead[i] = 1; + } + if (dead[i]) + continue; + missing = true; + if (first < 0) + first = i; + } + if (!missing) + return true; + long long left = (long long)deadline_ms - (p2_now_ms() - t0); + if (left <= 0 || __atomic_load_n(&kv->maint_stop, __ATOMIC_ACQUIRE)) { + if (first >= 0) { + p2_slot_t *s = &kv->hdr->slots[first]; + fprintf(stderr, + "kvspace: barrier round %u abandoned (waited %dms): " + "slot %d pid=%u start=%llu ack=%llu quiesced=%u\n", + r, deadline_ms, first, s->pid, + (unsigned long long)s->starttime, + (unsigned long long)__atomic_load_n(&s->recovery_ack, + __ATOMIC_ACQUIRE), + __atomic_load_n(&s->quiesced, __ATOMIC_ACQUIRE)); + } + return false; + } + p2_futex_wait(&kv->hdr->ack_seq, seq, (int)(left < 5 ? left : 5)); + } +} + +/* 严格 FIFO:EMPTY 推进;RESERVED 活 claim → 停,死 claim → 取消+审计; + * COMMITTED 且 epoch < r 才释放(cap 在 free 之前取)。 */ +static int p2_drain(kvspace_t *kv, uint32_t r) { + int freed = 0; + uint64_t i = __atomic_load_n(&kv->hdr->garb_head, __ATOMIC_ACQUIRE); + for (;;) { + /* 环尾之外是未写条目:到 tail 必须停,否则会把 head 推过 tail + * (tail-head 下溢后 reserve 永久失败)。 */ + if (i >= __atomic_load_n(&kv->hdr->garb_tail, __ATOMIC_ACQUIRE)) + goto out; + garb_ent_t *e = &kv->garb[i & (GARB_CAP - 1)]; + uint32_t m = __atomic_load_n(&e->meta, __ATOMIC_ACQUIRE); + uint32_t st = GE_STATE(m); + if (st == GE_EMPTY) { + __atomic_store_n(&kv->hdr->garb_head, i + 1, __ATOMIC_RELEASE); + i++; + continue; + } + if (st == GE_RESERVED) { + if (p2_claim_alive(kv, GE_CLAIM(m))) + goto out; + __atomic_store_n(&e->meta, GE_EMPTY, __ATOMIC_RELEASE); + __atomic_add_fetch(&kv->hdr->leak_boxes, 1, __ATOMIC_ACQ_REL); + fprintf(stderr, + "kvspace: garbage entry %llu reserved by dead claim %u: " + "leaked\n", (unsigned long long)i, GE_CLAIM(m)); + __atomic_store_n(&kv->hdr->garb_head, i + 1, __ATOMIC_RELEASE); + i++; + continue; + } + if (st != GE_COMMITTED) + goto out; /* 非法状态:停,不猜 */ + if (!((int32_t)(r - __atomic_load_n(&e->epoch, __ATOMIC_ACQUIRE)) > 0)) + goto out; /* 未到轮次 */ + uint64_t off = __atomic_load_n(&e->off, __ATOMIC_ACQUIRE); + uint64_t cap = sbo_allocated_size(kv->sbo_meta, off); /* free 之前取 */ + __atomic_store_n(&kv->hdr->garb_freeing, off, __ATOMIC_RELEASE); + __atomic_store_n(&e->meta, GE_EMPTY, __ATOMIC_RELEASE); + __atomic_store_n(&kv->hdr->garb_head, i + 1, __ATOMIC_RELEASE); +#ifdef P2_TEST_HOOKS + { /* 崩溃窗口:head 已推进、box 未 free —— 只漏不双 free */ + const char *v = getenv("P2_TEST_HOLD_DRAIN_MS"); + if (v && atoi(v) > 0) + usleep((useconds_t)atoi(v) * 1000); + } +#endif + kv_sbo_free(kv, off); + __atomic_store_n(&kv->hdr->freed_bytes, + kv->hdr->freed_bytes + cap, __ATOMIC_RELEASE); + i++; + freed++; + } +out: + __atomic_store_n(&kv->hdr->garb_freeing, 0, __ATOMIC_RELEASE); + return freed; +} + +/* 一轮:选举 → 清崩溃标记 → 推进 epoch → 等 ack → drain → 回收死槽。 + * 返回释放条数;-1 = 别人持有;-2 = 超时放弃(零 free)。 */ +static int p2_round(kvspace_t *kv) { +#if !defined(__linux__) + (void)kv; + return 0; /* 无 /proc:不推进、不回收 */ +#else + if (!kv || kv->hdr->art_root < 0) + return 0; + if (kv->pid != (uint32_t)getpid()) + return -1; /* fork 子进程:句柄无效 */ + if (!p2_elect(kv)) + return -1; + /* 别的进程可能扩过 ART/SBO/gen:drain 前必须把本进程映射同步到当前尺寸, + * 否则 sbo_allocated_size / kv_sbo_free 会越界访问。 */ + if (kv_sync(kv) != 0) { + p2_release_busy(kv); + return -1; + } + uint64_t pending = __atomic_exchange_n(&kv->hdr->garb_freeing, 0, + __ATOMIC_ACQ_REL); + if (pending) { + __atomic_add_fetch(&kv->hdr->leak_boxes, 1, __ATOMIC_ACQ_REL); + fprintf(stderr, "kvspace: previous leader died freeing box %llu: " + "leaked\n", (unsigned long long)pending); + } + uint32_t r = __atomic_add_fetch(&kv->hdr->recovery_epoch, 1u, + __ATOMIC_ACQ_REL); + if (!p2_wait_acks(kv, r, p2_barrier_ms())) { + __atomic_add_fetch(&kv->hdr->round_aborted, 1, __ATOMIC_ACQ_REL); + p2_release_busy(kv); + return -2; + } + int freed = p2_drain(kv, r); + for (int i = 0; i < P2_SLOT_N; i++) { + p2_slot_t *s = &kv->hdr->slots[i]; + uint32_t v = P2_SLOT_MINE; + if (__atomic_load_n(&s->state, __ATOMIC_ACQUIRE) != P2_SLOT_MINE) + continue; + if (p2_slot_alive(s)) + continue; + __atomic_compare_exchange_n(&s->state, &v, P2_SLOT_FREE, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); + } + __atomic_add_fetch(&kv->hdr->round_ok, 1, __ATOMIC_ACQ_REL); + p2_release_busy(kv); + return freed; +#endif +} + +static void *p2_maint(void *arg) { + kvspace_t *kv = arg; + while (!__atomic_load_n(&kv->maint_stop, __ATOMIC_ACQUIRE)) { + uint32_t seq = __atomic_load_n(&kv->maint_seq, __ATOMIC_ACQUIRE); + if (__atomic_load_n(&kv->maint_stop, __ATOMIC_ACQUIRE)) + break; + p2_futex_wait_priv(&kv->maint_seq, seq, 50); + if (__atomic_load_n(&kv->maint_stop, __ATOMIC_ACQUIRE)) + break; + if (kv->hdr->committed_bytes - + __atomic_load_n(&kv->hdr->freed_bytes, __ATOMIC_ACQUIRE) < + p2_garb_threshold()) + continue; + p2_round(kv); /* 内含选举;忙/超时都直接返回 */ + } + return NULL; +} + +/* 懒启动:open 不建线程;未回收垃圾越过 64MiB 时才建(open / commit 后检查)。 + * 每个进程最多一个;创建失败置 reclaim_unavailable 并由写路径在环 3/4 满时硬失败。 */ +static int p2_maint_start(kvspace_t *kv) { +#if !defined(__linux__) + (void)kv; + return 0; /* macOS:不回收,无线程 */ +#else + uint32_t st = 0; + if (!__atomic_compare_exchange_n(&kv->maint_started, &st, 1u, false, + __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE)) + return st == 2u ? -1 : 0; /* 已启动 / 已知失败 */ + if (kv->pid != (uint32_t)getpid()) { + __atomic_store_n(&kv->maint_started, 0u, __ATOMIC_RELEASE); + return 0; /* fork 子进程不建 */ + } + if (pthread_create(&kv->maint_thread, NULL, p2_maint, kv) != 0) { + __atomic_store_n(&kv->maint_started, 2u, __ATOMIC_RELEASE); + kv->reclaim_unavailable = 1; + fprintf(stderr, "kvspace: maintenance thread create failed; reclaim " + "unavailable (writes hard-fail when the ring is 3/4 " + "full)\n"); + return -1; + } + return 0; +#endif +} + +static void p2_maint_stop(kvspace_t *kv) { + if (kv->pid != (uint32_t)getpid()) { /* fork 子进程里没有该线程 */ + __atomic_store_n(&kv->maint_started, 0u, __ATOMIC_RELEASE); + return; + } + if (__atomic_load_n(&kv->maint_started, __ATOMIC_ACQUIRE) != 1u) + return; /* 未启动 / 创建失败 */ + __atomic_store_n(&kv->maint_stop, 1, __ATOMIC_RELEASE); + __atomic_add_fetch(&kv->maint_seq, 1u, __ATOMIC_RELEASE); + p2_futex_wake_priv(&kv->maint_seq); + pthread_join(kv->maint_thread, NULL); + __atomic_store_n(&kv->maint_started, 0u, __ATOMIC_RELEASE); +} + +/* Q1:op 边界 ack。0 = 已 ack;1 = epoch 变了(先作废 token 再来);-1 = 非法。 */ +int kvspaceShmViewAck(kvspace_t *kv, uint32_t *epoch) { + if (!kv || !epoch || kv->p2_slot < 0 || P2_FORK_GUARD(kv)) + return -1; + uint32_t cur = __atomic_load_n(&kv->hdr->recovery_epoch, __ATOMIC_ACQUIRE); + if (*epoch != cur) { + *epoch = cur; + return 1; + } + p2_ack_slot(kv, cur); + return 0; +} + +/* Q2:显式静默声明。on 只接受 0/1;幂等;状态变化唤醒 leader。 + * 前置:调用线程已作废本进程全部 view token。声明期间 View* 一律 -1。 */ +int kvspaceShmViewQuiesce(kvspace_t *kv, int on) { + if (!kv || (on != 0 && on != 1) || kv->p2_slot < 0 || P2_FORK_GUARD(kv)) + return -1; + p2_slot_t *s = &kv->hdr->slots[kv->p2_slot]; + if (__atomic_load_n(&s->quiesced, __ATOMIC_ACQUIRE) != (uint32_t)on) { + __atomic_store_n(&s->quiesced, (uint32_t)on, __ATOMIC_RELEASE); + __atomic_add_fetch(&kv->hdr->ack_seq, 1u, __ATOMIC_RELEASE); + if (__atomic_load_n(&kv->hdr->barrier_state, __ATOMIC_ACQUIRE)) + p2_futex_wake(&kv->hdr->ack_seq); + } + kv->quiesced = (uint32_t)on; + return 0; +} + +#ifdef P2_TEST_HOOKS +void kvspaceShmViewRoundStats(kvspace_t *kv, uint64_t *ok, uint64_t *aborted, + uint64_t *leaks) { + *ok = kv ? __atomic_load_n(&kv->hdr->round_ok, __ATOMIC_ACQUIRE) : 0; + *aborted = kv ? __atomic_load_n(&kv->hdr->round_aborted, __ATOMIC_ACQUIRE) : 0; + *leaks = kv ? __atomic_load_n(&kv->hdr->leak_boxes, __ATOMIC_ACQUIRE) : 0; +} + +uint64_t kvspaceShmViewGarbBytes(kvspace_t *kv) { + return kv && kv->hdr + ? kv->hdr->committed_bytes - + __atomic_load_n(&kv->hdr->freed_bytes, __ATOMIC_ACQUIRE) + : 0; +} + +/* 对齐验收:块头 8、stride 8 倍数,且每个块的 val(偏移 16)不跨 64B 行。 + * 返回 -1 = 通过;>=0 = 第一个坏块 id;-2 = 块头宽度不是 8。 */ +int64_t kvspaceShmArtAlignCheck(kvspace_t *kv) { + if (!kv || !kv->art_meta) + return -2; + blocks_meta_t *m = kv->art_meta; + if (m->sizeof_block_head != 8) + return -2; + uint64_t stride = (uint64_t)m->sizeof_block_head + m->block_size; + if (stride % 8) + return -2; + uint64_t n = m->total_size / stride; + for (uint64_t id = 0; id < n; id++) { + uint64_t off = blockdata_offset(m, id); + uint64_t val = off + offsetof(art_hdr_t, val); + if (off % 8 || val % 8 || (val & 63) > 56) + return (int64_t)id; + } + return -1; +} + +/* 旧 store 拒绝测试:把 hdr 的 format_version 改成指定值(只测试用)。 */ +void kvspaceShmTestSetFormatVersion(kvspace_t *kv, uint32_t v) { + if (kv && kv->hdr) + kv->hdr->format_version = v; +} +#endif + +/* 同步强制一轮:返回释放条数;-1 = 别人持有;-2 = 超时放弃(零 free)。 */ +int kvspaceShmViewSweep(kvspace_t *kv) { + if (!kv || kv->hdr->art_root < 0) + return 0; + return p2_round(kv); +} + +int kvspaceShmViewFlushTomb(kvspace_t *kv) { + return kvspaceShmViewSweep(kv); +} + +int kvspaceShmViewRefresh(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key) { + kvspaceViewToken_t t; + if (kvspaceShmViewResolve(kv, key, &t) != 0) + return -1; + *tok = t; + return 0; +} + +static int kv_sync(kvspace_t *kv) { + if (region_map(&kv->r_art, ART_OFF + (size_t)kv->hdr->art_slab_size) != 0) + return -1; + /* 别的进程可能扩过 ART slab:本进程 gens 映射同步跟到同一容量。 */ + if (kv->art_meta->total_size != kv->art_total_seen && + gens_grow(kv) != 0) + return -1; + if (region_map(&kv->r_head, (size_t)kv->hdr->sbo_head_size) != 0) + return -1; + return region_map(&kv->r_data, (size_t)kv->hdr->sbo_data_size); +} + +/* blockmalloc only checks total_size; grow the file before publishing it. */ +static int art_slab_grow(kvspace_t *kv) { + size_t want = (size_t)kv->hdr->art_slab_size * 2; + if (ART_OFF + want > kv->r_art.reserve || + ftruncate(kv->r_art.fd, (off_t)(ART_OFF + want)) != 0 || + region_map(&kv->r_art, ART_OFF + want) != 0) + return -1; + kv->hdr->art_slab_size = want; + kv->art_meta->total_size = want; + 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) & ~(uint64_t)7; + 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->max_blocks = (uint32_t)(want / m->block_stride); + kv->hdr->sbo_head_size = SBO_HEAD_FIXED + want; + 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; + b->parent_slot = 0; + b->self_id = (int32_t)nid; + uint64_t mm = root->child_mask; + while (mm) { + int i = __builtin_ctzll(mm); + mm &= mm - 1; + sbo_box(m, root->children[i])->parent = (int32_t)nid; + } + sbo_lock_t *lk = sbo_plock(m); + uint8_t old_lvl = b->objlevel; + if (old_lvl >= 1 && old_lvl <= 3 && lk->hot_id1[old_lvl - 1] == 1) + lk->hot_id1[old_lvl - 1] = (uint32_t)nid + 1; + uint8_t lvl = (uint8_t)(old_lvl + 1); + sbo_box_format(root, lvl, -1, 255, 0); + root->free_mask = ~1ULL; + root->child_mask = 1ULL; + root->states[0] = SBO_BOX; + root->children[0] = (int32_t)nid; + uint16_t b_comb = sbo_combined(b); + root->child_cap[0] = b_comb; + root->child_max_enc = b_comb; + root->child_max_count = 1; + root->max_obj_cap = SBO_MULT_MAX; + root->hint_child = 0; + m->data_size = want; + m->slot_shift = (uint8_t)__builtin_ctzll(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) { +#if defined(__linux__) + /* Linux 用 fallocate PUNCH_HOLE 归还磁盘块;macOS 无该 API(仅空间回收优化),跳过。 */ + 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)); + } +#endif + sbo_free(kv->sbo_meta, off); +} + +/* P2: tombstone —— 有界 ring(L1 内)+ 后台 flush 线程。 + * 前台只做:ring[tail & MASK] = off; tail++(release); 唤醒后台。 + * 队列满时前台等待(backpressure),绝不在调用线程里 sbo_free。 */ +/* ---- helpers ---- */ +static void *art_blk(kvspace_t *kv, int32_t id) { + if (id < 0) + return NULL; + return kv->art_data + blockdata_offset(kv->art_meta, (uint64_t)id); +} +/* Bump-allocate k consecutive blocks; the node spans them (interior heads unused). */ +static int64_t art_bump(kvspace_t *kv, uint32_t k) { + blocks_meta_t *m = kv->art_meta; + spin_lock(&m->lock); + uint64_t id = m->malloc_blocks; + if ((uint64_t)block_offset(m, id + k) > m->total_size) { + spin_unlock(&m->lock); + return -1; + } + m->malloc_blocks += k; + m->used_blocks += k; + setblock_t(m, kv->art_data + block_offset(m, id), 1, -1); + spin_unlock(&m->lock); + return (int64_t)id; +} +static int32_t art_balloc(kvspace_t *kv, int t) { + const uint64_t stride = (uint64_t)kv->art_meta->sizeof_block_head + + kv->art_meta->block_size; + const uint32_t k = (uint32_t)(((uint64_t)art_node_sz(t) + + kv->art_meta->sizeof_block_head + stride - 1) / + stride); + int64_t id = art_bump(kv, k); + while (id < 0) { + if (art_slab_grow(kv) != 0 || gens_grow(kv) != 0) + return -1; + id = art_bump(kv, k); + } + if (id >= 0) { + kv_gens_t *g = gens_at(kv, (int32_t)id); + if (g) { + /* 新 id 的 gen 取全局 epoch,保证被复用的 id 不会误命中旧 token。 */ + __atomic_store_n(&g->struct_gen, + (uint32_t)gen_alloc_epoch(kv), __ATOMIC_RELEASE); + __atomic_store_n(&g->value_gen, 0u, __ATOMIC_RELEASE); + } + } + return (int32_t)id; +} +static art_hdr_t *art_hdr(kvspace_t *kv, int32_t id) { + return (art_hdr_t *)art_blk(kv, id); +} + +/* ---- prefix ---- */ +static int pfx_shared(const uint8_t *a, int al, const uint8_t *b, int bl) { + int n = al < bl ? al : bl; + for (int i = 0; i < n; i++) + if (a[i] != b[i]) + return i; + return n; +} + +/* ---- child lookup ---- */ +static int32_t art_child(kvspace_t *kv, void *n, uint8_t b) { + art_hdr_t *h = (art_hdr_t *)n; + switch (h->type) { + case ART_N4: { + art_n4_t *x = n; + for (int i = 0; i < (int)h->count; i++) + if (x->keys[i] == b) + return x->children[i]; + return -1; + } + case ART_N16: { + art_n16_t *x = n; + int lo = 0, hi = (int)h->count - 1; + while (lo <= hi) { + int m = (lo + hi) / 2; + if (x->keys[m] == b) + return x->children[m]; + if (x->keys[m] < b) + lo = m + 1; + else + hi = m - 1; + } + return -1; + } + case ART_N48: { + art_n48_t *x = n; + uint8_t idx = x->index[b]; + return idx == 255 ? -1 : x->children[idx]; + } + case ART_N256: { + return ((art_n256_t *)n)->children[b]; + } + } + return -1; +} + +static int32_t art_follow(kvspace_t *kv, int32_t id); + +static int32_t art_walk(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, + int d, int32_t *par, int *pard) { + while (nid >= 0) { + art_hdr_t *h = art_hdr(kv, nid); + if (!h || h->type == ART_MOVED) + return -1; + if (h->prefix_len) { + int s = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); + if (s != h->prefix_len) { + if (d + s < klen) + return -1; + if (s < h->prefix_len) + return -1; + } + d += h->prefix_len; + if (d > klen) + return -1; + } + if (d == klen) + return AV_HAS(art_val_load(h)) ? nid : -1; + if (par) + *par = nid; + if (pard) + *pard = d; + nid = art_child(kv, h, key[d]); + d++; + } + return -1; +} + +/* ---- art_search ---- */ +static int32_t art_find2(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, + int32_t *par, int *pard) { + if (nid < 0 || !key) + return -1; + int32_t parent = -1; + int parent_d = 0; + int32_t id = art_walk(kv, nid, key, klen, 0, &parent, &parent_d); + if (par) + *par = parent; + if (pard) + *pard = parent_d; + return id; +} +static int32_t art_find(kvspace_t *kv, int32_t nid, const uint8_t *key, + int klen) { + return art_find2(kv, nid, key, klen, NULL, NULL); +} + +/* Last path '/' or kvlang member '·' (U+00B7, utf-8 C2 B7). */ +static int last_key_sep(const uint8_t *key, int klen, int *seplen) { + int slash = -1, mid = -1; + for (int i = 0; i < klen; i++) { + if (key[i] == '/') + slash = i; + if (i + 1 < klen && key[i] == 0xC2 && key[i + 1] == 0xB7) + mid = i; + } + if (mid > slash) { + *seplen = 2; + return mid; + } + if (slash > 0) { + *seplen = 1; + return slash; + } + *seplen = 0; + return -1; +} + +/* One walk: leaf plus the ancestor covering last '/' or '·' (after that + * node's prefix). First match: node at the separator, not a deeper unique + * prefix that swallowed it. */ +static int32_t art_find_dir(kvspace_t *kv, int32_t nid, const uint8_t *key, + int klen, int last_sep, int seplen, int32_t *dirn, + int *dird) { + int d = 0; + int32_t dir = -1; + int dd = 0; + if (nid < 0 || !key) + return -1; + while (nid >= 0) { + art_hdr_t *h = art_hdr(kv, nid); + if (!h || h->type == ART_MOVED) + return -1; + int entry_d = d; + if (h->prefix_len) { + int s = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); + if (s != h->prefix_len) + return -1; + d += h->prefix_len; + if (d > klen) + return -1; + } + if (dir < 0 && last_sep > 0 && seplen > 0 && + ((entry_d <= last_sep && last_sep < d) || + d == last_sep + seplen)) { + dir = nid; + dd = d; + } + if (d == klen) { + if (dirn) + *dirn = dir; + if (dird) + *dird = dd; + return AV_HAS(art_val_load(h)) ? nid : -1; + } + nid = art_child(kv, h, key[d]); + d++; + } + return -1; +} +static art_hdr_t *art_search(kvspace_t *kv, int32_t nid, const uint8_t *key, + int klen) { + int32_t id = art_find(kv, nid, key, klen); + return id < 0 ? NULL : art_hdr(kv, id); +} +static int32_t art_follow(kvspace_t *kv, int32_t id) { + for (int i = 0; i < 8 && id >= 0; i++) { + art_hdr_t *h = art_hdr(kv, id); + if (!h) + return -1; + if (h->type != ART_MOVED) + return id; + id = (int32_t)AV_OFF(art_val_load(h)); + } + return -1; +} + +/* ---- node create ---- */ +static int32_t art_new_leaf(kvspace_t *kv, uint64_t off) { + int32_t id = art_balloc(kv, ART_N4); + if (id < 0) + return -1; + art_n4_t *x = art_blk(kv, id); + memset(x, 0, sizeof(*x)); + x->h.type = ART_N4; + art_val_store(&x->h, AV_MAKE(1, off)); + return id; +} +static int32_t art_new_node(kvspace_t *kv, int t) { + int32_t id = art_balloc(kv, t); + if (id < 0) + return -1; + void *x = art_blk(kv, id); + int sz = art_node_sz(t); + memset(x, 0, sz); + ((art_hdr_t *)x)->type = (uint8_t)t; + if (t == ART_N48) + memset(((art_n48_t *)x)->index, 255, 256); + if (t == ART_N256) { + art_n256_t *n = x; + for (int i = 0; i < 256; i++) + n->children[i] = -1; + } + return id; +} + +/* ---- grow ---- */ +static int32_t art_grow(kvspace_t *kv, void *on) { + art_hdr_t *oh = on; + int nt; + if (oh->type == ART_N4) + nt = ART_N16; + else if (oh->type == ART_N16) + nt = ART_N48; + else if (oh->type == ART_N48) + nt = ART_N256; + else + return -1; + int32_t nid = art_new_node(kv, nt); + if (nid < 0) + return -1; + art_hdr_t *nh = art_hdr(kv, nid); + nh->prefix_len = oh->prefix_len; + memcpy(nh->prefix, oh->prefix, oh->prefix_len); + art_val_store(nh, art_val_load(oh)); + // copy children + for (int i = 0; i < (int)oh->count; i++) { + uint8_t b = 0; + int32_t c = -1; + switch (oh->type) { + case ART_N4: { + art_n4_t *x = on; + b = x->keys[i]; + c = x->children[i]; + break; + } + case ART_N16: { + art_n16_t *x = on; + b = x->keys[i]; + c = x->children[i]; + break; + } + case ART_N48: { + art_n48_t *x = on; + c = x->children[i]; + for (int j = 0; j < 256; j++) + if (x->index[j] == i) { + b = (uint8_t)j; + break; + } + break; + } + } + switch (nt) { + case ART_N16: { + art_n16_t *x = (art_n16_t *)nh; + x->keys[i] = b; + x->children[i] = c; + break; + } + case ART_N48: { + art_n48_t *x = (art_n48_t *)nh; + x->index[b] = (uint8_t)i; + x->children[i] = c; + break; + } + case ART_N256: { + ((art_n256_t *)nh)->children[b] = c; + break; + } + } + } + nh->count = oh->count; + if (nt == ART_N16) { + art_n16_t *x = (art_n16_t *)nh; + for (int i = 0; i < (int)nh->count - 1; i++) + for (int j = i + 1; j < (int)nh->count; j++) + if (x->keys[i] > x->keys[j]) { + uint8_t tk = x->keys[i]; + x->keys[i] = x->keys[j]; + x->keys[j] = tk; + int32_t tc = x->children[i]; + x->children[i] = x->children[j]; + x->children[j] = tc; + } + } + { /* P2: 搬迁 —— 新节点 struct_gen = 旧 +1;旧节点也动一次 */ + int32_t oid = art_id_of(kv, oh); + (void)gens_at; + gen_bump_struct(kv, nid); /* 新节点 gen 先抬一代 */ + gen_bump_struct(kv, oid); /* 旧节点也失效 */ + } + oh->type = ART_MOVED; + art_val_store(oh, AV_MAKE(0, (uint32_t)nid)); + return nid; +} + +/* ---- add child ---- */ +static int art_add(kvspace_t *kv, void *n, uint8_t b, int32_t cid) { + gen_bump_struct(kv, art_id_of(kv, n)); /* P2: 子集变化 */ + art_hdr_t *h = n; + switch (h->type) { + case ART_N4: { + if (h->count >= 4) + return -1; + art_n4_t *x = n; + x->keys[h->count] = b; + x->children[h->count] = cid; + h->count++; + return 0; + } + case ART_N16: { + if (h->count >= 16) + return -1; + art_n16_t *x = n; + int p = (int)h->count; + while (p > 0 && x->keys[p - 1] > b) { + x->keys[p] = x->keys[p - 1]; + x->children[p] = x->children[p - 1]; + p--; + } + x->keys[p] = b; + x->children[p] = cid; + h->count++; + return 0; + } + case ART_N48: { + if (h->count >= 48) + return -1; + art_n48_t *x = n; + int s = (int)h->count; + x->index[b] = (uint8_t)s; + x->children[s] = cid; + h->count++; + return 0; + } + case ART_N256: { + art_n256_t *x = n; + if (x->children[b] >= 0) + return -1; + x->children[b] = cid; + h->count++; + return 0; + } + } + return -1; +} + +/* ---- remove child ---- */ +static void art_rm(kvspace_t *kv, void *n, uint8_t b) { + art_hdr_t *h = n; + switch (h->type) { + case ART_N4: { + art_n4_t *x = n; + for (int i = 0; i < (int)h->count; i++) + if (x->keys[i] == b) { + for (int j = i; j < (int)h->count - 1; j++) { + x->keys[j] = x->keys[j + 1]; + x->children[j] = x->children[j + 1]; + } + h->count--; + return; + } + break; + } + case ART_N16: { + art_n16_t *x = n; + int lo = 0, hi = (int)h->count - 1, pos = -1; + while (lo <= hi) { + int m = (lo + hi) / 2; + if (x->keys[m] == b) { + pos = m; + break; + } + if (x->keys[m] < b) + lo = m + 1; + else + hi = m - 1; + } + if (pos < 0) + return; + for (int j = pos; j < (int)h->count - 1; j++) { + x->keys[j] = x->keys[j + 1]; + x->children[j] = x->children[j + 1]; + } + h->count--; + break; + } + case ART_N48: { + art_n48_t *x = n; + uint8_t idx = x->index[b]; + if (idx == 255) + return; + x->index[b] = 255; + int last = (int)h->count - 1; + if (idx != last) { + x->children[idx] = x->children[last]; + for (int j = 0; j < 256; j++) + if (x->index[j] == last) { + x->index[j] = idx; + break; + } + } + h->count--; + break; + } + case ART_N256: { + art_n256_t *x = n; + x->children[b] = -1; + h->count--; + break; + } + } +} + +/* ---- 叶子链:key[d..klen) 建链,末端叶子存 off(尾段超 ART_PREFIX_MAX 分级) + * ---- */ +static int32_t art_leaf_chain(kvspace_t *kv, const uint8_t *key, int klen, + int d, uint64_t off) { + int rem = klen - d; + if (rem <= ART_PREFIX_MAX) { + int32_t id = art_new_leaf(kv, off); + if (id < 0) + return -1; + art_n4_t *x = art_blk(kv, id); + if (rem > 0) + memcpy(x->h.prefix, key + d, (size_t)rem); + x->h.prefix_len = (uint8_t)rem; + return id; + } + int32_t id = art_new_node(kv, ART_N4); + if (id < 0) + return -1; + art_hdr_t *x = art_hdr(kv, id); + memcpy(x->prefix, key + d, ART_PREFIX_MAX); + x->prefix_len = ART_PREFIX_MAX; + int32_t sub = art_leaf_chain(kv, key, klen, d + ART_PREFIX_MAX + 1, off); + if (sub < 0) + return -1; + art_add(kv, x, key[d + ART_PREFIX_MAX], sub); + return id; +} + +/* ---- insert ---- */ +static int32_t art_ins(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, + int d, uint64_t off) { + if (nid < 0) + return art_leaf_chain(kv, key, klen, d, off); + + art_hdr_t *h = art_hdr(kv, nid); + if (!h) + return -1; + int shared = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); + int mpl = h->prefix_len < (klen - d) ? h->prefix_len : (klen - d); + + if (shared == mpl && (klen - d) < h->prefix_len) { + /* key 在节点 prefix 内部耗尽(含 d==klen):拆出 shared + * 字节作新值,旧节点作 child */ + int32_t nn = art_new_node(kv, ART_N4); + if (nn < 0) + return -1; + art_hdr_t *nh = art_hdr(kv, nn); + nh->prefix_len = (uint8_t)shared; + if (shared > 0) + memcpy(nh->prefix, key + d, (size_t)shared); + uint8_t ob = h->prefix[shared]; + h->prefix_len -= (uint8_t)(shared + 1); + if (h->prefix_len > 0) + memmove(h->prefix, h->prefix + shared + 1, h->prefix_len); + art_add(kv, nh, ob, nid); + art_val_store(nh, AV_MAKE(1, off)); + return nn; + } + if (shared < mpl) { // prefix split + int32_t nn = art_new_node(kv, ART_N4); + if (nn < 0) + return -1; + art_hdr_t *nh = art_hdr(kv, nn); + nh->prefix_len = (uint8_t)shared; + memcpy(nh->prefix, key + d, shared); + uint8_t ob = h->prefix[shared]; + h->prefix_len -= (uint8_t)(shared + 1); + if (h->prefix_len > 0) + memmove(h->prefix, h->prefix + shared + 1, h->prefix_len); + art_add(kv, nh, ob, nid); + int32_t leaf = art_leaf_chain(kv, key, klen, d + shared + 1, off); + if (leaf < 0) + return -1; + art_add(kv, nh, key[d + shared], leaf); + return nn; + } + d += h->prefix_len; + if (d == klen) { + art_val_store(h, AV_MAKE(1, off)); + return nid; + } + int32_t cid = art_child(kv, h, key[d]); + if (cid >= 0) { + int32_t nc = art_ins(kv, cid, key, klen, d + 1, off); + if (nc < 0) + return -1; + if (nc != cid) { + art_rm(kv, h, key[d]); + art_add(kv, h, key[d], nc); + } + return nid; + } + int32_t leaf = art_leaf_chain(kv, key, klen, d + 1, off); + if (leaf < 0) + return -1; + if ((h->type == ART_N4 && h->count >= 4) || + (h->type == ART_N16 && h->count >= 16) || + (h->type == ART_N48 && h->count >= 48)) { + int32_t g = art_grow(kv, h); + if (g < 0) + return -1; + art_add(kv, art_hdr(kv, g), key[d], leaf); + return g; + } + art_add(kv, h, key[d], leaf); + return nid; +} + +/* ---- delete ---- */ +static int32_t art_del(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, + int d, bool *del) { + if (nid < 0) + return -1; + art_hdr_t *h = art_hdr(kv, nid); + if (!h) + return -1; + if (h->prefix_len) { + int s = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); + if (s != h->prefix_len || d + h->prefix_len > klen) + return nid; + d += h->prefix_len; + } + if (d == klen) { + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return nid; + uint64_t gidx; + if (garb_reserve(kv, &gidx) != 0) + return -1; /* 表满:硬错误,未变更 */ + if (!art_val_cas(h, &hv, 0)) { + garb_cancel(kv, gidx); + return nid; + } + gen_bump_value(kv, art_id_of(kv, h)); /* P2: 值删 */ + gen_bump_struct(kv, art_id_of(kv, h)); /* P2: 槽消失,view 失效 */ + garb_commit(kv, gidx, AV_OFF(hv), + sbo_allocated_size(kv->sbo_meta, AV_OFF(hv))); + *del = true; + } else { + int32_t cid = art_child(kv, h, key[d]); + if (cid >= 0) { + int32_t rc = art_del(kv, cid, key, klen, d + 1, del); + if (rc < 0) + return rc; + } + } + return nid; +} + +/* ---- path ---- */ +static char *pjoin(const char *a, const char *b) { + size_t al = strlen(a), bl = strlen(b); + int sep = (al > 0 && a[al - 1] != '/') ? 1 : 0; + char *r = malloc(al + sep + bl + 1); + memcpy(r, a, al); + if (sep) + r[al] = '/'; + memcpy(r + al + sep, b, bl + 1); + return r; +} +static void psplit(const char *k, char **p, char **n) { + const char *s = strrchr(k, '/'); + if (!s || s == k) { + *p = strdup("/"); + *n = strdup(s ? s + 1 : k); + return; + } + size_t pl = (size_t)(s - k) + 1; /* 含尾 '/' */ + *p = malloc(pl + 1); + memcpy(*p, k, pl); + (*p)[pl] = '\0'; + *n = strdup(s + 1); +} +/* 非法目录前缀(不是 / 且不以 / 或 · 结尾)→ 返回非 0,对齐 durable + * validate_dir。 */ +static int bad_dir_prefix(const char *p) { + if (!p || !p[0]) + return 1; + if (strcmp(p, "/") == 0) + return 0; + size_t l = strlen(p); + if (p[l - 1] == '/') + return 0; + if (l >= 2 && (unsigned char)p[l - 2] == 0xC2 && + (unsigned char)p[l - 1] == 0xB7) + return 0; + return 1; +} + +static char *edir(const char *p) { + size_t l = strlen(p); + if (l > 0 && p[l - 1] == '/') + return strdup(p); + char *r = malloc(l + 2); + memcpy(r, p, l); + r[l] = '/'; + r[l + 1] = '\0'; + return r; +} +/* Covering node for pfx: *bpos is reconstructed length at arrival. */ +static int32_t art_cover(kvspace_t *kv, int32_t nid, const uint8_t *pfx, + int plen, char *buf, int bcap, int *bpos) { + int d = 0; + if (nid < 0 || !pfx || plen < 0 || !buf || !bpos) + return -1; + while (nid >= 0) { + art_hdr_t *h = art_hdr(kv, nid); + if (!h) + return -1; + int pd = d, cover = 0; + if (h->prefix_len) { + int s = pfx_shared(h->prefix, h->prefix_len, pfx + d, plen - d); + if (s < h->prefix_len) { + if (d + s < plen) + return -1; + cover = 1; + } else + d += h->prefix_len; + } + if (cover || d == plen) { + if (pd >= bcap) + return -1; + if (pd) + memcpy(buf, pfx, (size_t)pd); + *bpos = pd; + return nid; + } + nid = art_child(kv, h, pfx[d]); + if (nid < 0) + return -1; + d++; + } + return -1; +} + +typedef struct { + char **keys; + int32_t count; + int32_t cap; +} keyscan_t; + +static void keyscan_free(keyscan_t *scan) { + for (int32_t i = 0; i < scan->count; i++) + free(scan->keys[i]); + free(scan->keys); + scan->keys = NULL; + scan->count = scan->cap = 0; +} + +static int keyscan_add_n(keyscan_t *scan, const char *key, size_t len) { + if (scan->count == INT32_MAX) + return -1; + if (scan->count == scan->cap) { + int32_t cap = scan->cap > INT32_MAX / 2 ? INT32_MAX : + scan->cap ? scan->cap * 2 : 64; + char **keys = realloc(scan->keys, (size_t)cap * sizeof *keys); + if (!keys) + return -1; + scan->keys = keys; + scan->cap = cap; + } + char *copy = strndup(key, len); + if (!copy) + return -1; + scan->keys[scan->count++] = copy; + return 0; +} + +/* Direct children end at '/' or U+00B7. */ +static int child_name_len(const char *rest, int restlen) { + for (int i = 0; i < restlen; i++) { + if (rest[i] == '/') + return i; + if (i + 1 < restlen && (unsigned char)rest[i] == 0xC2 && + (unsigned char)rest[i + 1] == 0xB7) + return i; + } + return restlen; +} + +/* Deleted values leave ART nodes behind. */ +static int art_has_live_value(kvspace_t *kv, int32_t nid) { + if (nid < 0) + return 0; + art_hdr_t *h = art_hdr(kv, nid); + if (!h) + return -1; + if (AV_HAS(art_val_load(h))) + return 1; + const int32_t *children; + int count = h->count; + switch (h->type) { + case ART_N4: + children = ((art_n4_t *)h)->children; + break; + case ART_N16: + children = ((art_n16_t *)h)->children; + break; + case ART_N48: + children = ((art_n48_t *)h)->children; + break; + case ART_N256: + children = ((art_n256_t *)h)->children; + count = 256; + break; + default: + return 0; + } + for (int i = 0; i < count; i++) { + int live = art_has_live_value(kv, children[i]); + if (live != 0) + return live; + } + return 0; +} + +static int art_scan_children(kvspace_t *kv, int32_t nid, char *buf, int bpos, int bcap, + int plen, keyscan_t *scan) { + if (nid < 0) + return 0; + art_hdr_t *h = art_hdr(kv, nid); + if (!h || h->prefix_len > bcap - bpos - 1) + return -1; + /* Recheck the last parent byte for a split U+00B7. */ + int start = bpos > plen + 1 ? bpos - plen - 2 : 0; + for (int i = 0; i < h->prefix_len; i++) + buf[bpos++] = h->prefix[i]; + buf[bpos] = 0; + if (bpos > plen) { + const char *rest = buf + plen; + int restlen = bpos - plen; + int len = start + child_name_len(rest + start, restlen - start); + /* Stop at the child boundary. */ + if (len < restlen) { + if (len == 0) + return 0; + int live = art_has_live_value(kv, nid); + if (live <= 0) + return live; + if (rest[len] == '/' && len + 1 == restlen && + AV_HAS(art_val_load(h))) + len++; + return keyscan_add_n(scan, rest, (size_t)len); + } + if (AV_HAS(art_val_load(h)) && + keyscan_add_n(scan, rest, (size_t)len) != 0) + return -1; + } + switch (h->type) { + case ART_N4: { + art_n4_t *x = (art_n4_t *)h; + for (int i = 0; i < (int)h->count; i++) { + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = x->keys[i]; + if (art_scan_children(kv, x->children[i], buf, bpos + 1, bcap, + plen, scan) != 0) + return -1; + } + break; + } + case ART_N16: { + art_n16_t *x = (art_n16_t *)h; + for (int i = 0; i < (int)h->count; i++) { + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = x->keys[i]; + if (art_scan_children(kv, x->children[i], buf, bpos + 1, bcap, + plen, scan) != 0) + return -1; + } + break; + } + case ART_N48: { + art_n48_t *x = (art_n48_t *)h; + for (int i = 0; i < 256; i++) { + if (x->index[i] == 255) + continue; + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (uint8_t)i; + if (art_scan_children(kv, x->children[x->index[i]], buf, bpos + 1, + bcap, plen, scan) != 0) + return -1; + } + break; + } + case ART_N256: { + art_n256_t *x = (art_n256_t *)h; + for (int i = 0; i < 256; i++) { + if (x->children[i] < 0) + continue; + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (uint8_t)i; + if (art_scan_children(kv, x->children[i], buf, bpos + 1, bcap, + plen, scan) != 0) + return -1; + } + break; + } + } + return 0; +} + +static int art_scan_pfx(kvspace_t *kv, const char *pfx, int plen, char *buf, + int bcap, keyscan_t *scan) { + if (plen >= bcap) + return -1; + int bpos = 0; + int32_t nid = art_cover(kv, kv->hdr->art_root, (const uint8_t *)pfx, + plen, buf, bcap, &bpos); + if (nid < 0) + return 0; + return art_scan_children(kv, nid, buf, bpos, bcap, plen, scan); +} + +/* ============ lifecycle ============ */ +/* 8 * 64^k; s wraps to 0. */ +static bool sbo_data_size_ok(size_t n) { + for (uint64_t s = 8; s; s *= 64) + if (s == n) + return true; + return false; +} + +kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { + if (!path) + return NULL; + char head_path[PATH_MAX], data_path[PATH_MAX], gen_path[PATH_MAX], + garb_path[PATH_MAX]; + if (snprintf(head_path, sizeof head_path, "%s.sbo.head", path) >= + (int)sizeof head_path || + snprintf(data_path, sizeof data_path, "%s.sbo.data", path) >= + (int)sizeof data_path || + snprintf(gen_path, sizeof gen_path, "%s.gen", path) >= + (int)sizeof gen_path || + snprintf(garb_path, sizeof garb_path, "%s.garb", path) >= + (int)sizeof garb_path) + return NULL; + + kvspace_t *kv = calloc(1, sizeof(*kv)); + if (!kv) + return NULL; + kv->p2_slot = -1; + kv->r_art.fd = kv->r_head.fd = kv->r_data.fd = kv->r_gen.fd = + kv->r_garb.fd = -1; + + /* validate before O_EXCL create: no empty file left behind */ + bool created = false; + kv->r_art.fd = open(path, O_RDWR); + if (kv->r_art.fd < 0) { + if (!sbo_data_size_ok(data_size)) + goto fail; + kv->r_art.fd = open(path, O_RDWR | O_CREAT | O_EXCL, 0644); + if (kv->r_art.fd < 0) + goto fail; + created = true; + } + + size_t art_slab, sbo_head; + if (created) { + art_slab = ART_SLAB_INIT; + 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 || + memcmp(tmp.magic, KVS_MAGIC, sizeof(KVS_MAGIC) - 1) != 0) + goto fail; + art_slab = (size_t)tmp.art_slab_size; + sbo_head = (size_t)tmp.sbo_head_size; + data_size = (size_t)tmp.sbo_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, + ART_RESERVE) != 0 || + (kv->r_head.fd = open(head_path, fl, 0644)) < 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, DATA_RESERVE) != 0 || + (kv->r_gen.fd = open(gen_path, fl, 0644)) < 0 || + (kv->r_garb.fd = open(garb_path, fl, 0644)) < 0 || + region_attach(&kv->r_garb, created, + (size_t)GARB_CAP * sizeof(garb_ent_t), + (size_t)GARB_CAP * sizeof(garb_ent_t)) != 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 = (sbo_meta_t *)kv->r_head.base; + kv->sbo_data = kv->r_data.base; + kv->garb = (garb_ent_t *)kv->r_garb.base; + + /* 每进程单调句柄序号:close 后重开即使 malloc 复用同一 kv 地址,旧 token 也失效。 */ + { + static uint32_t store_seq; + kv->store_id = ((uint64_t)getpid() << 32) | (uint64_t)++store_seq; + } + + if (created) { + memset(kv->hdr, 0, sizeof(*kv->hdr)); + kv->hdr->format_version = KVSPACE_FORMAT_VERSION; + kv->hdr->garb_cap = GARB_CAP; + kv->hdr->art_slab_size = art_slab; + kv->hdr->sbo_head_size = sbo_head; + kv->hdr->sbo_data_size = data_size; + kv->hdr->art_root = -1; + if (blocks_init(kv->art_meta, art_slab, ART_BLOCK_SZ) != 0 || + sbo_init(kv->sbo_meta, sbo_head, data_size) != 0) + goto fail; + /* blocks_init 按容量选 2/4/8 字节块头。4 字节头 + 8 对齐的块大小使 + * 节点地址交替落在 8k+4,art_hdr_t.val(偏移 16)跨 64B 行:x86 + * locked CAS 变成 split lock(实测 ~7k tick/次,del p95 26k)。 + * 节点地址必须 8 字节对齐(原子字不可跨行),故强制 8 字节头。 */ + kv->art_meta->sizeof_block_head = 8; + if ((kv->art_meta->sizeof_block_head + + kv->art_meta->block_size) % 8 != 0) + goto fail; + /* magic last: half-initialized file fails reopen */ + memcpy(kv->hdr->magic, KVS_MAGIC, sizeof(KVS_MAGIC) - 1); + } + if (!created && + (kv->hdr->format_version != KVSPACE_FORMAT_VERSION || + kv->art_meta->sizeof_block_head != 8 || + kv->hdr->garb_cap != GARB_CAP)) { + fprintf(stderr, "kvspace: unsupported store format (version=%u, " + "garb_cap=%llu); rebuild the shm store\n", + kv->hdr->format_version, (unsigned long long)kv->hdr->garb_cap); + goto fail; + } + if (gens_grow(kv) != 0) /* P2: gen 表在 blocks_init 之后 */ + goto fail; + + /* growth code assumes the single-root pool layout (SBO_HEAD_FIXED) */ + if (kv->sbo_meta->root_slots != 1) + goto fail; + + /* P2: 注册表认领。占满(N=64)拒绝 open,错误明确,不静默扩容。 */ + kv->p2_slot = p2_reg_claim(kv); + if (kv->p2_slot == -2) { + fprintf(stderr, "kvspace: too many processes, N=%d\n", P2_SLOT_N); + goto fail; + } + if (kv->p2_slot == -3) { + fprintf(stderr, "kvspace: claim_id exhausted (2^30 opens): " + "rebuild the store\n"); + goto fail; + } + if (kv->p2_slot < 0) + goto fail; + kv->pid = (uint32_t)getpid(); + (void)pthread_once(&g_atfork_once, p2_atfork_install); + kv->fork_gen = __atomic_load_n(&g_fork_gen, __ATOMIC_ACQUIRE); + kv->quiesced = 1; /* claim 时 slot->quiesced=1,二者一致 */ + if (kv->hdr->committed_bytes - + __atomic_load_n(&kv->hdr->freed_bytes, __ATOMIC_ACQUIRE) >= + p2_garb_threshold() && + p2_maint_start(kv) != 0) + goto fail; /* 已有大量垃圾且建不了线程:open 硬失败 */ + + pthread_mutex_init(&kv->wlock, NULL); + for (int i = 0; i < WATCH_TABLE_SZ; i++) { + pthread_cond_init(&kv->watches[i].cond, NULL); + pthread_mutex_init(&kv->watches[i].mtx, NULL); + } + return kv; + +fail: + p2_maint_stop(kv); + if (kv->p2_slot >= 0 && kv->hdr) + p2_reg_release(kv); + region_close(&kv->r_art); + region_close(&kv->r_head); + region_close(&kv->r_data); + region_close(&kv->r_gen); + region_close(&kv->r_garb); + free(kv); + if (created) { + unlink(path); + unlink(head_path); + unlink(data_path); + unlink(gen_path); + unlink(garb_path); + } + return NULL; +} +/* 除自己外是否还有存活进程(macOS 无 /proc:只有 FREE 槽才算“不在”)。 */ +static bool p2_other_alive(kvspace_t *kv) { + for (int i = 0; i < P2_SLOT_N; i++) { + if (i == kv->p2_slot) + continue; + p2_slot_t *s = &kv->hdr->slots[i]; + if (__atomic_load_n(&s->state, __ATOMIC_ACQUIRE) != P2_SLOT_MINE) + continue; + if (p2_slot_alive(s)) + return true; + } + return false; +} + +void kvspaceShmClose(kvspace_t *kv) { + if (!kv) + return; + p2_setstats_dump(); + /* 最后一个存活进程负责清场:Linux 跑一次机会性 round;macOS 直接清环。 */ + if (kv->pid == (uint32_t)getpid() && kv->p2_slot >= 0 && + !p2_other_alive(kv)) { +#if defined(__linux__) + __atomic_store_n(&kv->hdr->slots[kv->p2_slot].quiesced, 1u, + __ATOMIC_RELEASE); + (void)p2_round(kv); +#else + uint64_t h = __atomic_load_n(&kv->hdr->garb_head, __ATOMIC_ACQUIRE); + uint64_t t = __atomic_load_n(&kv->hdr->garb_tail, __ATOMIC_ACQUIRE); + for (; h < t; h++) { + garb_ent_t *e = &kv->garb[h & (GARB_CAP - 1)]; + uint32_t m = __atomic_load_n(&e->meta, __ATOMIC_ACQUIRE); + if (GE_STATE(m) == GE_COMMITTED) { + uint64_t off = __atomic_load_n(&e->off, __ATOMIC_ACQUIRE); + uint64_t cap = sbo_allocated_size(kv->sbo_meta, off); + __atomic_store_n(&e->meta, GE_EMPTY, __ATOMIC_RELEASE); + kv_sbo_free(kv, off); + __atomic_store_n(&kv->hdr->freed_bytes, + kv->hdr->freed_bytes + cap, __ATOMIC_RELEASE); + } else { + __atomic_store_n(&e->meta, GE_EMPTY, __ATOMIC_RELEASE); + } + } + __atomic_store_n(&kv->hdr->garb_head, t, __ATOMIC_RELEASE); +#endif + } + p2_maint_stop(kv); + p2_reg_release(kv); + for (int i = 0; i < WATCH_TABLE_SZ; i++) { + pthread_cond_destroy(&kv->watches[i].cond); + pthread_mutex_destroy(&kv->watches[i].mtx); + free(kv->watches[i].val); + } + pthread_mutex_destroy(&kv->wlock); + region_close(&kv->r_art); + region_close(&kv->r_head); + region_close(&kv->r_data); + region_close(&kv->r_gen); + region_close(&kv->r_garb); + free(kv); +} + +/* ---- link resolve helpers ---- */ +static int read_value(kvspace_t *kv, uint64_t off, uint8_t **out, int32_t *ol) { + uint8_t *s = kv->sbo_data + off; + size_t sz = sbo_allocated_size(kv->sbo_meta, off); + kvspaceXvalueHead wire; + if (head_of(s, (int32_t)sz, &wire) == 0) { + uint64_t l = kvspaceXvalueHeadLen(&wire); + if (l > INT32_MAX) + return -1; + *out = s; + /* 完整值长 = head + body + attr(attr_len:u16 @8+len 自描述;空 attr=2)。 */ + if ((uint64_t)sz < KVSPACE_XVALUE_HEAD_LEN + l + 2) + return -1; + uint64_t alen = kvspaceXvalueHeadAttrLen(s, l); + if (alen < 2 || alen > (uint64_t)sz - KVSPACE_XVALUE_HEAD_LEN - l) + return -1; + *ol = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l + alen); + return 0; + } + return -1; +} +/* link 解析:ptr(ref=1)与 @ext(ref=2)的 XValue 恒为叶子(spec 硬规则——指针/扩展键 + * 之后不可能再有成员,成员只挂在 target 上),故路径中任何 `/` 分隔的容器前缀都绝不会是 + * link;唯一可能是 ptr 的只有整键叶子本身。因此只需查整键一次:非 ptr 即完成,ptr 则顺链 + * 到 target(本身又是一个完整叶子键)再查,最多追 16 跳。叶子不存在→非链,原样返回(调用 + * 方另做 extindex/dir 兜底)。取代旧的「逐前缀 1~3 次 art_search + 追加 rest」全下降探测。 */ +static void resolve_path(kvspace_t *kv, const char *path, char *out, int osz) { + strncpy(out, path, osz - 1); + out[osz - 1] = '\0'; + for (int depth = 0; depth < 16; depth++) { + art_hdr_t *h = + art_search(kv, kv->hdr->art_root, (const uint8_t *)out, (int)strlen(out)); + if (!h) + return; + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return; + uint8_t *raw; + int32_t rl; + if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0) + return; + kvspaceXvalueHead wire; + if (head_of(raw, rl, &wire) == 0) { + uint64_t l2 = kvspaceXvalueHeadLen(&wire); + if (wire.ptr != KVSPACE_XVALUE_PTR_PTR || l2 >= (uint64_t)osz) + return; + memcpy(out, raw + KVSPACE_XVALUE_HEAD_LEN, (size_t)l2); + out[l2] = 0; + continue; + } + return; + } +} + +/* resolve_path 的融合版:解析 link 链的同时把终端叶子的值一并取出,令调用方省掉 + * 「解析后再 art_search + read_value」的整套重复下降。每跳只取一次 head(ref 判定与 + * 值长度共用),命中带值节点即返 fetched=1(*raw/*rl 就位)。语义与 + * resolve_path + art_search + read_value 逐字等价:未命中/无值 → 返回该节点、fetched=0, + * 交调用方走 extindex/dir 兜底。 */ +static art_hdr_t *resolve_fetch(kvspace_t *kv, const char *path, char *out, int osz, + uint8_t **raw, int32_t *rl, int *fetched) { + *fetched = 0; + strncpy(out, path, osz - 1); + out[osz - 1] = '\0'; + for (int depth = 0; depth < 16; depth++) { + art_hdr_t *h = + art_search(kv, kv->hdr->art_root, (const uint8_t *)out, (int)strlen(out)); + if (!h) + return h; + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return h; + uint8_t *s = kv->sbo_data + AV_OFF(hv); + size_t sz = sbo_allocated_size(kv->sbo_meta, AV_OFF(hv)); + kvspaceXvalueHead wire; + if (head_of(s, (int32_t)sz, &wire) == 0) { + uint64_t l3 = kvspaceXvalueHeadLen(&wire); + if (wire.ptr != KVSPACE_XVALUE_PTR_PTR || l3 >= (uint64_t)osz) { + /* 完整值长 = head + body + attr(attr_len @8+len 自描述)。 */ + if ((uint64_t)sz < KVSPACE_XVALUE_HEAD_LEN + l3 + 2) + return NULL; + uint64_t alen = kvspaceXvalueHeadAttrLen(s, l3); + if (alen < 2 || alen > (uint64_t)sz - KVSPACE_XVALUE_HEAD_LEN - l3) + return NULL; + *raw = s; + *rl = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l3 + alen); + *fetched = 1; + return h; + } + memcpy(out, s + KVSPACE_XVALUE_HEAD_LEN, (size_t)l3); + out[l3] = 0; + continue; + } + return NULL; + } + return NULL; +} + +/* Read the extension locator stored at a directory key. */ +static int dir_ext_path(kvspace_t *kv, const char *dir, char *out, int osz) { + out[0] = 0; + art_hdr_t *h = + art_search(kv, kv->hdr->art_root, (const uint8_t *)dir, (int)strlen(dir)); + if (!h) + return 0; + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return 0; + uint8_t *raw; + int32_t rl; + if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0) + return 0; + kvspaceXvalueHead wire; + if (head_of(raw, rl, &wire) != 0 || + wire.ptr != KVSPACE_XVALUE_PTR_EXT) + return 0; + uint64_t l4 = kvspaceXvalueHeadLen(&wire); + if (l4 == 0 || l4 >= (uint64_t)osz) + return 0; + memcpy(out, raw + KVSPACE_XVALUE_HEAD_LEN, (size_t)l4); + out[l4] = 0; + return 1; +} + +/* ============ CRUD ============ */ +uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, + int32_t *ol) { + if (!kv || !key || !ol) + return NULL; + *ol = 0; + if (reserved_meta_path(key)) + return NULL; + if (kv_sync(kv) != 0) + return NULL; + char kbuf[1024]; + uint8_t *raw; + int32_t rl; + int fetched = 0; + art_hdr_t *h; + if (resolve) + h = resolve_fetch(kv, key, kbuf, sizeof(kbuf), &raw, &rl, &fetched); + else { + strncpy(kbuf, key, sizeof(kbuf) - 1); + kbuf[sizeof(kbuf) - 1] = '\0'; + h = art_search(kv, kv->hdr->art_root, (const uint8_t *)kbuf, + (int)strlen(kbuf)); + } + uint64_t hv = h ? art_val_load(h) : 0; + if (!h || !AV_HAS(hv)) { + /* extindex fallback:父目录是 extindex → 读 extpath + name */ + char *parent = NULL, *name = NULL; + psplit(kbuf, &parent, &name); + char extpath[1024]; + if (dir_ext_path(kv, parent, extpath, sizeof extpath)) { + char *target = pjoin(extpath, name); + art_hdr_t *eh = art_search(kv, kv->hdr->art_root, (const uint8_t *)target, + (int)strlen(target)); + free(target); + uint64_t ehv = eh ? art_val_load(eh) : 0; + if (eh && AV_HAS(ehv)) { + uint8_t *raw; + int32_t rl; + if (read_value(kv, AV_OFF(ehv), &raw, &rl) == 0) { + *ol = rl; + free(parent); + free(name); + return raw; + } + } + } + free(parent); + free(name); + return NULL; + } + if (!fetched && read_value(kv, AV_OFF(hv), &raw, &rl) < 0) + return NULL; + *ol = rl; + return raw; +} + +/* gen>0: block_id is parent at depth gen; key must share that prefix. */ +static int32_t ref_leaf(kvspace_t *kv, kvspaceRef_t *ref, const char *key) { + int32_t id = art_follow(kv, (int32_t)ref->block_id); + if (id < 0) + return -1; + ref->block_id = (uint32_t)id; + if (ref->gen == 0) + return id; + if (!key) + return -1; + int klen = (int)strlen(key); + int d = (int)ref->gen; + if (d < 0 || d > klen) /* P2: d == klen 表示前缀节点就是全部路径 */ + return -1; + if (d == klen) + return art_follow(kv, (int32_t)ref->block_id); + art_hdr_t *h = art_hdr(kv, id); + if (!h || h->type == ART_MOVED) + return -1; + int32_t cid = art_child(kv, h, (uint8_t)key[d]); + if (cid < 0) + return -1; + return art_walk(kv, cid, (const uint8_t *)key, klen, d + 1, NULL, NULL); +} + +int kvspaceShmResolveRef(kvspace_t *kv, const char *key, kvspaceRef_t *ref) { + if (!kv || !key || !ref || reserved_meta_path(key)) + return -1; + memset(ref, 0, sizeof(*ref)); + if (kv_sync(kv) != 0) + return -1; + int klen = (int)strlen(key); + int seplen = 0; + int sep = last_key_sep((const uint8_t *)key, klen, &seplen); + int dir_d = 0; + int32_t dn = -1; + int32_t id = art_find_dir(kv, kv->hdr->art_root, (const uint8_t *)key, klen, + sep, seplen, &dn, &dir_d); + if (id < 0) + return -1; + ref->block_id = (uint32_t)id; + ref->gen = 0; + if (dn >= 0 && dir_d > 0) { + ref->parent_id = (uint32_t)dn; + ref->depth = (uint32_t)dir_d; + } else { + ref->parent_id = 0; + ref->depth = 0; + } + return 0; +} + +uint8_t *kvspaceShmGetByRef(kvspace_t *kv, kvspaceRef_t *ref, + const char *key_fallback, int32_t *ol) { + if (!kv || !ref || !ol) + return NULL; + *ol = 0; + if (kv_sync(kv) != 0) + return NULL; + int32_t id = ref_leaf(kv, ref, key_fallback); + art_hdr_t *h = id >= 0 ? art_hdr(kv, id) : NULL; + uint64_t hv = h ? art_val_load(h) : 0; + if (h && AV_HAS(hv)) { + uint8_t *raw; + int32_t rl; + if (read_value(kv, AV_OFF(hv), &raw, &rl) == 0) { + *ol = rl; + return raw; + } + } + /* gen>0: parent walk miss — do not full-Get; caller falls back. */ + if (ref->gen != 0) + return NULL; + if (!key_fallback) + return NULL; + uint8_t *raw = kvspaceShmGet(kv, key_fallback, 0, ol); + if (raw) + kvspaceShmResolveRef(kv, key_fallback, ref); + return raw; +} + +/* 定位写的前置条件:只允许改 body [8, 8+len),head 与 attr 一律不可写。 */ +int kvspaceShmPartInBody(const uint8_t *raw, int32_t total, uint32_t offset, + uint32_t buf_len) { + if (!raw || total < (int32_t)KVSPACE_XVALUE_HEAD_LEN) + return 0; + kvspaceXvalueHead h; + memcpy(&h, raw, sizeof h); + const uint64_t len = kvspaceXvalueHeadLen(&h); + if (len > (uint64_t)total - KVSPACE_XVALUE_HEAD_LEN) + return 0; + if (offset < KVSPACE_XVALUE_HEAD_LEN) + return 0; + const uint64_t rel = (uint64_t)offset - KVSPACE_XVALUE_HEAD_LEN; + return rel <= len && (uint64_t)buf_len <= len - rel; +} + +int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, + const char *key_fallback, uint32_t offset, + const uint8_t *buf, uint32_t buf_len) { + if (!kv || !ref || !buf) + return -1; + if (kv_sync(kv) != 0) + return -1; + int32_t id = ref_leaf(kv, ref, key_fallback); + art_hdr_t *h = id >= 0 ? art_hdr(kv, id) : NULL; + /* SLACK 的 part-write 快路径:固定 8 字节 head 直读 + per-box 物理容量校验后 + * 写正文。任何不自洽(gen>0、无值、storetype/ptr 不符、offset/buf_len 越界、 + * 超出 box 物理容量)一律回退现有路径,行为不变。不修改 head。 */ + uint64_t hv = h ? art_val_load(h) : 0; + if (h && AV_HAS(hv) && ref->gen == 0) { + uint64_t data_size = kv->hdr->sbo_data_size; + uint64_t off = AV_OFF(hv); + if (off <= data_size && data_size - off >= KVSPACE_XVALUE_HEAD_LEN) { + const uint8_t *hs = kv->sbo_data + off; + kvspaceXvalueHead w; + memcpy(&w, hs, sizeof w); + if (w.storetype == KVSPACE_XVALUE_ST_SLACK && + w.ptr == KVSPACE_XVALUE_PTR_INLINE) { + uint64_t len = kvspaceXvalueHeadLen(&w); + /* per-box 物理容量:损坏 head 的 len 可能大于 box 但仍在全局内, + * 不做这一步会跨 box 写坏相邻值。 */ + uint64_t cap = sbo_allocated_size(kv->sbo_meta, off); + const uint64_t rel = + offset >= KVSPACE_XVALUE_HEAD_LEN + ? (uint64_t)offset - KVSPACE_XVALUE_HEAD_LEN + : UINT64_MAX; + if (cap <= data_size - off && len <= cap - KVSPACE_XVALUE_HEAD_LEN && + rel <= len && (uint64_t)buf_len <= len - rel) { + kv_gens_t *g = gens_at(kv, id); + if (g) + __atomic_add_fetch(&g->value_gen, 1u, + __ATOMIC_ACQ_REL); + memcpy(kv->sbo_data + off + offset, buf, buf_len); + if (g) + __atomic_add_fetch(&g->value_gen, 1u, + __ATOMIC_ACQ_REL); + return 0; + } + } + } + } + if (!h || !AV_HAS(hv)) { + if (ref->gen != 0) + return -1; + if (!key_fallback) + return -1; + int32_t rl = 0; + uint8_t *d = kvspaceShmGet(kv, key_fallback, 0, &rl); + if (!d || !kvspaceShmPartInBody(d, rl, offset, buf_len)) + return -1; + kvspaceShmResolveRef(kv, key_fallback, ref); + kv_gens_t *g = gens_at(kv, (int32_t)ref->block_id); + if (g) + __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); + memcpy(d + offset, buf, buf_len); + if (g) + __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); + return 0; + } + uint8_t *raw; + int32_t rl; + if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0 || + !kvspaceShmPartInBody(raw, rl, offset, buf_len)) + return -1; + kv_gens_t *g2 = gens_at(kv, id); + if (g2) + __atomic_add_fetch(&g2->value_gen, 1u, __ATOMIC_ACQ_REL); + memcpy(raw + offset, buf, buf_len); + if (g2) + __atomic_add_fetch(&g2->value_gen, 1u, __ATOMIC_ACQ_REL); + return 0; +} + +/* /vthread//‥status:合法内容长度上限与创建时物理容量下限。 */ +#define KVSPACE_STATUS_MAX_LEN 16u +#define KVSPACE_STATUS_MIN_CAP 80u + +static int is_vthread_status_key(const char *key) { + static const char pfx[] = "/vthread/"; + static const char suf[] = "/\xE2\x80\xA5status"; + if (!key) + return 0; + size_t n = strlen(key), sl = sizeof suf - 1; + return n > sl && strncmp(key, pfx, sizeof pfx - 1) == 0 && + memcmp(key + n - sl, suf, sl) == 0; +} + +int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, + const char *key_fallback, kvspaceXvalueBodyView_t *out) { + /* status 识别依赖 key_fallback;view 一律要求非 NULL(runtime 必须传)。 */ + if (!kv || !ref || !out || !key_fallback) + return -1; + if (ref->gen != 0) + return -1; + if (kv_sync(kv) != 0) + return -1; + kvspaceRef_t r = *ref; + int32_t id = ref_leaf(kv, &r, key_fallback); + art_hdr_t *h = id >= 0 ? art_hdr(kv, id) : NULL; + if (!h) + return -1; + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return -1; + uint8_t *raw = NULL; + int32_t rl = 0; + if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0 || !raw) + return -1; + kvspaceXvalueHead wire; + if (head_of(raw, rl, &wire) != 0) + return -1; + /* ptr/ext 槽不可视:没有稳定的 head/body 语义。 */ + if (wire.ptr != KVSPACE_XVALUE_PTR_INLINE) + return -1; + uint64_t cap = sbo_allocated_size(kv->sbo_meta, AV_OFF(hv)); + uint64_t l5 = kvspaceXvalueHeadLen(&wire); + if (cap < KVSPACE_XVALUE_HEAD_LEN || + l5 > cap - KVSPACE_XVALUE_HEAD_LEN) + return -1; + /* status:只暴露规范形状(SLACK、len≤16)且物理容量达标的槽; + 类型([n]char/utf8)在 x‥langtype attr key,不在此判。 */ + if (is_vthread_status_key(key_fallback)) { + if (wire.storetype != KVSPACE_XVALUE_ST_SLACK || + cap < KVSPACE_STATUS_MIN_CAP || + l5 > KVSPACE_STATUS_MAX_LEN) + return -1; + } + out->head = raw; + out->headlen = KVSPACE_XVALUE_HEAD_LEN; + out->body = raw + KVSPACE_XVALUE_HEAD_LEN; + out->cap = cap; + out->body_cap = cap - KVSPACE_XVALUE_HEAD_LEN; + return 0; +} + +/* 临时探针(P2_SETSTATS=):统计写入形态,用于静默屏障可行性评估。 */ +static struct { uint64_t same, realloc_, fresh, retired_bytes; } g_setstats; +static void p2_setstats_dump(void) { + const char *f = getenv("P2_SETSTATS"); + if (!f || !f[0]) + return; + FILE *fp = fopen(f, "a"); + if (!fp) + return; + fprintf(fp, "pid=%d same=%llu realloc=%llu fresh=%llu retired_bytes=%llu\n", + (int)getpid(), (unsigned long long)g_setstats.same, + (unsigned long long)g_setstats.realloc_, + (unsigned long long)g_setstats.fresh, + (unsigned long long)g_setstats.retired_bytes); + fclose(fp); +} + +/* 原始落盘(不处理容器/member 语义,供内部调用,避免递归)。 */ +static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, + int32_t val_len, art_hdr_t *old) { + { + static bool reg; + if (!reg) { reg = true; atexit(p2_setstats_dump); } + } + uint64_t ohv = old ? art_val_load(old) : 0; + bool retiring = old && AV_HAS(ohv); + uint64_t gidx = 0; + uint64_t cap = 0; + if (retiring) { + cap = sbo_allocated_size(kv->sbo_meta, AV_OFF(ohv)); + if ((uint64_t)val_len <= cap) { + /* 同尺寸就地写:指针不变,value_gen 奇偶保护(一次查表) */ + kv_gens_t *g = gens_at(kv, art_id_of(kv, old)); + if (g) + __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); + memcpy(kv->sbo_data + AV_OFF(ohv), val, (size_t)val_len); + if (g) + __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); + g_setstats.same++; + return 0; + } + /* 换 box:reserve 在任何变更之前;失败 = 硬错误、旧值原样 */ + if (garb_reserve(kv, &gidx) != 0) + return -1; +#ifdef P2_TEST_HOOKS + { + const char *v = getenv("P2_TEST_HOLD_RESERVE_MS"); + if (v && atoi(v) > 0) + usleep((useconds_t)atoi(v) * 1000); + } + if (getenv("P2_TEST_FAIL_ALLOC")) { + garb_cancel(kv, gidx); + return -1; + } +#endif + g_setstats.realloc_++; + g_setstats.retired_bytes += cap; + } else { + g_setstats.fresh++; + } + size_t alloc_len = (size_t)val_len; + if (is_vthread_status_key(key) && alloc_len < KVSPACE_STATUS_MIN_CAP) + alloc_len = KVSPACE_STATUS_MIN_CAP; + uint64_t off = kv_sbo_alloc(kv, alloc_len); + if (off == (uint64_t)-1) { + if (retiring) + garb_cancel(kv, gidx); + return -1; + } + memcpy(kv->sbo_data + off, val, val_len); +#ifdef P2_TEST_HOOKS + if (retiring && getenv("P2_TEST_FAIL_ARTINS")) { + garb_cancel(kv, gidx); + kv_sbo_free(kv, off); + return -1; + } +#endif + int32_t root = art_ins(kv, kv->hdr->art_root, (const uint8_t *)key, + (int)strlen(key), 0, off); + if (root < 0) { + if (retiring) + garb_cancel(kv, gidx); + kv_sbo_free(kv, off); /* 新 box 从未被引用 */ + return -1; + } + kv->hdr->art_root = root; + { /* 换 box / 墓碑复活:旧 token 全部 STALE */ + int32_t nid = p2_node_at(kv, key); + kv_gens_t *g = gens_at(kv, nid); + if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG reset-lookup key=%s nid=%d g=%p root=%d\n", key, nid, (void *)g, kv->hdr->art_root); } + if (g) { + gen_bump_struct(kv, nid); + gen_bump_value(kv, nid); + } + } + if (retiring) + garb_commit(kv, gidx, AV_OFF(ohv), cap); + return 0; +} + +static int shm_set_raw(kvspace_t *kv, const char *key, const uint8_t *val, + int32_t val_len) { + art_hdr_t *old = art_search(kv, kv->hdr->art_root, (const uint8_t *)key, + (int)strlen(key)); + return shm_set_local(kv, key, val, val_len, old); +} + +static int frame_operand_ptr(const char *key, const kvspaceXvalueHead *wire) { + if (wire->ptr != KVSPACE_XVALUE_PTR_PTR || + strncmp(key, "/vthread/", 9) != 0) + return 0; + const char *slot = strrchr(key, '/'); + if (!slot || slot[1] != '[') + return 0; + char *end; + long row = strtol(slot + 2, &end, 10); + if (row <= 0 || *end++ != ',') + return 0; + long col = strtol(end, &end, 10); + return col != 0 && *end++ == ']' && *end == '\0'; +} + +static int shm_set_validated(kvspace_t *kv, const char *key, + const uint8_t *val, int32_t val_len, + const kvspaceXvalueHead *wire) { + if (kv_sync(kv) != 0) + return -1; + char kbuf[1024]; + if (strlen(key) >= sizeof kbuf || strstr(key, "//") || + reserved_meta_path(key)) + return -1; + strcpy(kbuf, key); + + art_hdr_t *local = art_search(kv, kv->hdr->art_root, + (const uint8_t *)kbuf, (int)strlen(kbuf)); + /* Existing local values already shadow the extension. */ + uint64_t lhv = local ? art_val_load(local) : 0; + if (!local || !AV_HAS(lhv)) { + char *pp = NULL, *nn = NULL; + psplit(kbuf, &pp, &nn); + char extpath[1024]; + if (pp && nn && dir_ext_path(kv, pp, extpath, sizeof extpath)) { + char *tgt = pjoin(extpath, nn); + art_hdr_t *eh = art_search(kv, kv->hdr->art_root, + (const uint8_t *)tgt, (int)strlen(tgt)); + free(tgt); + if (eh && AV_HAS(art_val_load(eh)) && + !frame_operand_ptr(kbuf, wire)) { + free(pp); + free(nn); + return -1; + } + } + free(pp); + free(nn); + } + + /* status 固定档:形状固定(SLACK、len≤16)、物理容量 ≥ 下限。 + * 类型([n]char/utf8)在 x‥langtype attr key,不在此判。 */ + if (is_vthread_status_key(kbuf)) { + if (wire->storetype != KVSPACE_XVALUE_ST_SLACK || + kvspaceXvalueHeadLen(wire) > KVSPACE_STATUS_MAX_LEN) + return -1; + if (local && AV_HAS(lhv)) { + uint8_t *old_raw = kv->sbo_data + AV_OFF(lhv); + uint64_t old_sz = sbo_allocated_size(kv->sbo_meta, AV_OFF(lhv)); + kvspaceXvalueHead old; + /* 旧 store 的不合规 status 槽(形状/容量任一不符): + * 不自动升级、不 realloc——写入硬错,需重建/重置。 */ + if (head_of(old_raw, (int32_t)old_sz, &old) != 0 || + old.storetype != KVSPACE_XVALUE_ST_SLACK || + kvspaceXvalueHeadLen(&old) > KVSPACE_STATUS_MAX_LEN || + old_sz < KVSPACE_STATUS_MIN_CAP) + return -1; + } + } + return shm_set_local(kv, kbuf, val, val_len, local); +} + +int kvspaceShmSet(kvspace_t *kv, const char *key, const uint8_t *val, + int32_t val_len) { + if (!kv || !key || !val || val_len <= 0) + return -1; + kvspaceXvalueHead wire; + if (head_of(val, val_len, &wire) != 0) + return -1; + return shm_set_validated(kv, key, val, val_len, &wire); +} + +int kvspaceShmSetValue(kvspace_t *kv, const char *key, const uint8_t *val, + int32_t val_len) { + kvspaceXvalueHead head; + if (!kv || !key || !val || val_len <= 0 || + head_of(val, val_len, &head) != 0) + return -2; + return shm_set_validated(kv, key, val, val_len, &head); +} + +int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, + const char *key_fallback, const uint8_t *val, + int32_t val_len) { + kvspaceXvalueHead head; + if (!kv || !ref || !key_fallback || !val || val_len <= 0 || + head_of(val, val_len, &head) != 0) + return -2; + if (kv_sync(kv) != 0) + return -1; + if (strlen(key_fallback) >= 1024 || strstr(key_fallback, "//") || + reserved_meta_path(key_fallback)) + return -1; + int32_t id = ref_leaf(kv, ref, key_fallback); + art_hdr_t *h = id >= 0 ? art_hdr(kv, id) : NULL; + if (h && AV_HAS(art_val_load(h))) + return shm_set_local(kv, key_fallback, val, val_len, h); + /* ref 过期或尚无值:退回完整按 key 写(含 extindex 检查),成功后重解析。 */ + int rc = shm_set_validated(kv, key_fallback, val, val_len, &head); + if (rc == 0) + kvspaceShmResolveRef(kv, key_fallback, ref); + return rc; +} + +int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, + int32_t body_len, uint8_t **body) { + if (!kv || !key || !body || body_len < 0 || reserved_meta_path(key)) + return -1; + if (kv_sync(kv) != 0) + return -1; + char kbuf[1024]; + uint8_t *raw; + int32_t rl; + int fetched = 0; + art_hdr_t *h; + if (resolve) + h = resolve_fetch(kv, key, kbuf, sizeof(kbuf), &raw, &rl, &fetched); + else { + strncpy(kbuf, key, sizeof(kbuf) - 1); + kbuf[sizeof(kbuf) - 1] = '\0'; + h = art_search(kv, kv->hdr->art_root, (const uint8_t *)kbuf, + (int)strlen(kbuf)); + } + if (!h) + return -1; + uint64_t hv = art_val_load(h); + if (!AV_HAS(hv)) + return -1; + if ((!fetched && read_value(kv, AV_OFF(hv), &raw, &rl) < 0) || rl <= 0) + return -1; /* None 或读失败 → 强制走 NewPlace */ + kvspaceXvalueHead wire; + if (head_of(raw, rl, &wire) == 0) { + if (kvspaceXvalueHeadLen(&wire) != (uint64_t)body_len) + return -1; + *body = raw + KVSPACE_XVALUE_HEAD_LEN; + return 0; + } + return -1; +} + +int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, + uint8_t storetype, int32_t body_len, + uint64_t body_cap, const char *langtype, + uint8_t **body) { + if (!kv || !key || !body || body_len < 0) + return -1; + if (body_cap > INT32_MAX - 32) + return -1; + *body = NULL; + if (kv_sync(kv) != 0) + return -1; + size_t key_len = strlen(key); + if (key_len >= 1024 || strstr(key, "//") || reserved_meta_path(key)) + return -1; + uint8_t ptr; + if (ref == 0 && storetype <= KVSPACE_XVALUE_ST_EXT) + ptr = KVSPACE_XVALUE_PTR_INLINE; + else if (ref == 1 && storetype == KVSPACE_XVALUE_ST_SLACK) + ptr = KVSPACE_XVALUE_PTR_PTR; + else if (ref == 2 && storetype == KVSPACE_XVALUE_ST_EXT) + ptr = KVSPACE_XVALUE_PTR_EXT; + else + return -1; + uint8_t *value = NULL; + uint64_t total = 0; + if (kvspaceXvalueHeadReserve(storetype, (uint64_t)body_len, + body_cap, langtype, &value, &total) != 0) + return -1; + if (total > INT32_MAX) { + free(value); + return -1; + } + /* 固定头 8 字节:body 偏移恒 = 8;langtype 尾部 @8+body_len 整体单写。 */ + uint32_t headlen = KVSPACE_XVALUE_HEAD_LEN; + (void)ptr; + int rc = shm_set_raw(kv, key, value, (int32_t)total); + free(value); + if (rc != 0) + return -1; + art_hdr_t *node = art_search(kv, kv->hdr->art_root, + (const uint8_t *)key, (int)key_len); + if (!node) + return -1; + uint64_t nv = art_val_load(node); + if (!AV_HAS(nv)) + return -1; + *body = kv->sbo_data + AV_OFF(nv) + headlen; + return 0; +} + +int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool ex, int resolve, + int32_t *out_count) { + char **names; + int32_t count; + if (kvspaceShmList(kv, prefix, ex, resolve, &names, &count) != 0) { + *out_count = 0; + return -1; + } + for (int32_t i = 0; i < count; i++) + free(names[i]); + free(names); + *out_count = count; + return 0; +} + +int kvspaceShmDel(kvspace_t *kv, const char *key) { + if (!kv || !key) + return -1; + if (kv_sync(kv) != 0) + return -1; + char kbuf[1024]; + strncpy(kbuf, key, sizeof(kbuf) - 1); + kbuf[sizeof(kbuf) - 1] = '\0'; /* 删键本身,不穿透 link——显式解引用由 runtime 掌控 */ + bool d = false; + int32_t root = art_del(kv, kv->hdr->art_root, (const uint8_t *)kbuf, + (int)strlen(kbuf), 0, &d); + if (root < 0) + return -1; /* 表满等硬错误:树未变 */ + kv->hdr->art_root = root; + return d ? 0 : -1; +} + +int kvspaceShmDeltree(kvspace_t *kv, const char *prefix) { + if (!kv || !prefix || reserved_meta_path(prefix)) + return -1; + if (kv_sync(kv) != 0) + return -1; + // if prefix itself is a link, only delete the link + art_hdr_t *h = art_search(kv, kv->hdr->art_root, (const uint8_t *)prefix, + (int)strlen(prefix)); + uint64_t hv = h ? art_val_load(h) : 0; + if (h && AV_HAS(hv)) { + uint8_t *raw; + int32_t rl; + read_value(kv, AV_OFF(hv), &raw, &rl); + kvspaceXvalueHead wire; + int is_ptr = rl > 0 && head_of(raw, rl, &wire) == 0 && + wire.ptr == KVSPACE_XVALUE_PTR_PTR; + if (is_ptr) { + return kvspaceShmDel(kv, prefix); + } + } + char *e = edir(prefix); // ensure trailing / for listing children + char **ns; + int32_t nc; + kvspaceShmList(kv, e, false, 1, &ns, &nc); + for (int i = 0; i < nc; i++) { + char *c = pjoin(e, ns[i]); + kvspaceShmDeltree(kv, c); + free(c); + } + for (int i = 0; i < nc; i++) + free(ns[i]); + free(ns); + + /* 成员目录 marker(prefix·,U+00B7):成员 key = + prefix·,直接拼接后递归删除。 slash 版 e=prefix/ 覆盖不到 · + 成员(json 的 stringkeymap 落盘形态)。 */ + size_t pl = strlen(prefix); + char *m = malloc(pl + 3); + memcpy(m, prefix, pl); + m[pl] = (char)0xC2; + m[pl + 1] = (char)0xB7; + m[pl + 2] = 0; + char **ms; + int32_t mc; + kvspaceShmList(kv, m, false, 1, &ms, &mc); + for (int i = 0; i < mc; i++) { + size_t ml = strlen(m), nl = strlen(ms[i]); + char *c = malloc(ml + nl + 1); + memcpy(c, m, ml); + memcpy(c + ml, ms[i], nl + 1); + kvspaceShmDeltree(kv, c); + free(c); + } + for (int i = 0; i < mc; i++) + free(ms[i]); + free(ms); + kvspaceShmDel(kv, m); + free(m); + + kvspaceShmDel(kv, prefix); + if (strcmp(e, prefix) != 0) + kvspaceShmDel(kv, e); + free(e); + return 0; +} + +/* 成员目录 marker(p·,U+00B7)。 */ +static char *memdir(const char *p) { + size_t l = strlen(p); + char *r = malloc(l + 3); + memcpy(r, p, l); + r[l] = (char)0xC2; + r[l + 1] = (char)0xB7; + r[l + 2] = 0; + return r; +} + +/* Copy one value before Set can move its slab. */ +int kvspaceShmCp(kvspace_t *kv, const char *src, const char *dst) { + if (!kv || !src || !dst) + return -1; + int32_t rl; + uint8_t *raw = kvspaceShmGet(kv, src, 0, &rl); + if (!raw || rl <= 0) + return -1; + uint8_t *tmp = malloc((size_t)rl); + if (!tmp) + return -1; + memcpy(tmp, raw, (size_t)rl); + int rc = kvspaceShmSet(kv, dst, tmp, rl); + free(tmp); + return rc; +} + +/* Copy values and physical children recursively. */ +static int cptree_rec(kvspace_t *kv, const char *src, const char *dst) { + { + int32_t rl; + uint8_t *raw = kvspaceShmGet(kv, src, 0, &rl); + if (raw && rl > 0) { + if (kvspaceShmCp(kv, src, dst) != 0) + return -1; + } + } + char *es = edir(src), *ed = edir(dst); + char **ns; + int32_t nc; + if (kvspaceShmList(kv, es, false, 1, &ns, &nc) != 0) { + free(es); + free(ed); + return -1; + } + int rc = 0; + for (int i = 0; i < nc; i++) { + char *cs = pjoin(es, ns[i]), *cd = pjoin(ed, ns[i]); + if (!cs || !cd || cptree_rec(kv, cs, cd) != 0) + rc = -1; + free(cs); + free(cd); + if (rc != 0) + break; + } + for (int i = 0; i < nc; i++) + free(ns[i]); + free(ns); + free(es); + free(ed); + if (rc != 0) + return -1; + + char *ms = memdir(src), *md = memdir(dst); + char **mms; + int32_t mc; + if (kvspaceShmList(kv, ms, false, 1, &mms, &mc) != 0) { + free(ms); + free(md); + return -1; + } + for (int i = 0; i < mc; i++) { + size_t msl = strlen(ms), mdl = strlen(md), nl = strlen(mms[i]); + char *cs = malloc(msl + nl + 1); + char *cd = malloc(mdl + nl + 1); + if (!cs || !cd) { + rc = -1; + } else { + memcpy(cs, ms, msl); + memcpy(cs + msl, mms[i], nl + 1); + memcpy(cd, md, mdl); + memcpy(cd + mdl, mms[i], nl + 1); + if (cptree_rec(kv, cs, cd) != 0) + rc = -1; + } + free(cs); + free(cd); + if (rc != 0) + break; + } + for (int i = 0; i < mc; i++) + free(mms[i]); + free(mms); + free(ms); + free(md); + return rc; +} + +int kvspaceShmCptree(kvspace_t *kv, const char *src, const char *dst) { + if (!kv || !src || !dst || reserved_meta_path(src) || + reserved_meta_path(dst)) + return -1; + size_t sl = strlen(src); + while (sl > 1 && src[sl - 1] == '/') + sl--; + if (sl == 1 && src[0] == '/' && dst[0] == '/') + return dst[1] ? -1 : 0; + if (strncmp(src, dst, sl) == 0) { + if (dst[sl] == 0 || (dst[sl] == '/' && dst[sl + 1] == 0)) + return 0; + if (dst[sl] == '/' || + ((unsigned char)dst[sl] == 0xc2 && + (unsigned char)dst[sl + 1] == 0xb7)) + return -1; + } + kvspaceShmDeltree(kv, dst); + return cptree_rec(kv, src, dst); +} + +/* Copy the base value and direct member values. */ +int kvspaceShmCplist(kvspace_t *kv, const char *src, const char *dst) { + if (!kv || !src || !dst || reserved_meta_path(src) || + reserved_meta_path(dst)) + return -1; + if (strcmp(src, dst) == 0) + return 0; + kvspaceShmDeltree(kv, dst); + int32_t rl; + uint8_t *raw = kvspaceShmGet(kv, src, 0, &rl); + if (raw && rl > 0 && kvspaceShmCp(kv, src, dst) != 0) + return -1; + char *ms = memdir(src), *md = memdir(dst); + char **mms; + int32_t mc; + if (kvspaceShmList(kv, ms, false, 1, &mms, &mc) != 0) { + free(ms); + free(md); + return -1; + } + size_t msl = strlen(ms), mdl = strlen(md); + int rc = 0; + for (int i = 0; i < mc; i++) { + size_t nl = strlen(mms[i]); + char *cs = malloc(msl + nl + 1); + char *cd = malloc(mdl + nl + 1); + if (!cs || !cd) { + rc = -1; + } else { + memcpy(cs, ms, msl); + memcpy(cs + msl, mms[i], nl + 1); + memcpy(cd, md, mdl); + memcpy(cd + mdl, mms[i], nl + 1); + if (kvspaceShmCp(kv, cs, cd) != 0) + rc = -1; + } + free(cs); + free(cd); + if (rc != 0) + break; + } + for (int i = 0; i < mc; i++) + free(mms[i]); + free(mms); + free(ms); + free(md); + return rc; +} + +int kvspaceShmMkindex(kvspace_t *kv, const char *path, uint32_t capacity) { + if (!kv || !path) + return -1; + (void)capacity; + char *d = edir(path); + int32_t existing_len = 0; + if (kvspaceShmGet(kv, d, 0, &existing_len)) { + free(d); + return 0; + } + uint8_t *v = NULL; + uint64_t vl = 0; + int r = kvspaceXvalueHeadNewNone(&v, &vl); + if (r == 0) + r = kvspaceShmSetValue(kv, d, v, (int32_t)vl); + free(v); + free(d); + return r; +} + +static int child_cmp(const void *a, const void *b) { + const char *x = *(const char *const *)a; + const char *y = *(const char *const *)b; + size_t nx = strlen(x), ny = strlen(y); + char *tx = nx && x[nx - 1] == '/' ? strndup(x, nx - 1) : NULL; + char *ty = ny && y[ny - 1] == '/' ? strndup(y, ny - 1) : NULL; + int c = kvspaceCoordCmp(tx ? tx : x, ty ? ty : y); + free(tx); + free(ty); + return c ? c : strcmp(x, y); +} + +static int same_child(const char *a, const char *b) { + size_t na = strlen(a), nb = strlen(b); + if (na && a[na - 1] == '/') na--; + if (nb && b[nb - 1] == '/') nb--; + return na == nb && memcmp(a, b, na) == 0; +} + +int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, + char ***on, int32_t *oc) { + if (!kv || !prefix || !on || !oc) + return -1; + *on = NULL; + *oc = 0; + if (reserved_meta_path(prefix)) + return 0; + if (kv_sync(kv) != 0 || bad_dir_prefix(prefix)) + return -1; + const char *pfx = prefix; + char tbuf[1024]; + if (resolve) { + resolve_path(kv, prefix, tbuf, sizeof tbuf); + pfx = tbuf; + } + if (reserved_meta_path(pfx)) + return 0; + keyscan_t children = {0}; + char buf[4096]; + if (kv->hdr->art_root >= 0 && + art_scan_pfx(kv, pfx, (int)strlen(pfx), buf, (int)sizeof buf, &children) != 0) + goto fail; + + if (ex) { + char extpath[1024]; + char *dir = edir(pfx); + if (!dir) + goto fail; + int has_ext = dir_ext_path(kv, dir, extpath, sizeof extpath); + free(dir); + if (has_ext) { + if (art_scan_pfx(kv, extpath, (int)strlen(extpath), buf, + (int)sizeof buf, &children) != 0) + goto fail; + } + } + if (children.count > 1) + qsort(children.keys, (size_t)children.count, sizeof *children.keys, + child_cmp); + int32_t unique = 0; + for (int32_t i = 0; i < children.count; i++) { + if (strcmp(pfx, "/") == 0 && + strcmp(children.keys[i], ".kvspace-meta") == 0) { + free(children.keys[i]); + continue; + } + /* 成员名含 ‥(\xE2\x80\xA5)是系统变量/attr key(`0‥langtype` 等), + List 时隐藏——对齐 durable 后端(RUNTIME_MEMBER_SEP 隐藏)。 */ + { + const char *p = children.keys[i]; + for (; *p; p++) { + if ((unsigned char)p[0] == 0xE2 && (unsigned char)p[1] == 0x80 && + (unsigned char)p[2] == 0xA5) { + free(children.keys[i]); + goto hidden; + } + } + } + if (0) { +hidden: + continue; + } + if (unique && same_child(children.keys[unique - 1], children.keys[i])) { + if (children.keys[i][strlen(children.keys[i]) - 1] == '/') { + free(children.keys[unique - 1]); + children.keys[unique - 1] = children.keys[i]; + continue; + } + free(children.keys[i]); + continue; + } + children.keys[unique++] = children.keys[i]; + } + children.count = unique; + *on = children.keys; + *oc = children.count; + return 0; + +fail: + keyscan_free(&children); + return -1; +} + +int kvspaceShmExtindex(kvspace_t *kv, const char *p, const char *ep, + char *err, uint32_t err_cap) { + if (!kv || !p || !ep) + return -1; + /* 对齐 durable ext_index:path 与 ext_path 必须是目录形键(/ 或 · 结尾)。 */ + if (bad_dir_prefix(p) || bad_dir_prefix(ep)) { + if (err && err_cap) + snprintf(err, err_cap, + "kvspace: index must end with /: ExtIndex path=%s extpath=%s", + p, ep); + return -1; + } + int32_t source_len = 0; + uint8_t *source = kvspaceShmGet(kv, ep, 0, &source_len); + if (!source || source_len <= 0) { + if (err && err_cap) + snprintf(err, err_cap, "ExtIndex target missing: %s", ep); + return -1; + } + /* source 类型在 ep 值 attr 区(明文事实源);ext 句柄同样携带。 */ + char lt[256]; + const uint8_t *ltd = NULL; + uint16_t ltl = 0; + if (source_len >= (int32_t)KVSPACE_XVALUE_HEAD_LEN) { + kvspaceXvalueHead sh; + memcpy(&sh, source, sizeof sh); + if (sh.storetype <= KVSPACE_XVALUE_ST_EXT && + sh.ptr <= KVSPACE_XVALUE_PTR_EXT) { + uint64_t sbody = kvspaceXvalueHeadLen(&sh); + if (sbody <= (uint64_t)source_len - KVSPACE_XVALUE_HEAD_LEN) + ltl = kvspaceXvalueHeadLangtype(source, sbody, <d); + } + } + if (ltl > (uint16_t)(sizeof lt - 1)) + ltl = (uint16_t)(sizeof lt - 1); + memcpy(lt, ltd, ltl); + lt[ltl] = 0; + uint8_t *v = NULL; + uint64_t vl = 0; + int r = kvspaceXvalueHeadNewExt((const uint8_t *)ep, (uint32_t)strlen(ep), + lt, &v, &vl); + if (r == 0) + r = kvspaceShmSetValue(kv, p, v, (int32_t)vl); + free(v); + return r; +} +int kvspaceShmDelextindex(kvspace_t *kv, const char *p) { + return kvspaceShmDel(kv, p); +} + +/* ============ Watch/Notify ============ */ +static int wslot(const char *k) { + uint32_t h = 5381; + for (const char *p = k; *p; p++) + h = ((h << 5) + h) + (uint8_t)*p; + return (int)(h % WATCH_TABLE_SZ); +} +int kvspaceShmNotify(kvspace_t *kv, const char *k, const uint8_t *v, + int32_t vl) { + if (!kv || !k) + return -1; + int s = wslot(k); + pthread_mutex_lock(&kv->wlock); + watch_t *w = &kv->watches[s]; + if (w->key[0] && strcmp(w->key, k) == 0) { + pthread_mutex_lock(&w->mtx); + free(w->val); + w->val = v ? memcpy(malloc(vl), v, vl) : NULL; + w->val_len = vl; + w->ready = true; + pthread_cond_signal(&w->cond); + pthread_mutex_unlock(&w->mtx); + } + pthread_mutex_unlock(&kv->wlock); + return 0; +} +uint8_t *kvspaceShmWatch(kvspace_t *kv, const char *k, int32_t to, + int32_t *ol) { + if (!kv || !k || !ol) + return NULL; + *ol = 0; + int s = wslot(k); + pthread_mutex_lock(&kv->wlock); + watch_t *w = &kv->watches[s]; + strncpy(w->key, k, 255); + w->key[255] = '\0'; + w->ready = false; + free(w->val); + w->val = NULL; + pthread_mutex_unlock(&kv->wlock); + pthread_mutex_lock(&w->mtx); + if (!w->ready) { + struct timespec ts; + clock_gettime(CLOCK_REALTIME, &ts); + ts.tv_sec += to / 1000; + ts.tv_nsec += (to % 1000) * 1000000L; + if (ts.tv_nsec >= 1000000000L) { + ts.tv_sec++; + ts.tv_nsec -= 1000000000L; + } + pthread_cond_timedwait(&w->cond, &w->mtx, &ts); + } + uint8_t *r = NULL; + if (w->ready && w->val) { + r = w->val; + *ol = w->val_len; + w->val = NULL; + } + w->ready = false; + pthread_mutex_unlock(&w->mtx); + return r; +} diff --git a/kvspace_shm.h b/kvspace_shm.h new file mode 100644 index 0000000..68f7884 --- /dev/null +++ b/kvspace_shm.h @@ -0,0 +1,166 @@ +/* + * kvspace.h — KVSpace C API + * + * file-backed mmap, ART 树索引 + slotsboxmalloc 变长存储. + * 对齐 kvspace-go KVSpace 接口. + */ + +#ifndef KVSPACE_H +#define KVSPACE_H + +#include "coord.h" +#include +#include +#include + +typedef struct kvspace kvspace_t; + +/* shm store 格式版本:4 = ART val 原子字 + 垃圾环 + 单写者降级 + 8B 节点对齐。 + * 不兼容旧版本:open 硬失败并提示重建(不迁移)。 */ +#define KVSPACE_FORMAT_VERSION 5 + +/* 对齐 kvspace/include/kvspace/kvspace.h:叶子 + 目录祖先。 */ +typedef struct { + uint32_t block_id; + uint32_t gen; + uint32_t parent_id; + uint32_t depth; +} kvspaceRef_t; + +/* 与 kvspace/include/kvspace/kvspace.h 的 kvspaceXvalueBodyView_t 逐字段一致。 */ +typedef struct { + uint8_t *head; + uint32_t headlen; + uint8_t *body; + uint64_t body_cap; + uint64_t cap; +} kvspaceXvalueBodyView_t; + +/* ================================================================ + * 生命周期 + * ================================================================ */ + +// 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); + +/* ================================================================ + * 单点读写 + * ================================================================ */ + +// Get: resolve=1 穿透 link,resolve=0 返回 link 本身。 +uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, + int32_t *out_len); +int kvspaceShmResolveRef(kvspace_t *kv, const char *key, kvspaceRef_t *ref); +uint8_t *kvspaceShmGetByRef(kvspace_t *kv, kvspaceRef_t *ref, + const char *key_fallback, int32_t *out_len); +/* shm slot view:返回映射内 head/body 与整 box 物理容量。0=成功,-1=不可视槽。 */ +int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, + const char *key_fallback, kvspaceXvalueBodyView_t *out); +/* 定位写的前置条件:只允许改 body [8, 8+len),head 与 attr 一律不可写。 */ +int kvspaceShmPartInBody(const uint8_t *raw, int32_t total, uint32_t offset, + uint32_t buf_len); +int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, + const char *key_fallback, uint32_t offset, + const uint8_t *buf, uint32_t buf_len); + +// Set: 写入 value(XValue 字节)。总是穿透 link 写入 target。 +int kvspaceShmSet(kvspace_t *kv, const char *key, const uint8_t *val, + int32_t val_len); +int kvspaceShmSetValue(kvspace_t *kv, const char *key, const uint8_t *val, + int32_t val_len); +/* 整值写 by cached ref:ref 失效时退回 key 的完整 SetValue 并重解析。 */ +int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, + const char *key_fallback, const uint8_t *val, + int32_t val_len); + +// 零拷贝写原语——返回 SHM 常驻 body 偏移指针供调用方直接写;写即持久,无收尾。 +// WriteInPlace: key 必须已存在、body_len 必须等于原 body_len(同 kind +// 覆写);否则返回非 0。 +int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, + int32_t body_len, uint8_t **body); +// WriteNewPlace: 按 (ref, storetype, body_len, langtype) 分配新 box、写固定头 + +// langtype 尾部(整体单写)、维护父索引,返回 body 指针。 +int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, + uint8_t storetype, int32_t body_len, + uint64_t body_cap, const char *langtype, + uint8_t **body); +// ListLen: 只返回前缀下子项计数,无缓冲。 +int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool expand_ext, + int resolve, int32_t *out_count); + +/* ================================================================ + * 目录操作 + * ================================================================ */ + +// List: resolve=1 穿透 link 列出 target 子节点。 +int kvspaceShmList(kvspace_t *kv, const char *prefix, bool expand_ext, + int resolve, char ***out_names, int32_t *out_count); + +int kvspaceShmDel(kvspace_t *kv, const char *key); +int kvspaceShmDeltree(kvspace_t *kv, const char *prefix); +int kvspaceShmCp(kvspace_t *kv, const char *src, + const char *dst); // 单 key 拷贝 +int kvspaceShmCptree(kvspace_t *kv, const char *src, + const char *dst); // 递归子树拷贝 +int kvspaceShmCplist(kvspace_t *kv, const char *src, + const char *dst); // 浅拷贝:base 值 + 一层 · 成员,不递归 +int kvspaceShmMkindex( + kvspace_t *kv, const char *path, + uint32_t capacity); // 递归创建目录,capacity 预留叶 index 容量(减少扩容) + +/* ================================================================ + * ExtIndex + * ================================================================ */ + +int kvspaceShmExtindex(kvspace_t *kv, const char *path, const char *extpath, + char *err, uint32_t err_cap); +int kvspaceShmDelextindex(kvspace_t *kv, const char *path); // 移除 extindex + +/* ================================================================ + * Watch / Notify + * ================================================================ */ + +int kvspaceShmNotify(kvspace_t *kv, const char *key, const uint8_t *val, + int32_t val_len); + +// 阻塞等待通知,timeout_ms 毫秒。返回 malloc XValue,超时返回 NULL. +uint8_t *kvspaceShmWatch(kvspace_t *kv, const char *key, int32_t timeout_ms, + int32_t *out_len); + +#ifndef KVSPACE_VIEW_TOKEN_DEFINED +#define KVSPACE_VIEW_TOKEN_DEFINED +typedef struct { + uint8_t *head; uint8_t *body; uint32_t headlen; uint32_t value_len; + uint64_t cap; uint64_t body_cap; + uint32_t struct_gen; uint32_t value_gen; + uint64_t store_id; uint32_t node_id; uint32_t _pad; +} kvspaceViewToken_t; +#endif +int kvspaceShmResolvePrefix(kvspace_t *kv, const char *path, kvspaceRef_t *ref); +int kvspaceShmViewResolve(kvspace_t *kv, const char *key, kvspaceViewToken_t *tok); +int kvspaceShmViewFromRef(kvspace_t *kv, const kvspaceRef_t *ref, + const char *key_fallback, kvspaceViewToken_t *tok); +int kvspaceShmViewResolveFrom(kvspace_t *kv, const kvspaceRef_t *parent, + const char *rel, kvspaceViewToken_t *tok); +int kvspaceShmViewCheck(kvspace_t *kv, const kvspaceViewToken_t *tok); +int kvspaceShmViewWrite(kvspace_t *kv, kvspaceViewToken_t *tok, + const uint8_t *data, uint32_t len); +int kvspaceShmViewDel(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key); +int kvspaceShmViewRefresh(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key); +int kvspaceShmViewAck(kvspace_t *kv, uint32_t *epoch); +int kvspaceShmViewQuiesce(kvspace_t *kv, int on); +#ifdef P2_TEST_HOOKS +uint64_t kvspaceShmViewGarbBytes(kvspace_t *kv); +void kvspaceShmViewRoundStats(kvspace_t *kv, uint64_t *ok, uint64_t *aborted, + uint64_t *leaks); +/* 对齐/格式验收(仅测试构建):返回 -1=通过,>=0=首个坏块 id,-2=块头宽度 + * 不是 8;SetFormatVersion 用于构造"旧 store 必须拒绝"的用例。 */ +int64_t kvspaceShmArtAlignCheck(kvspace_t *kv); +void kvspaceShmTestSetFormatVersion(kvspace_t *kv, uint32_t v); +#endif +int kvspaceShmViewSweep(kvspace_t *kv); +int kvspaceShmViewFlushTomb(kvspace_t *kv); + +#endif diff --git a/py/kvspace/__init__.py b/py/kvspace/__init__.py index ea741d2..86dc955 100644 --- a/py/kvspace/__init__.py +++ b/py/kvspace/__init__.py @@ -28,29 +28,31 @@ def _bind(fn, argtypes, restype): _bind(_lib.kvspaceShmSet, [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int32], ctypes.c_int) _bind(_lib.kvspaceShmDel, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) _bind(_lib.kvspaceShmDeltree, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) -_bind(_lib.kvspaceShmMkindex, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) +_bind(_lib.kvspaceShmMkindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_uint32], ctypes.c_int) _bind(_lib.kvspaceShmList, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_bool, ctypes.c_int, ctypes.POINTER(ctypes.c_void_p), ctypes.POINTER(ctypes.c_int32)], ctypes.c_int) -_bind(_lib.kvspaceShmExtindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p], ctypes.c_int) +_bind(_lib.kvspaceShmExtindex, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_char_p, + ctypes.c_char_p, ctypes.c_uint32], ctypes.c_int) _bind(_lib.kvspaceShmDelextindex, [ctypes.c_void_p, ctypes.c_char_p], ctypes.c_int) _bind(_lib.kvspaceShmNotify, [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_uint8), ctypes.c_int32], ctypes.c_int) _bind(_lib.kvspaceShmWatch, [ctypes.c_void_p, ctypes.c_char_p, ctypes.c_int32, ctypes.POINTER(ctypes.c_int32)], ctypes.POINTER(ctypes.c_uint8)) # ── 权威 head 构造器绑定(kvspace ABI,无 handle)────────────────── -# #566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -# struct + body——构造走 C 构造器 kvspaceXvalueHeadNew*,读取本地 memcpy 固定头 + 字段访问。 +# #566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at +# 8+len ([attr_len:u16][records], record = [tag:u8][len:u16][data], tag 1 = langtype). +# Constructed via C kvspaceXvalueHeadNew*; parsed locally on read. _U8P = ctypes.POINTER(ctypes.c_uint8) _U8PP = ctypes.POINTER(_U8P) _U64P = ctypes.POINTER(ctypes.c_uint64) _bind(_lib.kvspaceXvalueHeadNewScalar, - [ctypes.c_uint8, _U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int) + [ctypes.c_uint8, _U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) _bind(_lib.kvspaceXvalueHeadNewSlack, - [_U8P, ctypes.c_uint64, ctypes.c_uint64, _U8PP, _U64P], ctypes.c_int) + [_U8P, ctypes.c_uint64, ctypes.c_uint64, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) _bind(_lib.kvspaceXvalueHeadNewPtr, - [_U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int) + [_U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) _bind(_lib.kvspaceXvalueHeadNewExt, - [_U8P, ctypes.c_uint32, _U8PP, _U64P], ctypes.c_int) + [_U8P, ctypes.c_uint32, ctypes.c_char_p, _U8PP, _U64P], ctypes.c_int) ST_SHORT, ST_SLACK, ST_LARGE, ST_EXT = 0, 1, 2, 3 PTR_INLINE, PTR_PTR, PTR_EXT = 0, 1, 2 @@ -61,8 +63,11 @@ def _bind(fn, argtypes, restype): # ── XValue helpers ──────────────────────────────────────────────── -# 构造走 C 构造器(输出 frontend malloc,_take 拷出后 free);读取本地 -# memcpy 固定头 + 字段访问。 +# Construct via C constructors (_take copies and frees malloc buffer); parse header + attr locally. +# Empty langtype = untyped (empty attr). + +ATTR_TAG_LANGTYPE = 1 + def _take(out, total: ctypes.c_uint64) -> bytes: data = ctypes.string_at(out, total.value) @@ -74,61 +79,95 @@ def _new(fn, *args) -> bytes: out = _U8P() total = ctypes.c_uint64() if fn(*args, ctypes.byref(out), ctypes.byref(total)): - raise ValueError(f"{fn.__name__} 构造失败") + raise ValueError(f"{fn.__name__} failed") return _take(out, total) -def _xv_scalar(raw: bytes) -> bytes: - buf = (ctypes.c_uint8 * len(raw)).from_buffer_copy(raw) if raw else None - return _new(_lib.kvspaceXvalueHeadNewScalar, ST_SHORT, - ctypes.cast(buf, _U8P) if buf is not None else None, len(raw)) +def _u8buf(raw: bytes): + if not raw: + return None + return ctypes.cast((ctypes.c_uint8 * len(raw)).from_buffer_copy(raw), _U8P) -def _xv_slack(raw: bytes) -> bytes: - buf = (ctypes.c_uint8 * len(raw)).from_buffer_copy(raw) if raw else None - return _new(_lib.kvspaceXvalueHeadNewSlack, - ctypes.cast(buf, _U8P) if buf is not None else None, len(raw), len(raw)) +def _xv_scalar(raw: bytes, langtype: str) -> bytes: + return _new(_lib.kvspaceXvalueHeadNewScalar, ST_SHORT, _u8buf(raw), len(raw), + langtype.encode()) -def _xv_from_wire(data: Optional[bytes]) -> tuple[int, int, bytes]: - """读固定头 → (ref, storetype, body)。langtype 不在 head,kind 无从解析。""" - if not data or len(data) < HEAD_LEN: - return (0, 0, b"") +def _xv_slack(raw: bytes, langtype: str) -> bytes: + return _new(_lib.kvspaceXvalueHeadNewSlack, _u8buf(raw), len(raw), len(raw), + langtype.encode()) + + +def _split_wire(data: Optional[bytes]): + """Validate and split wire into (storetype, ptr, body, attr); returns None if malformed.""" + if not data or len(data) < HEAD_LEN + 2: + return None body_len = int.from_bytes(data[:6], "little") storetype, ptr = data[6], data[7] - if storetype > ST_EXT or ptr > PTR_EXT or HEAD_LEN + body_len != len(data): + if storetype > ST_EXT or ptr > PTR_EXT or HEAD_LEN + body_len + 2 > len(data): + return None + a = HEAD_LEN + body_len + attr_len = int.from_bytes(data[a:a + 2], "little") + if attr_len < 2 or a + attr_len != len(data): + return None + return storetype, ptr, data[HEAD_LEN:a], data[a:a + attr_len] + + +def _xv_from_wire(data: Optional[bytes]) -> tuple[int, int, bytes]: + """Read fixed header -> (ref, storetype, body) excluding attr; returns (0, 0, b"") if invalid.""" + parts = _split_wire(data) + if parts is None: return (0, 0, b"") + storetype, ptr, body, _ = parts ref = REF_PTR if ptr == PTR_PTR else REF_EXT if ptr == PTR_EXT else REF_INLINE - return (ref, storetype, data[HEAD_LEN:]) + return (ref, storetype, body) + + +def xv_langtype(data: Optional[bytes]) -> str: + """Read attr tag=1 (langtype) record; returns "" if untyped or invalid.""" + parts = _split_wire(data) + if parts is None: + return "" + attr = parts[3] + p = 2 + while p + 3 <= len(attr): + tag, rlen = attr[p], int.from_bytes(attr[p + 1:p + 3], "little") + if p + 3 + rlen > len(attr): + return "" + if tag == ATTR_TAG_LANGTYPE: + return attr[p + 3:p + 3 + rlen].decode() + p += 3 + rlen + return "" -def xv_int(v: int) -> bytes: - return _xv_scalar(struct.pack(" bytes: + return _xv_scalar(struct.pack(" bytes: - return _xv_scalar(struct.pack(" bytes: + return _xv_scalar(struct.pack(" bytes: - return _xv_slack(s.encode()) +def xv_str(s: str, langtype: str = "") -> bytes: + return _xv_slack(s.encode(), langtype) def xv_index() -> bytes: - """目录值(空 body 的 SHORT 头)。""" - return _xv_scalar(b"") + """Directory value (empty-body SHORT header, untyped).""" + return _xv_scalar(b"", "") -def xv_link(target: str) -> bytes: - """指针值(ptr 位):body=目标 key。langtype 由调用方写 x‥langtype attr key。""" - buf = (ctypes.c_uint8 * len(target)).from_buffer_copy(target.encode()) - return _new(_lib.kvspaceXvalueHeadNewPtr, buf, len(target)) +def xv_link(target: str, langtype: str = "") -> bytes: + """Pointer value (ptr=PTR): body=target key, langtype=target constraint ("" = unconstrained).""" + t = target.encode() + return _new(_lib.kvspaceXvalueHeadNewPtr, _u8buf(t), len(t), langtype.encode()) -def xv_ext(locator: str) -> bytes: - """外部定位符(EXT class):body=locator。langtype 由调用方写 x‥langtype attr key。""" - buf = (ctypes.c_uint8 * len(locator)).from_buffer_copy(locator.encode()) - return _new(_lib.kvspaceXvalueHeadNewExt, buf, len(locator)) +def xv_ext(locator: str, langtype: str = "") -> bytes: + """External locator (EXT class): body=locator.""" + loc = locator.encode() + return _new(_lib.kvspaceXvalueHeadNewExt, _u8buf(loc), len(loc), langtype.encode()) # ── KVSpace ───────────────────────────────────────────────────── @@ -177,22 +216,29 @@ def deltree(self, prefix: str): # ── Directory ──────────────────────────────────────────── - def mkindex(self, path: str): - _lib.kvspaceShmMkindex(self._kv, path.encode()) + def mkindex(self, path: str, capacity: int = 0): + _lib.kvspaceShmMkindex(self._kv, path.encode(), capacity) def list(self, prefix: str, resolve: bool = True) -> list[str]: out = ctypes.c_void_p() oc = ctypes.c_int32(0) _lib.kvspaceShmList(self._kv, prefix.encode(), False, 1 if resolve else 0, ctypes.byref(out), ctypes.byref(oc)) - if oc.value == 0: + if not out.value: return [] - ptrs = ctypes.cast(out, ctypes.POINTER(ctypes.c_char_p)) - return [ptrs[i].decode() for i in range(oc.value)] + # Copy and free the C-allocated name array. + ptrs = ctypes.cast(out, ctypes.POINTER(ctypes.c_void_p)) + names = [ctypes.string_at(ptrs[i]).decode() for i in range(oc.value)] + for i in range(oc.value): + _libc.free(ptrs[i]) + _libc.free(out) + return names # ── Link / ExtIndex ────────────────────────────────────── def extindex(self, path: str, extpath: str): - _lib.kvspaceShmExtindex(self._kv, path.encode(), extpath.encode()) + err = ctypes.create_string_buffer(256) + if _lib.kvspaceShmExtindex(self._kv, path.encode(), extpath.encode(), err, len(err)): + raise RuntimeError(err.value.decode() or f"extindex({path}, {extpath}) failed") def delextindex(self, path: str): _lib.kvspaceShmDelextindex(self._kv, path.encode()) diff --git a/py/tests/test_wire.py b/py/tests/test_wire.py index ade041c..000dbcb 100644 --- a/py/tests/test_wire.py +++ b/py/tests/test_wire.py @@ -1,48 +1,49 @@ #!/usr/bin/env python3 -"""wrapper ↔ C 构造器双向对拍 + ShmSet 读回。 - -#566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -struct + body——C 侧权威是 kvspaceXvalueHeadNew* 构造器;wrapper 构造走同一构造器, -读取本地 memcpy 固定头 + 字段访问。断言 ref/storetype/body。 +"""wrapper <-> C constructor round-trip + ShmSet readback. +#566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at 8+len +([attr_len:u16][records], tag 1 = langtype), total = 8 + len + attr_len. +Constructed via kvspaceXvalueHeadNew*; parsed locally on read. """ import ctypes, os, struct, sys, tempfile from pathlib import Path - sys.path.insert(0, str(Path(__file__).resolve().parents[1])) import kvspace from kvspace import (xv_int, xv_float, xv_str, xv_index, xv_link, xv_ext, - _xv_from_wire, KVSpace, ST_SHORT, ST_SLACK, ST_EXT, + xv_langtype, _xv_from_wire, KVSpace, ST_SHORT, ST_SLACK, ST_EXT, PTR_INLINE, PTR_PTR, PTR_EXT, HEAD_LEN) - _lib = ctypes.CDLL(kvspace._SO) _libc = ctypes.CDLL(None) _libc.free.argtypes = [ctypes.c_void_p] - _U8P = ctypes.POINTER(ctypes.c_uint8) _U8PP = ctypes.POINTER(_U8P) _U64P = ctypes.POINTER(ctypes.c_uint64) -_lib.kvspaceXvalueHeadNewScalar.argtypes = [ctypes.c_uint8, _U8P, ctypes.c_uint32, _U8PP, _U64P] +_CS = ctypes.c_char_p +_lib.kvspaceXvalueHeadNewScalar.argtypes = [ctypes.c_uint8, _U8P, ctypes.c_uint32, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewScalar.restype = ctypes.c_int -_lib.kvspaceXvalueHeadNewSlack.argtypes = [_U8P, ctypes.c_uint64, ctypes.c_uint64, _U8PP, _U64P] +_lib.kvspaceXvalueHeadNewSlack.argtypes = [_U8P, ctypes.c_uint64, ctypes.c_uint64, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewSlack.restype = ctypes.c_int -_lib.kvspaceXvalueHeadNewPtr.argtypes = [_U8P, ctypes.c_uint32, _U8PP, _U64P] +_lib.kvspaceXvalueHeadNewPtr.argtypes = [_U8P, ctypes.c_uint32, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewPtr.restype = ctypes.c_int -_lib.kvspaceXvalueHeadNewExt.argtypes = [_U8P, ctypes.c_uint32, _U8PP, _U64P] +_lib.kvspaceXvalueHeadNewExt.argtypes = [_U8P, ctypes.c_uint32, _CS, _U8PP, _U64P] _lib.kvspaceXvalueHeadNewExt.restype = ctypes.c_int -def c_new(storetype, ptr, body): - """C 侧构造(独立 oracle):(storetype, ptr, body) → 固定头 + body。""" +def c_new(storetype, ptr, body, langtype=""): + """C constructor oracle: (storetype, ptr, body, langtype) -> full wire.""" buf = (ctypes.c_uint8 * len(body)).from_buffer_copy(body) if body else None rp = ctypes.cast(buf, _U8P) if buf is not None else None + lt = langtype.encode() out = _U8P() total = ctypes.c_uint64() if ptr == PTR_PTR: - rc = _lib.kvspaceXvalueHeadNewPtr(rp, len(body), ctypes.byref(out), ctypes.byref(total)) + rc = _lib.kvspaceXvalueHeadNewPtr(rp, len(body), lt, ctypes.byref(out), ctypes.byref(total)) elif ptr == PTR_EXT: - rc = _lib.kvspaceXvalueHeadNewExt(rp, len(body), ctypes.byref(out), ctypes.byref(total)) + rc = _lib.kvspaceXvalueHeadNewExt(rp, len(body), lt, ctypes.byref(out), ctypes.byref(total)) + elif storetype == ST_SLACK: + rc = _lib.kvspaceXvalueHeadNewSlack(rp, len(body), len(body), lt, + ctypes.byref(out), ctypes.byref(total)) else: - rc = _lib.kvspaceXvalueHeadNewScalar(storetype, rp, len(body), + rc = _lib.kvspaceXvalueHeadNewScalar(storetype, rp, len(body), lt, ctypes.byref(out), ctypes.byref(total)) assert rc == 0, f"c_new(st={storetype}, ptr={ptr}) rc={rc}" data = ctypes.string_at(out, total.value) @@ -51,13 +52,20 @@ def c_new(storetype, ptr, body): def c_peek(data): - """本地解析固定头 → (ref, storetype, body)。""" - assert len(data) >= HEAD_LEN, "短于 8 字节头" + """Independent wire parser -> (ref, storetype, body, langtype) with total length check.""" + assert len(data) >= HEAD_LEN + 2, "shorter than 8-byte header + empty attr" body_len = int.from_bytes(data[:6], "little") storetype, ptr = data[6], data[7] - assert HEAD_LEN + body_len == len(data), f"总长不匹配 {len(data)}" + a = HEAD_LEN + body_len + attr_len = int.from_bytes(data[a:a + 2], "little") + assert a + attr_len == len(data), f"total length mismatch {len(data)} != {a}+{attr_len}" + lt = "" + if attr_len > 2: + assert data[a + 2] == 1, "first attr record must be langtype" + rlen = int.from_bytes(data[a + 3:a + 5], "little") + lt = data[a + 5:a + 5 + rlen].decode() ref = 1 if ptr == PTR_PTR else 2 if ptr == PTR_EXT else 0 - return (ref, storetype, data[HEAD_LEN:]) + return (ref, storetype, data[HEAD_LEN:a], lt) _fails = 0 @@ -75,71 +83,49 @@ def check(name, got, want): def main(): - print("── wrapper 构造 → C 头解析 ──") - r, st, body = c_peek(xv_int(42)) - check("int64.ref", r, 0) - check("int64.storetype", st, ST_SHORT) - check("int64.body", body, struct.pack(" independent parse --") + check("int64", c_peek(xv_int(42, "int64")), (0, ST_SHORT, struct.pack(" wrapper read --") + v = c_new(ST_SHORT, PTR_INLINE, struct.pack(" i32; - // Head 构造(#566:head 固定 8 字节 [len:u48][storetype][ptr],直接 memcpy - // struct + body——构造器输出 frontend malloc,由调用方 free)。 + // Header constructors (#566/#571: fixed 8-byte header + body + attr region; + // empty string = untyped; malloc-allocated output freed by caller). pub fn kvspaceXvalueHeadNewScalar( storetype: u8, raw: *const u8, raw_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; @@ -64,18 +67,21 @@ mod ffi { data: *const u8, data_len: u64, cap: u64, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; pub fn kvspaceXvalueHeadNewPtr( path: *const u8, path_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; pub fn kvspaceXvalueHeadNewExt( locator: *const u8, locator_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; @@ -86,9 +92,9 @@ mod ffi { // ── xvalue helpers ───────────────────────────────────────────── // -// #566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -// struct + body——构造走 C 构造器 kvspaceXvalueHeadNew*,读取本地 memcpy 固定头 + 字段访问。 -// langtype 不在 head(x‥langtype attr key),固定头读取只给存储元数据。 +// #566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at 8+len +// ([attr_len:u16][records], tag 1 = langtype), total = 8 + len + attr_len. Constructed via C +// kvspaceXvalueHeadNew*; helpers here write untyped values (empty attr). pub mod xvalue { use super::ffi; @@ -101,8 +107,10 @@ pub mod xvalue { pub const PTR_PTR: u8 = 1; pub const PTR_EXT: u8 = 2; pub const HEAD_LEN: usize = 8; + /// Empty langtype (untyped, empty attr). + const NO_TYPE: *const c_char = b"\0".as_ptr() as *const c_char; - /// 取 C 构造器 malloc 输出(to_vec 后 free)。 + /// Copy and free malloc buffer returned by C constructor. fn take(out: *mut u8, total: u64) -> Vec { let bytes = unsafe { std::slice::from_raw_parts(out, total as usize).to_vec() }; unsafe { ffi::free(out as *mut std::ffi::c_void) }; @@ -114,7 +122,7 @@ pub mod xvalue { let mut total: u64 = 0; let raw = v.to_le_bytes(); let rc = unsafe { - ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); @@ -126,7 +134,7 @@ pub mod xvalue { let mut total: u64 = 0; let raw = v.to_le_bytes(); let rc = unsafe { - ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, raw.as_ptr(), 8, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); @@ -141,6 +149,7 @@ pub mod xvalue { s.as_ptr(), s.len() as u64, s.len() as u64, + NO_TYPE, &mut out, &mut total, ) @@ -150,31 +159,31 @@ pub mod xvalue { } take(out, total) } - /// 目录值(空 body 的 SHORT 头)。 + /// Directory value (empty-body SHORT header). pub fn index() -> Vec { let mut out: *mut u8 = ptr::null_mut(); let mut total: u64 = 0; let rc = unsafe { - ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, ptr::null(), 0, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewScalar(ST_SHORT, ptr::null(), 0, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); } take(out, total) } - /// 指针值(ptr 位):body = 目标 key。langtype 由调用方写 x‥langtype attr key。 + /// Pointer value (ptr=PTR): body = target key. pub fn link(target: &str) -> Vec { let mut out: *mut u8 = ptr::null_mut(); let mut total: u64 = 0; let rc = unsafe { - ffi::kvspaceXvalueHeadNewPtr(target.as_ptr(), target.len() as u32, &mut out, &mut total) + ffi::kvspaceXvalueHeadNewPtr(target.as_ptr(), target.len() as u32, NO_TYPE, &mut out, &mut total) }; if rc != 0 { return Vec::new(); } take(out, total) } - /// 外部定位符(EXT class):body = locator。langtype 由调用方写 x‥langtype attr key。 + /// External locator (EXT class): body = locator. pub fn ext(locator: &str) -> Vec { let mut out: *mut u8 = ptr::null_mut(); let mut total: u64 = 0; @@ -182,6 +191,7 @@ pub mod xvalue { ffi::kvspaceXvalueHeadNewExt( locator.as_ptr(), locator.len() as u32, + NO_TYPE, &mut out, &mut total, ) @@ -192,20 +202,28 @@ pub mod xvalue { take(out, total) } - /// 读固定头 → (ref, storetype, body)。ref 由 ptr 位推导;body 借用 data。 - /// langtype 不在 head,kind 无从解析(读 x‥langtype attr key 是调用方的事)。 + /// Read fixed header -> (ref, storetype, body) excluding attr; returns (0, 0, &[]) if invalid. pub fn from_wire(data: &[u8]) -> (u8, u8, &[u8]) { - if data.len() < HEAD_LEN { + if data.len() < HEAD_LEN + 2 { return (0, 0, &[]); } - let len6 = &data[..6]; let mut body_len: u64 = 0; - for (i, b) in len6.iter().enumerate() { + for (i, b) in data[..6].iter().enumerate() { body_len |= (*b as u64) << (8 * i); } let storetype = data[6]; let ptr = data[7]; - if storetype > ST_EXT || ptr > PTR_EXT || HEAD_LEN + body_len as usize > data.len() { + let Ok(blen) = usize::try_from(body_len) else { + return (0, 0, &[]); + }; + let Some(a) = HEAD_LEN.checked_add(blen) else { + return (0, 0, &[]); + }; + if storetype > ST_EXT || ptr > PTR_EXT || a + 2 > data.len() { + return (0, 0, &[]); + } + let attr_len = u16::from_le_bytes([data[a], data[a + 1]]) as usize; + if attr_len < 2 || a + attr_len != data.len() { return (0, 0, &[]); } let r#ref = if ptr == PTR_PTR { @@ -215,7 +233,7 @@ pub mod xvalue { } else { 0 }; - (r#ref, storetype, &data[HEAD_LEN..]) + (r#ref, storetype, &data[HEAD_LEN..a]) } } @@ -301,7 +319,7 @@ impl KVSpace { let cp = CString::new(path).unwrap(); let ce = CString::new(extpath).unwrap(); unsafe { - ffi::kvspaceShmExtindex(self.ptr, cp.as_ptr(), ce.as_ptr()); + ffi::kvspaceShmExtindex(self.ptr, cp.as_ptr(), ce.as_ptr(), ptr::null_mut(), 0); } } } diff --git a/rust/tests/wire.rs b/rust/tests/wire.rs index ad389d5..a51f571 100644 --- a/rust/tests/wire.rs +++ b/rust/tests/wire.rs @@ -1,69 +1,94 @@ -//! Wrapper ↔ C 构造器双向对拍。 +//! Wrapper <-> C constructor round-trip tests. //! -//! #566:head 固定 8 字节 [len:u48][storetype][ptr],body 偏移恒 8。直接 memcpy -//! struct + body——C 侧权威是 kvspaceXvalueHeadNew* 构造器;Rust 侧 xvalue::* 走同一构造器, -//! from_wire 本地 memcpy 固定头 + 字段访问。断言 storetype/ref(ptr)/body 一致。 - +//! #566/#571: Fixed 8-byte header [len:u48][storetype][ptr], body at offset 8, attr at 8+len +//! ([attr_len:u16][records], tag 1 = langtype), total = 8 + len + attr_len. Constructed via C +//! kvspaceXvalueHeadNew*; parsed locally via xvalue::from_wire. use kvspace_c::xvalue; - +use std::os::raw::c_char; extern "C" { fn kvspaceXvalueHeadNewScalar( storetype: u8, raw: *const u8, raw_len: u32, + langtype: *const c_char, + out: *mut *mut u8, + out_len: *mut u64, + ) -> i32; + fn kvspaceXvalueHeadNewSlack( + data: *const u8, + data_len: u64, + cap: u64, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; fn kvspaceXvalueHeadNewPtr( path: *const u8, path_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; fn kvspaceXvalueHeadNewExt( locator: *const u8, locator_len: u32, + langtype: *const c_char, out: *mut *mut u8, out_len: *mut u64, ) -> i32; fn free(p: *mut std::ffi::c_void); } - -/// C 侧构造(独立 oracle):(storetype, ptr, body) → 8 字节固定头 + body。 -fn c_new(storetype: u8, ptr: u8, body: &[u8]) -> Vec { +/// C constructor oracle: (storetype, ptr, body, langtype) -> full wire. +fn c_new(storetype: u8, ptr: u8, body: &[u8], langtype: &str) -> Vec { + let lt = std::ffi::CString::new(langtype).unwrap(); let mut out: *mut u8 = std::ptr::null_mut(); let mut total: u64 = 0; + let bp = if body.is_empty() { std::ptr::null() } else { body.as_ptr() }; let rc = match ptr { - xvalue::PTR_INLINE if body.is_empty() => unsafe { - kvspaceXvalueHeadNewScalar(storetype, std::ptr::null(), 0, &mut out, &mut total) + xvalue::PTR_INLINE if storetype == xvalue::ST_SLACK => unsafe { + kvspaceXvalueHeadNewSlack( + bp, + body.len() as u64, + body.len() as u64, + lt.as_ptr(), + &mut out, + &mut total, + ) }, xvalue::PTR_INLINE => unsafe { - kvspaceXvalueHeadNewScalar(storetype, body.as_ptr(), body.len() as u32, &mut out, &mut total) + kvspaceXvalueHeadNewScalar(storetype, bp, body.len() as u32, lt.as_ptr(), &mut out, &mut total) }, xvalue::PTR_PTR => unsafe { - kvspaceXvalueHeadNewPtr(body.as_ptr(), body.len() as u32, &mut out, &mut total) + kvspaceXvalueHeadNewPtr(bp, body.len() as u32, lt.as_ptr(), &mut out, &mut total) }, xvalue::PTR_EXT => unsafe { - kvspaceXvalueHeadNewExt(body.as_ptr(), body.len() as u32, &mut out, &mut total) + kvspaceXvalueHeadNewExt(bp, body.len() as u32, lt.as_ptr(), &mut out, &mut total) }, _ => panic!("bad ptr {ptr}"), }; - assert_eq!(rc, 0, "c_new(st={storetype}, ptr={ptr}) 失败"); + assert_eq!(rc, 0, "c_new(st={storetype}, ptr={ptr}) failed"); let v = unsafe { std::slice::from_raw_parts(out, total as usize).to_vec() }; unsafe { free(out as *mut std::ffi::c_void) }; v } - -/// 本地解析固定头 → (ref, storetype, body)。 -fn c_peek(data: &[u8]) -> (u8, u8, &[u8]) { - assert!(data.len() >= xvalue::HEAD_LEN, "短于 8 字节头"); +/// Independent wire parser -> (ref, storetype, body, langtype) with total length check. +fn c_peek(data: &[u8]) -> (u8, u8, Vec, String) { + assert!(data.len() >= xvalue::HEAD_LEN + 2, "shorter than 8-byte header + empty attr"); let mut body_len: u64 = 0; for (i, b) in data[..6].iter().enumerate() { body_len |= (*b as u64) << (8 * i); } let storetype = data[6]; let ptr = data[7]; - assert_eq!(xvalue::HEAD_LEN + body_len as usize, data.len(), "总长不匹配"); + let a = xvalue::HEAD_LEN + body_len as usize; + let attr_len = u16::from_le_bytes([data[a], data[a + 1]]) as usize; + assert_eq!(a + attr_len, data.len(), "total length mismatch"); + let mut lt = String::new(); + if attr_len > 2 { + assert_eq!(data[a + 2], 1, "first attr record must be langtype"); + let rlen = u16::from_le_bytes([data[a + 3], data[a + 4]]) as usize; + lt = String::from_utf8(data[a + 5..a + 5 + rlen].to_vec()).unwrap(); + } let r#ref = if ptr == xvalue::PTR_PTR { 1 } else if ptr == xvalue::PTR_EXT { @@ -71,9 +96,8 @@ fn c_peek(data: &[u8]) -> (u8, u8, &[u8]) { } else { 0 }; - (r#ref, storetype, &data[xvalue::HEAD_LEN..]) + (r#ref, storetype, data[xvalue::HEAD_LEN..a].to_vec(), lt) } - struct Checker { failures: usize, } @@ -87,88 +111,53 @@ impl Checker { } } } - #[test] fn wrapper_reads_c_peeks() { let mut c = Checker { failures: 0 }; - - let raw = 42i64.to_le_bytes(); - let enc = xvalue::int64(42); - let (r, st, body) = c_peek(&enc); - c.eq("int64.ref", r, 0); - c.eq("int64.storetype", st, 0); - c.eq("int64.body", body.to_vec(), raw.to_vec()); - - let enc = xvalue::float64(1.5); - let (r, st, body) = c_peek(&enc); - c.eq("float64.ref", r, 0); - c.eq("float64.storetype", st, 0); - c.eq("float64.body", body.to_vec(), 1.5f64.to_le_bytes().to_vec()); - + let none = String::new(); + c.eq("int64", c_peek(&xvalue::int64(42)), (0, 0, 42i64.to_le_bytes().to_vec(), none.clone())); + c.eq( + "float64", + c_peek(&xvalue::float64(1.5)), + (0, 0, 1.5f64.to_le_bytes().to_vec(), none.clone()), + ); let s = "héllo"; // 5 chars, 6 bytes - let enc = xvalue::string(s); - let (r, st, body) = c_peek(&enc); - c.eq("str.ref", r, 0); - c.eq("str.storetype", st, 1); // slack - c.eq("str.body", body.to_vec(), s.as_bytes().to_vec()); - - let enc = xvalue::index(); - let (r, st, body) = c_peek(&enc); - c.eq("index.ref", r, 0); - c.eq("index.storetype", st, 0); - c.eq("index.body", body.to_vec(), Vec::::new()); - - let enc = xvalue::link("/t/x"); - let (r, st, body) = c_peek(&enc); - c.eq("link.ref", r, 1); // ptr - c.eq("link.storetype", st, 1); // slack|ptr - c.eq("link.body", body.to_vec(), b"/t/x".to_vec()); - - let enc = xvalue::ext("s3://b/k"); - let (r, st, body) = c_peek(&enc); - c.eq("ext.ref", r, 2); // ext - c.eq("ext.storetype", st, 3); - c.eq("ext.body", body.to_vec(), b"s3://b/k".to_vec()); - - assert_eq!(c.failures, 0, "{} 项失败", c.failures); + c.eq("str", c_peek(&xvalue::string(s)), (0, 1, s.as_bytes().to_vec(), none.clone())); + c.eq("index", c_peek(&xvalue::index()), (0, 0, Vec::new(), none.clone())); + c.eq("link", c_peek(&xvalue::link("/t/x")), (1, 1, b"/t/x".to_vec(), none.clone())); + c.eq("ext", c_peek(&xvalue::ext("s3://b/k")), (2, 3, b"s3://b/k".to_vec(), none)); + assert_eq!(c.failures, 0, "{} failures", c.failures); } - #[test] fn c_new_wrapper_reads() { let mut c = Checker { failures: 0 }; - let raw = 7i64.to_le_bytes(); - let enc = c_new(xvalue::ST_SHORT, xvalue::PTR_INLINE, &raw); - let (r, st, body) = xvalue::from_wire(&enc); - c.eq("int64.ref", r, 0); - c.eq("int64.storetype", st, 0); - c.eq("int64.body", body.to_vec(), raw.to_vec()); - - let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_INLINE, "héllo".as_bytes()); - let (r, st, body) = xvalue::from_wire(&enc); - c.eq("str.ref", r, 0); - c.eq("str.storetype", st, 1); - c.eq("str.body", body.to_vec(), "héllo".as_bytes().to_vec()); - - // 指针:ptr 位 1。 - let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_PTR, b"/t/x"); - let (r, _, body) = xvalue::from_wire(&enc); - c.eq("ptr.ref", r, 1); - c.eq("ptr.body", body.to_vec(), b"/t/x".to_vec()); - - // 外部定位符:ptr 位 2。 - let enc = c_new(xvalue::ST_EXT, xvalue::PTR_EXT, b"s3://b/k"); - let (r, st, body) = xvalue::from_wire(&enc); - c.eq("ext.ref", r, 2); - c.eq("ext.storetype", st, 3); - c.eq("ext.body", body.to_vec(), b"s3://b/k".to_vec()); - - // 垃圾输入 → 空。 - let garbage = [0u8, 1, 2, 3]; - let (r, st, body) = xvalue::from_wire(&garbage); - c.eq("garbage.ref", r, 0); - c.eq("garbage.storetype", st, 0); - c.eq("garbage.body", body.to_vec(), Vec::::new()); - - assert_eq!(c.failures, 0, "{} 项失败", c.failures); + let enc = c_new(xvalue::ST_SHORT, xvalue::PTR_INLINE, &raw, "int64"); + c.eq("int64", xvalue::from_wire(&enc), (0, 0, &raw[..])); + let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_INLINE, "héllo".as_bytes(), "char/utf8"); + c.eq("str", xvalue::from_wire(&enc), (0, 1, "héllo".as_bytes())); + // Pointer: ptr=1. + let enc = c_new(xvalue::ST_SLACK, xvalue::PTR_PTR, b"/t/x", "int64"); + c.eq("ptr", xvalue::from_wire(&enc), (1, 1, &b"/t/x"[..])); + // External locator: ptr=2. + let enc = c_new(xvalue::ST_EXT, xvalue::PTR_EXT, b"s3://b/k", ""); + c.eq("ext", xvalue::from_wire(&enc), (2, 3, &b"s3://b/k"[..])); + // Malformed / missing attr -> empty. + c.eq("garbage", xvalue::from_wire(&[0u8, 1, 2, 3]), (0, 0, &[][..])); + c.eq("no-attr", xvalue::from_wire(&[1u8, 0, 0, 0, 0, 0, 0, 0, 42]), (0, 0, &[][..])); + assert_eq!(c.failures, 0, "{} failures", c.failures); +} +#[test] +fn shm_roundtrip() { + let dir = std::env::temp_dir().join(format!("kvspace-c-rs-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("db"); + let kv = kvspace_c::KVSpace::open(path.to_str().unwrap(), 32768).unwrap(); + kv.mkindex("/t/", 0); + kv.set("/t/x", &xvalue::int64(42)); + let v = kv.get("/t/x").unwrap(); + assert_eq!(xvalue::from_wire(&v), (0, 0, &[42, 0, 0, 0, 0, 0, 0, 0][..])); + assert_eq!(kv.list("/t/"), vec!["x".to_string()]); + drop(kv); + let _ = std::fs::remove_dir_all(&dir); } diff --git a/src/durable_abi.c b/src/durable_abi.c index 575277f..cda6172 100644 --- a/src/durable_abi.c +++ b/src/durable_abi.c @@ -34,7 +34,7 @@ typedef struct { uint64_t body_cap; } kvspaceHead_t; -/* 固定 8 字节 head → 元数据(langtype 在 body 尾 @8+len,见 kvspaceGetHead)。 */ +/* Decode fixed 8-byte header into metadata (langtype is in attr at 8+len; see kvspaceGetHead). */ static int from_wire_head(const uint8_t *d, uint32_t total, kvspaceHead_t *out) { if (!out || !d || total < KVSPACE_XVALUE_HEAD_LEN) return 1; @@ -56,11 +56,14 @@ static int from_wire_head(const uint8_t *d, uint32_t total, kvspaceHead_t *out) } static int parse_shm_path(const char *dsn, char *out, size_t osz) { + if (!dsn) + return -1; const char *sep = strstr(dsn, "://"); - if (!sep || strncmp(dsn, "shm", (size_t)(sep - dsn)) != 0) + /* Scheme must be exactly "shm" (prefix matching would accept "s://" or "://"). */ + if (!sep || sep - dsn != 3 || strncmp(dsn, "shm", 3) != 0) return -1; - snprintf(out, osz, "%s", sep + 3); - return 0; + int n = snprintf(out, osz, "%s", sep + 3); + return n > 0 && (size_t)n < osz ? 0 : -1; /* reject empty or truncated path */ } void *kvspaceConnect(const char *dsn) { @@ -185,14 +188,12 @@ int kvspaceGetPart(void *h, const char *key, uint32_t offset, uint32_t len, int kvspaceSetPart(void *h, const char *key, uint32_t offset, const uint8_t *buf, uint32_t buf_len, char *err, uint32_t err_cap) { - int32_t total = 0; - uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &total); - if (!d || total < 0 || !kvspaceShmPartInBody(d, total, offset, buf_len)) { + if (kvspaceShmSetPart((kvspace_t *)h, key, offset, buf, buf_len) != 0) { if (err && err_cap) - snprintf(err, err_cap, "kvspace: set-part out of range at %s", key); + snprintf(err, err_cap, "kvspace: set-part out of range at %s", + key ? key : ""); return 1; } - memcpy(d + offset, buf, buf_len); return 0; } @@ -264,85 +265,95 @@ int kvspaceListLen(void *h, const char *prefix, int expand_ext, int resolve, int kvspaceListAt(void *h, const char *prefix, int expand_ext, int resolve, int32_t idx, uint8_t *buf, uint32_t buf_cap, uint32_t *out_len) { - *out_len = 0; - char **names = NULL; - int32_t count = 0; - if (kvspaceShmList((kvspace_t *)h, prefix, expand_ext != 0, resolve, &names, - &count) != 0) - return -1; - int rc = -1; - if (idx >= 0 && idx < count) { - size_t l = strlen(names[idx]); - *out_len = (uint32_t)l; - if (buf && l <= buf_cap) { - memcpy(buf, names[idx], l); - rc = 0; - } - } - for (int32_t i = 0; i < count; i++) - free(names[i]); - free(names); - return rc; + return kvspaceShmListAt((kvspace_t *)h, prefix, expand_ext != 0, resolve, + idx, buf, buf_cap, out_len); } int kvspaceDel(void *h, const char *const *keys, uint32_t nkeys, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - for (uint32_t i = 0; i < nkeys; i++) + if (!h || (!keys && nkeys > 0)) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: invalid arguments to del"); + return 1; + } + for (uint32_t i = 0; i < nkeys; i++) { + if (!keys[i]) { + if (err && err_cap) + snprintf(err, err_cap, "kvspace: null key in del at index %u", i); + return 1; + } + /* Missing keys and >=1024-byte keys are idempotent no-ops in KV del. */ kvspaceShmDel((kvspace_t *)h, keys[i]); + } return 0; } int kvspaceDelTree(void *h, const char *prefix, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmDeltree((kvspace_t *)h, prefix); + int rc = kvspaceShmDeltree((kvspace_t *)h, prefix); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: deltree failed at %s", + prefix ? prefix : "(null)"); + return rc; } int kvspaceCp(void *h, const char *src, const char *dst, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmCp((kvspace_t *)h, src, dst); + int rc = kvspaceShmCp((kvspace_t *)h, src, dst); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: cp failed from %s to %s", + src ? src : "(null)", dst ? dst : "(null)"); + return rc; } int kvspaceCpTree(void *h, const char *src, const char *dst, char *err, - uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmCptree((kvspace_t *)h, src, dst); + uint32_t err_cap) { + int rc = kvspaceShmCptree((kvspace_t *)h, src, dst); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: cptree failed from %s to %s", + src ? src : "(null)", dst ? dst : "(null)"); + return rc; } int kvspaceCpList(void *h, const char *src, const char *dst, char *err, - uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmCplist((kvspace_t *)h, src, dst); + uint32_t err_cap) { + int rc = kvspaceShmCplist((kvspace_t *)h, src, dst); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: cplist failed from %s to %s", + src ? src : "(null)", dst ? dst : "(null)"); + return rc; } int kvspaceMkindex(void *h, const char *path, uint32_t capacity, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmMkindex((kvspace_t *)h, path, capacity); + int rc = kvspaceShmMkindex((kvspace_t *)h, path, capacity); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: mkindex failed at %s", + path ? path : "(null)"); + return rc; } int kvspaceMkindexExt(void *h, const char *path, const char *ext_path, char *err, uint32_t err_cap) { - return kvspaceShmExtindex((kvspace_t *)h, path, ext_path, err, err_cap); + int rc = kvspaceShmExtindex((kvspace_t *)h, path, ext_path, err, err_cap); + if (rc != 0 && err && err_cap && err[0] == '\0') + snprintf(err, err_cap, "kvspace: mkindex-ext failed at %s -> %s", + path ? path : "(null)", ext_path ? ext_path : "(null)"); + return rc; } int kvspaceRmindexExt(void *h, const char *path, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmDelextindex((kvspace_t *)h, path); + int rc = kvspaceShmDelextindex((kvspace_t *)h, path); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: rmindex-ext failed at %s", + path ? path : "(null)"); + return rc; } int kvspaceClear(void *h, char *err, uint32_t err_cap) { - (void)err; - (void)err_cap; - return kvspaceShmDeltree((kvspace_t *)h, "/"); + int rc = kvspaceShmDeltree((kvspace_t *)h, "/"); + if (rc != 0 && err && err_cap) + snprintf(err, err_cap, "kvspace: clear failed"); + return rc; } /* New* 统一收口:New* 构造 head+body(malloc),total 超 u32 上限即失败。 */ @@ -416,6 +427,21 @@ int kvspaceNewFloat64(double v, const char *langtype, return rc == 0 ? finish(value, total, out, out_len) : 1; } +static int wire_is_none(const uint8_t *d, uint32_t len) { + if (!d || len == 0) + return 1; + if (len < KVSPACE_XVALUE_HEAD_LEN) + return 0; + const kvspaceXvalueHead *xh = (const kvspaceXvalueHead *)d; + if (xh->storetype != KVSPACE_XVALUE_ST_SHORT || + xh->ptr != KVSPACE_XVALUE_PTR_INLINE || + kvspaceXvalueHeadLen(xh) != 0) + return 0; + const uint8_t *lt = NULL; + uint16_t ltn = kvspaceXvalueHeadLangtypeBounded(d, len, <); + return ltn == 0 || (ltn == 4 && memcmp(lt, "None", 4) == 0); +} + int kvspaceWatch(void *h, const char *key, const uint8_t *target, uint32_t target_len, uint64_t tick_ns, uint8_t **out, uint32_t *out_len) { @@ -423,13 +449,20 @@ int kvspaceWatch(void *h, const char *key, const uint8_t *target, return 1; *out = NULL; *out_len = 0; + const int target_is_none = wire_is_none(target, target_len); struct timespec t0, tn; clock_gettime(CLOCK_MONOTONIC, &t0); for (;;) { int32_t len = 0; uint8_t *d = kvspaceShmGet((kvspace_t *)h, key, 1, &len); - if (d && (uint32_t)len == target_len && - memcmp(d, target, target_len) == 0) { + if (target_is_none) { + if (!d || len <= 0 || wire_is_none(d, (uint32_t)len)) { + *out = d; + *out_len = (d && len > 0) ? (uint32_t)len : 0; + return 0; + } + } else if (d && (uint32_t)len == target_len && + memcmp(d, target, target_len) == 0) { *out = d; *out_len = (uint32_t)len; return 0; diff --git a/src/kvspace.c b/src/kvspace.c index 011f3c0..aa2dbf4 100644 --- a/src/kvspace.c +++ b/src/kvspace.c @@ -162,6 +162,9 @@ typedef struct { uint64_t freed_bytes; /* 只由 leader 改 */ uint64_t garb_freeing; uint64_t round_ok, round_aborted, leak_boxes; + /* v6: Key-set generation. Incremented (release) whenever a key appears (insert/revive) + * or disappears (delete/tombstone) to invalidate per-handle List caches across processes. */ + uint64_t list_gen; uint32_t claim_seq; /* 30 位发号;耗尽 open 硬失败 */ p2_slot_t slots[P2_SLOT_N]; } kvspace_hdr_t; @@ -233,7 +236,23 @@ struct kvspace { uint8_t *sbo_data; watch_t watches[WATCH_TABLE_SZ]; pthread_mutex_t wlock; + /* Per-handle List cache (see list_cached): ListLen + indexed ListAt scans only once. + * Hits when (prefix, ex, resolve), resolved target, extindex mount, and hdr->list_gen + * all match, producing identical results to a fresh scan. */ + pthread_mutex_t lc_mtx; + char *lc_prefix; + char *lc_target; /* resolved target directory (equals prefix when resolve=0) */ + char *lc_ext; /* extindex mount source when ex=1; "" if unmounted */ + char **lc_names; + int32_t lc_count; + uint8_t lc_ex, lc_res; + uint64_t lc_gen; }; +/* Bump after key-set changes (insert/revive/delete) to invalidate List caches across processes. + * Ptr/@ext value changes do not bump this since list_cached re-checks target and mount each call. */ +static inline void list_gen_bump(kvspace_t *kv) { + __atomic_add_fetch(&kv->hdr->list_gen, 1ull, __ATOMIC_RELEASE); +} /* ---- shm region ---- */ static int region_map(shm_region_t *r, size_t size) { @@ -264,8 +283,6 @@ static int region_attach(shm_region_t *r, bool create, size_t size, return region_map(r, size); } -static void region_close(shm_region_t *r); /* fwd */ - static void region_close(shm_region_t *r) { if (r->base) munmap(r->base, r->reserve); @@ -273,11 +290,8 @@ static void region_close(shm_region_t *r) { close(r->fd); } -/* Pick up growth done by another process (mid-op growth: kvspace#11). */ - /* ── P2: 旁路 gen 表(node id → {struct_gen, value_gen})──────────── */ static void kv_sbo_free(kvspace_t *kv, uint64_t off); -static void p2_setstats_dump(void); static art_hdr_t *art_hdr(kvspace_t *kv, int32_t id); static int32_t art_child(kvspace_t *kv, void *n, uint8_t b); static int pfx_shared(const uint8_t *a, int al, const uint8_t *b, int bl); @@ -333,6 +347,7 @@ static int64_t gen_alloc_epoch(kvspace_t *kv) { static int p2_maint_start(kvspace_t *kv); /* 懒启动(阈值触发) */ static bool p2_claim_alive(kvspace_t *kv, uint32_t claim); static int kv_sync(kvspace_t *kv); +static void list_cache_drop(kvspace_t *kv); /* 阈值:生产固定 64MiB;测试构建可用 P2_TEST_GARB_THRESHOLD_MB 调低。 */ static uint64_t p2_garb_threshold(void) { @@ -430,17 +445,13 @@ static int32_t p2_node_at(kvspace_t *kv, const char *key) { int32_t nid = kv->hdr->art_root; while (nid >= 0) { art_hdr_t *h = art_hdr(kv, nid); - if (!h || h->type == ART_MOVED) { - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at FAIL key=%s nid=%d type=%d d=%d/%d\n", key, nid, h ? (int)h->type : -1, d, klen); } + if (!h || h->type == ART_MOVED) return -1; - } if (h->prefix_len) { int s = pfx_shared(h->prefix, h->prefix_len, (const uint8_t *)key + d, klen - d); - if (s != h->prefix_len && d + s != klen) { - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at PFX key=%s nid=%d d=%d s=%d plen=%d klen=%d\n", key, nid, d, s, (int)h->prefix_len, klen); } + if (s != h->prefix_len && d + s != klen) return -1; - } d += s; } /* 键可能正好结束在 prefix_len==0 的分支节点上(如 k1 落在根 N4 的子叶), @@ -450,7 +461,6 @@ static int32_t p2_node_at(kvspace_t *kv, const char *key) { nid = art_child(kv, h, (uint8_t)key[d]); d++; } - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG node_at EOF key=%s d=%d/%d\n", key, d, klen); } return -1; } @@ -533,16 +543,6 @@ static int p2_reg_claim(kvspace_t *kv) { __atomic_compare_exchange_n(&s->state, &v, P2_SLOT_FREE, false, __ATOMIC_ACQ_REL, __ATOMIC_ACQUIRE); } - if (getenv("P2_DEBUG")) { - int nf = 0, nm = 0; - for (int i = 0; i < P2_SLOT_N; i++) { - uint32_t v = __atomic_load_n(&kv->hdr->slots[i].state, __ATOMIC_ACQUIRE); - if (v == P2_SLOT_FREE) nf++; - else if (v == P2_SLOT_MINE) nm++; - } - fprintf(stderr, "DBG claim pid=%u start=%llu free=%d mine=%d\n", me, - (unsigned long long)st, nf, nm); - } for (int i = 0; i < P2_SLOT_N; i++) { p2_slot_t *s = &kv->hdr->slots[i]; uint32_t expect = P2_SLOT_FREE; @@ -624,21 +624,6 @@ static int gens_grow(kvspace_t *kv) { return 0; } - -/* ── P2: tombstone 延迟释放(粗粒度 epoch:队满或显式 flush 时统一 free)── */ - - -int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, - const char *key_fallback, kvspaceXvalueBodyView_t *out); -static int32_t art_id_of(kvspace_t *kv, const void *p); -static art_hdr_t *art_hdr(kvspace_t *kv, int32_t id); -static int32_t art_follow(kvspace_t *kv, int32_t id); -static int32_t ref_leaf(kvspace_t *kv, kvspaceRef_t *ref, const char *key); -static int32_t art_walk(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, - int d, int32_t *par, int *pard); -static int32_t art_child(kvspace_t *kv, void *n, uint8_t b); -static int pfx_shared(const uint8_t *a, int al, const uint8_t *b, int bl); - /* ── P2: 版本化 view token ─────────────────────────────────────── */ /* fork 检测不能每 op 调 getpid()(本机 glibc 不缓存,一次 ~150ns)。 * 用 pthread_atfork 的子进程钩子把全局世代 +1;热路径只比两个 u32。 */ @@ -669,6 +654,22 @@ static int head_of(const uint8_t *val, int32_t val_len, kvspaceXvalueHead *out) return 0; } +/* Write validation: head_of + complete attr region (attr_len >= 2 and within val_len). + * read_value derives total length from 8+len+attr_len without storing val_len, so + * values with an incomplete attr region would be unreadable if accepted. */ +static int value_ok(const uint8_t *val, int32_t val_len, + kvspaceXvalueHead *out) { + if (head_of(val, val_len, out) != 0) + return -1; + uint64_t l = kvspaceXvalueHeadLen(out); + if ((uint64_t)val_len < KVSPACE_XVALUE_HEAD_LEN + l + 2) + return -1; + uint64_t alen = kvspaceXvalueHeadAttrLen(val, l); + if (alen < 2 || alen > (uint64_t)val_len - KVSPACE_XVALUE_HEAD_LEN - l) + return -1; + return 0; +} + int kvspaceShmViewFromRef(kvspace_t *kv, const kvspaceRef_t *ref, const char *key_fallback, kvspaceViewToken_t *tok) { (void)key_fallback; @@ -728,17 +729,20 @@ int kvspaceShmResolvePrefix(kvspace_t *kv, const char *path, kvspaceRef_t *ref) art_hdr_t *h = art_hdr(kv, nid); if (!h || h->type == ART_MOVED) return -1; + int s = 0; if (h->prefix_len) { - int s = pfx_shared(h->prefix, h->prefix_len, - (const uint8_t *)path + d, klen - d); + s = pfx_shared(h->prefix, h->prefix_len, + (const uint8_t *)path + d, klen - d); if (s != h->prefix_len && d + s != klen) return -1; d += s; - if (d == klen) { - ref->block_id = (uint32_t)nid; - ref->depth = (uint32_t)s; /* 该节点前缀里被 path 覆盖的字节数 */ - return 0; - } + } + /* path may end at a node with prefix_len == 0 (e.g. "/vthread/1/" next to + * "/vthread/10/"); return here before reading path[klen]. */ + if (d == klen) { + ref->block_id = (uint32_t)nid; + ref->depth = (uint32_t)s; /* bytes of this node's prefix covered by path */ + return 0; } nid = art_child(kv, h, (uint8_t)path[d]); d++; @@ -802,14 +806,18 @@ int kvspaceShmViewCheck(kvspace_t *kv, const kvspaceViewToken_t *tok) { return 0; } -/* 热路径写:同尺寸就地写(8B inline 情形)。0=成功;1=需要回落(尺寸/版本不符)。 */ +/* Hot-path write: in-place same-length body write. Returns 0 on success, 1 to fall back. + * The attr region immediately follows body (at 8+len), so only writes matching the current + * header len are accepted; variable-length writes fall back to full-value write. */ int kvspaceShmViewWrite(kvspace_t *kv, kvspaceViewToken_t *tok, const uint8_t *data, uint32_t len) { if (!kv || P2_VIEW_GUARD(kv)) return -1; if (kvspaceShmViewCheck(kv, tok) != 0) return 1; - if (len > tok->body_cap || len != tok->value_len) + kvspaceXvalueHead cur; + memcpy(&cur, tok->head, sizeof cur); + if ((uint64_t)len != kvspaceXvalueHeadLen(&cur) || len > tok->body_cap) return 1; kv_gens_t *g = gens_at(kv, (int32_t)tok->node_id); if (!g) @@ -851,6 +859,7 @@ int kvspaceShmViewDel(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key) { } __atomic_add_fetch(&g->value_gen, 2u, __ATOMIC_ACQ_REL); /* 静止态保持偶数 */ garb_commit(kv, gidx, AV_OFF(hv), tok->cap); + list_gen_bump(kv); /* bump after key removal */ (void)key; return 0; } @@ -1240,10 +1249,6 @@ int kvspaceShmViewSweep(kvspace_t *kv) { return p2_round(kv); } -int kvspaceShmViewFlushTomb(kvspace_t *kv) { - return kvspaceShmViewSweep(kv); -} - int kvspaceShmViewRefresh(kvspace_t *kv, kvspaceViewToken_t *tok, const char *key) { kvspaceViewToken_t t; if (kvspaceShmViewResolve(kv, key, &t) != 0) @@ -1280,12 +1285,19 @@ static int art_slab_grow(kvspace_t *kv) { 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. */ +#ifdef BLOCKMALLOC_COUNT_MASK +#define BM_MALLOC_BLOCKS(m) blocks_malloc_count(m) +#define BM_HEAD_SIZE(m) blocks_head_size(m) +#else +#define BM_MALLOC_BLOCKS(m) ((uint64_t)(m)->malloc_blocks) +#define BM_HEAD_SIZE(m) ((uint64_t)(m)->sizeof_block_head) +#endif 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; + uint64_t next_end = (uint64_t)block_offset(p, BM_MALLOC_BLOCKS(p)) + + BM_HEAD_SIZE(p) + p->block_size; return next_end > p->total_size; } @@ -1411,9 +1423,6 @@ static void kv_sbo_free(kvspace_t *kv, uint64_t off) { sbo_free(kv->sbo_meta, off); } -/* P2: tombstone —— 有界 ring(L1 内)+ 后台 flush 线程。 - * 前台只做:ring[tail & MASK] = off; tail++(release); 唤醒后台。 - * 队列满时前台等待(backpressure),绝不在调用线程里 sbo_free。 */ /* ---- helpers ---- */ static void *art_blk(kvspace_t *kv, int32_t id) { if (id < 0) @@ -1421,25 +1430,66 @@ static void *art_blk(kvspace_t *kv, int32_t id) { return kv->art_data + blockdata_offset(kv->art_meta, (uint64_t)id); } /* Bump-allocate k consecutive blocks; the node spans them (interior heads unused). */ +/* blockmalloc packs sizeof_block_head:4 and malloc_blocks:60 into a single u64 word + * between block_size and used_blocks. Lock-free readers (art_blk / art_id_of) read this + * word concurrently, so writers holding meta->lock must update the whole word atomically + * (via blocks_set_malloc_count when available, or whole-word atomic store fallback). */ +_Static_assert(offsetof(blocks_meta_t, used_blocks) == + offsetof(blocks_meta_t, block_size) + 2 * sizeof(uint64_t), + "blockmalloc: sizeof_block_head/malloc_blocks must share one u64 word"); +#ifdef BLOCKMALLOC_COUNT_MASK +static inline void bm_set_malloc_blocks(blocks_meta_t *m, uint64_t n) { + blocks_set_malloc_count(m, n); +} +#else +#define BM_WORD_OFF (offsetof(blocks_meta_t, block_size) + sizeof(uint64_t)) +static unsigned bm_count_shift(void) { + blocks_meta_t t; + memset(&t, 0, sizeof t); + t.malloc_blocks = 1; + uint64_t w; + memcpy(&w, (const char *)&t + BM_WORD_OFF, sizeof w); + return w ? (unsigned)__builtin_ctzll(w) : 64u; +} +/* Caller holds m->lock. */ +static void bm_set_malloc_blocks(blocks_meta_t *m, uint64_t n) { + uint64_t *w = (uint64_t *)((char *)m + BM_WORD_OFF); + const unsigned sh = bm_count_shift(); + const uint64_t mask = ((1ULL << 60) - 1) << sh; + uint64_t old = __atomic_load_n(w, __ATOMIC_RELAXED); + __atomic_store_n(w, (old & ~mask) | ((n << sh) & mask), __ATOMIC_RELEASE); +} +#endif +/* Verify whole-word bitfield layout at open time. */ +static bool bm_layout_ok(void) { + blocks_meta_t t; + memset(&t, 0, sizeof t); + t.sizeof_block_head = 8; + t.malloc_blocks = 5; + t.used_blocks = 7; + bm_set_malloc_blocks(&t, 0x0123456789ABCDEull); + return t.sizeof_block_head == 8 && t.malloc_blocks == 0x0123456789ABCDEull && + t.block_size == 0 && t.used_blocks == 7; +} static int64_t art_bump(kvspace_t *kv, uint32_t k) { blocks_meta_t *m = kv->art_meta; spin_lock(&m->lock); - uint64_t id = m->malloc_blocks; + uint64_t id = BM_MALLOC_BLOCKS(m); if ((uint64_t)block_offset(m, id + k) > m->total_size) { spin_unlock(&m->lock); return -1; } - m->malloc_blocks += k; - m->used_blocks += k; setblock_t(m, kv->art_data + block_offset(m, id), 1, -1); + bm_set_malloc_blocks(m, id + k); + m->used_blocks += k; spin_unlock(&m->lock); return (int64_t)id; } static int32_t art_balloc(kvspace_t *kv, int t) { - const uint64_t stride = (uint64_t)kv->art_meta->sizeof_block_head + - kv->art_meta->block_size; + const uint64_t head = BM_HEAD_SIZE(kv->art_meta); + const uint64_t stride = head + kv->art_meta->block_size; const uint32_t k = (uint32_t)(((uint64_t)art_node_sz(t) + - kv->art_meta->sizeof_block_head + stride - 1) / + head + stride - 1) / stride); int64_t id = art_bump(kv, k); while (id < 0) { @@ -1517,16 +1567,11 @@ static int32_t art_walk(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen if (!h || h->type == ART_MOVED) return -1; if (h->prefix_len) { - int s = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); - if (s != h->prefix_len) { - if (d + s < klen) - return -1; - if (s < h->prefix_len) - return -1; - } - d += h->prefix_len; - if (d > klen) + /* s == prefix_len implies d + prefix_len <= klen */ + if (pfx_shared(h->prefix, h->prefix_len, key + d, klen - d) != + h->prefix_len) return -1; + d += h->prefix_len; } if (d == klen) return AV_HAS(art_val_load(h)) ? nid : -1; @@ -1750,7 +1795,6 @@ static int32_t art_grow(kvspace_t *kv, void *on) { } { /* P2: 搬迁 —— 新节点 struct_gen = 旧 +1;旧节点也动一次 */ int32_t oid = art_id_of(kv, oh); - (void)gens_at; gen_bump_struct(kv, nid); /* 新节点 gen 先抬一代 */ gen_bump_struct(kv, oid); /* 旧节点也失效 */ } @@ -1994,8 +2038,8 @@ static int32_t art_del(kvspace_t *kv, int32_t nid, const uint8_t *key, int klen, if (!h) return -1; if (h->prefix_len) { - int s = pfx_shared(h->prefix, h->prefix_len, key + d, klen - d); - if (s != h->prefix_len || d + h->prefix_len > klen) + if (pfx_shared(h->prefix, h->prefix_len, key + d, klen - d) != + h->prefix_len) return nid; d += h->prefix_len; } @@ -2294,6 +2338,88 @@ static int art_scan_pfx(kvspace_t *kv, const char *pfx, int plen, char *buf, return art_scan_children(kv, nid, buf, bpos, bcap, plen, scan); } +/* Collect all full keys with values under nid (buf[0, bpos) holds the reconstructed prefix). + * Unlike art_scan_children, does not truncate at child boundaries or hide system names (used by DelTree). */ +static int art_collect_keys(kvspace_t *kv, int32_t nid, char *buf, int bpos, + int bcap, keyscan_t *scan) { + if (nid < 0) + return 0; + art_hdr_t *h = art_hdr(kv, nid); + if (!h || h->prefix_len > bcap - bpos - 1) + return -1; + for (int i = 0; i < h->prefix_len; i++) + buf[bpos++] = h->prefix[i]; + if (AV_HAS(art_val_load(h)) && keyscan_add_n(scan, buf, (size_t)bpos) != 0) + return -1; + const uint8_t *keys = NULL; + const int32_t *children = NULL; + int n = 0; + switch (h->type) { + case ART_N4: + keys = ((art_n4_t *)h)->keys; + children = ((art_n4_t *)h)->children; + n = h->count; + break; + case ART_N16: + keys = ((art_n16_t *)h)->keys; + children = ((art_n16_t *)h)->children; + n = h->count; + break; + case ART_N48: { + art_n48_t *x = (art_n48_t *)h; + for (int i = 0; i < 256; i++) { + if (x->index[i] == 255) + continue; + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (char)i; + if (art_collect_keys(kv, x->children[x->index[i]], buf, bpos + 1, + bcap, scan) != 0) + return -1; + } + return 0; + } + case ART_N256: { + art_n256_t *x = (art_n256_t *)h; + for (int i = 0; i < 256; i++) { + if (x->children[i] < 0) + continue; + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (char)i; + if (art_collect_keys(kv, x->children[i], buf, bpos + 1, bcap, + scan) != 0) + return -1; + } + return 0; + } + default: + return 0; + } + for (int i = 0; i < n; i++) { + if (bpos + 1 >= bcap) + return -1; + buf[bpos] = (char)keys[i]; + if (art_collect_keys(kv, children[i], buf, bpos + 1, bcap, scan) != 0) + return -1; + } + return 0; +} + +/* Collect all keys with values starting with pfx (including system variables). */ +static int art_scan_keys(kvspace_t *kv, const char *pfx, int plen, + keyscan_t *scan) { + char buf[4096]; + if (plen >= (int)sizeof buf) + return -1; + int bpos = 0; + int32_t nid = art_cover(kv, kv->hdr->art_root, (const uint8_t *)pfx, plen, + buf, (int)sizeof buf, &bpos); + if (nid < 0) + return 0; + return art_collect_keys(kv, nid, buf, bpos, (int)sizeof buf, scan); +} + /* ============ lifecycle ============ */ /* 8 * 64^k; s wraps to 0. */ static bool sbo_data_size_ok(size_t n) { @@ -2306,6 +2432,10 @@ static bool sbo_data_size_ok(size_t n) { kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { if (!path) return NULL; + if (!bm_layout_ok()) { + fprintf(stderr, "kvspace: unexpected blockmalloc blocks_meta_t layout\n"); + return NULL; + } char head_path[PATH_MAX], data_path[PATH_MAX], gen_path[PATH_MAX], garb_path[PATH_MAX]; if (snprintf(head_path, sizeof head_path, "%s.sbo.head", path) >= @@ -2346,6 +2476,14 @@ kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { if (pread(kv->r_art.fd, &tmp, sizeof tmp, 0) != (ssize_t)sizeof tmp || memcmp(tmp.magic, KVS_MAGIC, sizeof(KVS_MAGIC) - 1) != 0) goto fail; + /* Check format_version before region_attach: header size changes across versions, + * so an older store would otherwise fail in region_attach without printing a diagnostic. */ + if (tmp.format_version != KVSPACE_FORMAT_VERSION) { + fprintf(stderr, "kvspace: unsupported store format (version=%u, " + "expected %u): %s; rebuild the shm store\n", + tmp.format_version, KVSPACE_FORMAT_VERSION, path); + goto fail; + } art_slab = (size_t)tmp.art_slab_size; sbo_head = (size_t)tmp.sbo_head_size; data_size = (size_t)tmp.sbo_data_size; @@ -2441,6 +2579,7 @@ kvspace_t *kvspaceShmOpen(const char *path, size_t data_size) { goto fail; /* 已有大量垃圾且建不了线程:open 硬失败 */ pthread_mutex_init(&kv->wlock, NULL); + pthread_mutex_init(&kv->lc_mtx, NULL); for (int i = 0; i < WATCH_TABLE_SZ; i++) { pthread_cond_init(&kv->watches[i].cond, NULL); pthread_mutex_init(&kv->watches[i].mtx, NULL); @@ -2483,7 +2622,6 @@ static bool p2_other_alive(kvspace_t *kv) { void kvspaceShmClose(kvspace_t *kv) { if (!kv) return; - p2_setstats_dump(); /* 最后一个存活进程负责清场:Linux 跑一次机会性 round;macOS 直接清环。 */ if (kv->pid == (uint32_t)getpid() && kv->p2_slot >= 0 && !p2_other_alive(kv)) { @@ -2519,6 +2657,8 @@ void kvspaceShmClose(kvspace_t *kv) { free(kv->watches[i].val); } pthread_mutex_destroy(&kv->wlock); + list_cache_drop(kv); + pthread_mutex_destroy(&kv->lc_mtx); region_close(&kv->r_art); region_close(&kv->r_head); region_close(&kv->r_data); @@ -2528,64 +2668,46 @@ void kvspaceShmClose(kvspace_t *kv) { } /* ---- link resolve helpers ---- */ -static int read_value(kvspace_t *kv, uint64_t off, uint8_t **out, int32_t *ol) { - uint8_t *s = kv->sbo_data + off; - size_t sz = sbo_allocated_size(kv->sbo_meta, off); +/* Parse full value length = head + body + attr within [s, s+bound) (empty attr = 2). */ +static int parse_value(const uint8_t *s, uint64_t bound, int32_t *ol) { + if (bound > INT32_MAX) + bound = INT32_MAX; kvspaceXvalueHead wire; - if (head_of(s, (int32_t)sz, &wire) == 0) { - uint64_t l = kvspaceXvalueHeadLen(&wire); - if (l > INT32_MAX) - return -1; - *out = s; - /* 完整值长 = head + body + attr(attr_len:u16 @8+len 自描述;空 attr=2)。 */ - if ((uint64_t)sz < KVSPACE_XVALUE_HEAD_LEN + l + 2) - return -1; - uint64_t alen = kvspaceXvalueHeadAttrLen(s, l); - if (alen < 2 || alen > (uint64_t)sz - KVSPACE_XVALUE_HEAD_LEN - l) - return -1; - *ol = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l + alen); - return 0; - } - return -1; + if (head_of(s, (int32_t)bound, &wire) != 0) + return -1; + uint64_t l = kvspaceXvalueHeadLen(&wire); + if (bound < KVSPACE_XVALUE_HEAD_LEN + l + 2) + return -1; + uint64_t alen = kvspaceXvalueHeadAttrLen(s, l); + if (alen < 2 || alen > bound - KVSPACE_XVALUE_HEAD_LEN - l) + return -1; + *ol = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l + alen); + return 0; } -/* link 解析:ptr(ref=1)与 @ext(ref=2)的 XValue 恒为叶子(spec 硬规则——指针/扩展键 - * 之后不可能再有成员,成员只挂在 target 上),故路径中任何 `/` 分隔的容器前缀都绝不会是 - * link;唯一可能是 ptr 的只有整键叶子本身。因此只需查整键一次:非 ptr 即完成,ptr 则顺链 - * 到 target(本身又是一个完整叶子键)再查,最多追 16 跳。叶子不存在→非链,原样返回(调用 - * 方另做 extindex/dir 兜底)。取代旧的「逐前缀 1~3 次 art_search + 追加 rest」全下降探测。 */ -static void resolve_path(kvspace_t *kv, const char *path, char *out, int osz) { - strncpy(out, path, osz - 1); - out[osz - 1] = '\0'; - for (int depth = 0; depth < 16; depth++) { - art_hdr_t *h = - art_search(kv, kv->hdr->art_root, (const uint8_t *)out, (int)strlen(out)); - if (!h) - return; - uint64_t hv = art_val_load(h); - if (!AV_HAS(hv)) - return; - uint8_t *raw; - int32_t rl; - if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0) - return; - kvspaceXvalueHead wire; - if (head_of(raw, rl, &wire) == 0) { - uint64_t l2 = kvspaceXvalueHeadLen(&wire); - if (wire.ptr != KVSPACE_XVALUE_PTR_PTR || l2 >= (uint64_t)osz) - return; - memcpy(out, raw + KVSPACE_XVALUE_HEAD_LEN, (size_t)l2); - out[l2] = 0; - continue; - } - return; - } + +/* Write path: bound by physical box capacity (takes allocator lock) so a corrupted + * header cannot cause caller writes to overflow into adjacent boxes. */ +static int read_value(kvspace_t *kv, uint64_t off, uint8_t **out, int32_t *ol) { + *out = kv->sbo_data + off; + return parse_value(*out, sbo_allocated_size(kv->sbo_meta, off), ol); } -/* resolve_path 的融合版:解析 link 链的同时把终端叶子的值一并取出,令调用方省掉 - * 「解析后再 art_search + read_value」的整套重复下降。每跳只取一次 head(ref 判定与 - * 值长度共用),命中带值节点即返 fetched=1(*raw/*rl 就位)。语义与 - * resolve_path + art_search + read_value 逐字等价:未命中/无值 → 返回该节点、fetched=0, - * 交调用方走 extindex/dir 兜底。 */ +/* Read-only path: bound by mapped data region without taking the allocator lock. + * sbo_allocated_size contends on the cross-process spinlock on every read; since value_ok + * already ensures 8+len+attr_len fits in the box at write time, both bounds agree on valid values. */ +static uint64_t ro_bound(const kvspace_t *kv, uint64_t off) { + uint64_t m = kv->r_data.mapped; + return off < m ? m - off : 0; +} +static int read_value_ro(kvspace_t *kv, uint64_t off, uint8_t **out, + int32_t *ol) { + *out = kv->sbo_data + off; + return parse_value(*out, ro_bound(kv, off), ol); +} +/* Link resolution: ptr (ref=1) and @ext (ref=2) XValues are always leaves (never intermediate + * `/` prefixes), so only the full leaf key can be a link. Follow at most 16 hops, fetching + * the terminal leaf value at the same time (fetched=1) to avoid a second art_search + read_value. + * Missing/valueless leaf returns fetched=0 for extindex/dir fallback. Caller ensures strlen(path) < osz. */ static art_hdr_t *resolve_fetch(kvspace_t *kv, const char *path, char *out, int osz, uint8_t **raw, int32_t *rl, int *fetched) { *fetched = 0; @@ -2600,19 +2722,14 @@ static art_hdr_t *resolve_fetch(kvspace_t *kv, const char *path, char *out, int if (!AV_HAS(hv)) return h; uint8_t *s = kv->sbo_data + AV_OFF(hv); - size_t sz = sbo_allocated_size(kv->sbo_meta, AV_OFF(hv)); + uint64_t sz = ro_bound(kv, AV_OFF(hv)); kvspaceXvalueHead wire; - if (head_of(s, (int32_t)sz, &wire) == 0) { + if (head_of(s, sz > INT32_MAX ? INT32_MAX : (int32_t)sz, &wire) == 0) { uint64_t l3 = kvspaceXvalueHeadLen(&wire); if (wire.ptr != KVSPACE_XVALUE_PTR_PTR || l3 >= (uint64_t)osz) { - /* 完整值长 = head + body + attr(attr_len @8+len 自描述)。 */ - if ((uint64_t)sz < KVSPACE_XVALUE_HEAD_LEN + l3 + 2) - return NULL; - uint64_t alen = kvspaceXvalueHeadAttrLen(s, l3); - if (alen < 2 || alen > (uint64_t)sz - KVSPACE_XVALUE_HEAD_LEN - l3) + if (parse_value(s, sz, rl) != 0) return NULL; *raw = s; - *rl = (int32_t)(KVSPACE_XVALUE_HEAD_LEN + l3 + alen); *fetched = 1; return h; } @@ -2625,6 +2742,14 @@ static art_hdr_t *resolve_fetch(kvspace_t *kv, const char *path, char *out, int return NULL; } +/* Resolve path only (used by List). */ +static void resolve_path(kvspace_t *kv, const char *path, char *out, int osz) { + uint8_t *raw; + int32_t rl; + int fetched; + (void)resolve_fetch(kv, path, out, osz, &raw, &rl, &fetched); +} + /* Read the extension locator stored at a directory key. */ static int dir_ext_path(kvspace_t *kv, const char *dir, char *out, int osz) { out[0] = 0; @@ -2637,7 +2762,7 @@ static int dir_ext_path(kvspace_t *kv, const char *dir, char *out, int osz) { return 0; uint8_t *raw; int32_t rl; - if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0) + if (read_value_ro(kv, AV_OFF(hv), &raw, &rl) < 0) return 0; kvspaceXvalueHead wire; if (head_of(raw, rl, &wire) != 0 || @@ -2662,6 +2787,9 @@ uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, if (kv_sync(kv) != 0) return NULL; char kbuf[1024]; + /* Set rejects >= 1024-byte keys; do not truncate and look up a shorter key. */ + if (strlen(key) >= sizeof kbuf) + return NULL; uint8_t *raw; int32_t rl; int fetched = 0; @@ -2689,7 +2817,7 @@ uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, if (eh && AV_HAS(ehv)) { uint8_t *raw; int32_t rl; - if (read_value(kv, AV_OFF(ehv), &raw, &rl) == 0) { + if (read_value_ro(kv, AV_OFF(ehv), &raw, &rl) == 0) { *ol = rl; free(parent); free(name); @@ -2701,7 +2829,7 @@ uint8_t *kvspaceShmGet(kvspace_t *kv, const char *key, int resolve, free(name); return NULL; } - if (!fetched && read_value(kv, AV_OFF(hv), &raw, &rl) < 0) + if (!fetched && read_value_ro(kv, AV_OFF(hv), &raw, &rl) < 0) return NULL; *ol = rl; return raw; @@ -2772,7 +2900,7 @@ uint8_t *kvspaceShmGetByRef(kvspace_t *kv, kvspaceRef_t *ref, if (h && AV_HAS(hv)) { uint8_t *raw; int32_t rl; - if (read_value(kv, AV_OFF(hv), &raw, &rl) == 0) { + if (read_value_ro(kv, AV_OFF(hv), &raw, &rl) == 0) { *ol = rl; return raw; } @@ -2788,22 +2916,6 @@ uint8_t *kvspaceShmGetByRef(kvspace_t *kv, kvspaceRef_t *ref, return raw; } -/* 定位写的前置条件:只允许改 body [8, 8+len),head 与 attr 一律不可写。 */ -int kvspaceShmPartInBody(const uint8_t *raw, int32_t total, uint32_t offset, - uint32_t buf_len) { - if (!raw || total < (int32_t)KVSPACE_XVALUE_HEAD_LEN) - return 0; - kvspaceXvalueHead h; - memcpy(&h, raw, sizeof h); - const uint64_t len = kvspaceXvalueHeadLen(&h); - if (len > (uint64_t)total - KVSPACE_XVALUE_HEAD_LEN) - return 0; - if (offset < KVSPACE_XVALUE_HEAD_LEN) - return 0; - const uint64_t rel = (uint64_t)offset - KVSPACE_XVALUE_HEAD_LEN; - return rel <= len && (uint64_t)buf_len <= len - rel; -} - int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, const char *key_fallback, uint32_t offset, const uint8_t *buf, uint32_t buf_len) { @@ -2830,12 +2942,10 @@ int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, /* per-box 物理容量:损坏 head 的 len 可能大于 box 但仍在全局内, * 不做这一步会跨 box 写坏相邻值。 */ uint64_t cap = sbo_allocated_size(kv->sbo_meta, off); - const uint64_t rel = - offset >= KVSPACE_XVALUE_HEAD_LEN - ? (uint64_t)offset - KVSPACE_XVALUE_HEAD_LEN - : UINT64_MAX; if (cap <= data_size - off && len <= cap - KVSPACE_XVALUE_HEAD_LEN && - rel <= len && (uint64_t)buf_len <= len - rel) { + (uint64_t)offset <= KVSPACE_XVALUE_HEAD_LEN + len && + (uint64_t)buf_len <= + KVSPACE_XVALUE_HEAD_LEN + len - (uint64_t)offset) { kv_gens_t *g = gens_at(kv, id); if (g) __atomic_add_fetch(&g->value_gen, 1u, @@ -2856,7 +2966,9 @@ int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, return -1; int32_t rl = 0; uint8_t *d = kvspaceShmGet(kv, key_fallback, 0, &rl); - if (!d || !kvspaceShmPartInBody(d, rl, offset, buf_len)) + /* Re-bound by physical box capacity before writing; use u64 to prevent u32 wraparound. */ + if (!d || read_value(kv, (uint64_t)(d - kv->sbo_data), &d, &rl) != 0 || + (uint64_t)offset + buf_len > (uint64_t)rl) return -1; kvspaceShmResolveRef(kv, key_fallback, ref); kv_gens_t *g = gens_at(kv, (int32_t)ref->block_id); @@ -2870,7 +2982,7 @@ int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, uint8_t *raw; int32_t rl; if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0 || - !kvspaceShmPartInBody(raw, rl, offset, buf_len)) + (uint64_t)offset + buf_len > (uint64_t)rl) return -1; kv_gens_t *g2 = gens_at(kv, id); if (g2) @@ -2881,6 +2993,21 @@ int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, return 0; } +/* Partial write by key (follows links and extindex fallback like Get with resolve=1). + * Locates via lock-free read, then re-bounds by physical box capacity before writing. */ +int kvspaceShmSetPart(kvspace_t *kv, const char *key, uint32_t offset, + const uint8_t *buf, uint32_t buf_len) { + if (!kv || !key || (buf_len && !buf)) + return -1; + int32_t rl = 0; + uint8_t *d = kvspaceShmGet(kv, key, 1, &rl); + if (!d || read_value(kv, (uint64_t)(d - kv->sbo_data), &d, &rl) != 0 || + (uint64_t)offset + buf_len > (uint64_t)rl) + return -1; + memcpy(d + offset, buf, buf_len); + return 0; +} + /* /vthread//‥status:合法内容长度上限与创建时物理容量下限。 */ #define KVSPACE_STATUS_MAX_LEN 16u #define KVSPACE_STATUS_MIN_CAP 80u @@ -2927,8 +3054,8 @@ int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, if (cap < KVSPACE_XVALUE_HEAD_LEN || l5 > cap - KVSPACE_XVALUE_HEAD_LEN) return -1; - /* status:只暴露规范形状(SLACK、len≤16)且物理容量达标的槽; - 类型([n]char/utf8)在 x‥langtype attr key,不在此判。 */ + /* status: only expose canonical slots (SLACK, len <= 16) with sufficient physical capacity; + langtype ([n]char/utf8) lives in the inline attr region and is not checked here. */ if (is_vthread_status_key(key_fallback)) { if (wire.storetype != KVSPACE_XVALUE_ST_SLACK || cap < KVSPACE_STATUS_MIN_CAP || @@ -2943,45 +3070,29 @@ int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, return 0; } -/* 临时探针(P2_SETSTATS=):统计写入形态,用于静默屏障可行性评估。 */ -static struct { uint64_t same, realloc_, fresh, retired_bytes; } g_setstats; -static void p2_setstats_dump(void) { - const char *f = getenv("P2_SETSTATS"); - if (!f || !f[0]) - return; - FILE *fp = fopen(f, "a"); - if (!fp) - return; - fprintf(fp, "pid=%d same=%llu realloc=%llu fresh=%llu retired_bytes=%llu\n", - (int)getpid(), (unsigned long long)g_setstats.same, - (unsigned long long)g_setstats.realloc_, - (unsigned long long)g_setstats.fresh, - (unsigned long long)g_setstats.retired_bytes); - fclose(fp); -} - -/* 原始落盘(不处理容器/member 语义,供内部调用,避免递归)。 */ +/* Raw local write (internal helper without container/member semantics). + * reserve: minimum physical box capacity (for WriteNewPlace body_cap); 0 uses val_len. */ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, - int32_t val_len, art_hdr_t *old) { - { - static bool reg; - if (!reg) { reg = true; atexit(p2_setstats_dump); } - } + int32_t val_len, art_hdr_t *old, size_t reserve) { + size_t alloc_len = (size_t)val_len; + if (reserve > alloc_len) + alloc_len = reserve; + if (is_vthread_status_key(key) && alloc_len < KVSPACE_STATUS_MIN_CAP) + alloc_len = KVSPACE_STATUS_MIN_CAP; uint64_t ohv = old ? art_val_load(old) : 0; bool retiring = old && AV_HAS(ohv); uint64_t gidx = 0; uint64_t cap = 0; if (retiring) { cap = sbo_allocated_size(kv->sbo_meta, AV_OFF(ohv)); - if ((uint64_t)val_len <= cap) { - /* 同尺寸就地写:指针不变,value_gen 奇偶保护(一次查表) */ + if ((uint64_t)alloc_len <= cap) { + /* Existing box fits (including reserve): write in place under value_gen seqlock */ kv_gens_t *g = gens_at(kv, art_id_of(kv, old)); if (g) __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); memcpy(kv->sbo_data + AV_OFF(ohv), val, (size_t)val_len); if (g) __atomic_add_fetch(&g->value_gen, 1u, __ATOMIC_ACQ_REL); - g_setstats.same++; return 0; } /* 换 box:reserve 在任何变更之前;失败 = 硬错误、旧值原样 */ @@ -2998,14 +3109,7 @@ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, return -1; } #endif - g_setstats.realloc_++; - g_setstats.retired_bytes += cap; - } else { - g_setstats.fresh++; } - size_t alloc_len = (size_t)val_len; - if (is_vthread_status_key(key) && alloc_len < KVSPACE_STATUS_MIN_CAP) - alloc_len = KVSPACE_STATUS_MIN_CAP; uint64_t off = kv_sbo_alloc(kv, alloc_len); if (off == (uint64_t)-1) { if (retiring) @@ -3032,7 +3136,6 @@ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, { /* 换 box / 墓碑复活:旧 token 全部 STALE */ int32_t nid = p2_node_at(kv, key); kv_gens_t *g = gens_at(kv, nid); - if (getenv("P2_DEBUG")) { fprintf(stderr, "DBG reset-lookup key=%s nid=%d g=%p root=%d\n", key, nid, (void *)g, kv->hdr->art_root); } if (g) { gen_bump_struct(kv, nid); gen_bump_value(kv, nid); @@ -3040,14 +3143,16 @@ static int shm_set_local(kvspace_t *kv, const char *key, const uint8_t *val, } if (retiring) garb_commit(kv, gidx, AV_OFF(ohv), cap); + else + list_gen_bump(kv); /* new key or tombstone revive: bump after art_ins */ return 0; } static int shm_set_raw(kvspace_t *kv, const char *key, const uint8_t *val, - int32_t val_len) { + int32_t val_len, size_t reserve) { art_hdr_t *old = art_search(kv, kv->hdr->art_root, (const uint8_t *)key, (int)strlen(key)); - return shm_set_local(kv, key, val, val_len, old); + return shm_set_local(kv, key, val, val_len, old, reserve); } static int frame_operand_ptr(const char *key, const kvspaceXvalueHead *wire) { @@ -3100,8 +3205,8 @@ static int shm_set_validated(kvspace_t *kv, const char *key, free(nn); } - /* status 固定档:形状固定(SLACK、len≤16)、物理容量 ≥ 下限。 - * 类型([n]char/utf8)在 x‥langtype attr key,不在此判。 */ + /* status slot: fixed shape (SLACK, len <= 16) and physical capacity >= minimum. + * langtype ([n]char/utf8) lives in the inline attr region and is not checked here. */ if (is_vthread_status_key(kbuf)) { if (wire->storetype != KVSPACE_XVALUE_ST_SLACK || kvspaceXvalueHeadLen(wire) > KVSPACE_STATUS_MAX_LEN) @@ -3119,7 +3224,7 @@ static int shm_set_validated(kvspace_t *kv, const char *key, return -1; } } - return shm_set_local(kv, kbuf, val, val_len, local); + return shm_set_local(kv, kbuf, val, val_len, local, 0); } int kvspaceShmSet(kvspace_t *kv, const char *key, const uint8_t *val, @@ -3127,7 +3232,7 @@ int kvspaceShmSet(kvspace_t *kv, const char *key, const uint8_t *val, if (!kv || !key || !val || val_len <= 0) return -1; kvspaceXvalueHead wire; - if (head_of(val, val_len, &wire) != 0) + if (value_ok(val, val_len, &wire) != 0) return -1; return shm_set_validated(kv, key, val, val_len, &wire); } @@ -3136,7 +3241,7 @@ int kvspaceShmSetValue(kvspace_t *kv, const char *key, const uint8_t *val, int32_t val_len) { kvspaceXvalueHead head; if (!kv || !key || !val || val_len <= 0 || - head_of(val, val_len, &head) != 0) + value_ok(val, val_len, &head) != 0) return -2; return shm_set_validated(kv, key, val, val_len, &head); } @@ -3146,7 +3251,7 @@ int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, int32_t val_len) { kvspaceXvalueHead head; if (!kv || !ref || !key_fallback || !val || val_len <= 0 || - head_of(val, val_len, &head) != 0) + value_ok(val, val_len, &head) != 0) return -2; if (kv_sync(kv) != 0) return -1; @@ -3156,7 +3261,7 @@ int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, int32_t id = ref_leaf(kv, ref, key_fallback); art_hdr_t *h = id >= 0 ? art_hdr(kv, id) : NULL; if (h && AV_HAS(art_val_load(h))) - return shm_set_local(kv, key_fallback, val, val_len, h); + return shm_set_local(kv, key_fallback, val, val_len, h, 0); /* ref 过期或尚无值:退回完整按 key 写(含 extindex 检查),成功后重解析。 */ int rc = shm_set_validated(kv, key_fallback, val, val_len, &head); if (rc == 0) @@ -3171,6 +3276,8 @@ int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, if (kv_sync(kv) != 0) return -1; char kbuf[1024]; + if (strlen(key) >= sizeof kbuf) + return -1; /* reject rather than truncating to another key */ uint8_t *raw; int32_t rl; int fetched = 0; @@ -3178,8 +3285,7 @@ int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, if (resolve) h = resolve_fetch(kv, key, kbuf, sizeof(kbuf), &raw, &rl, &fetched); else { - strncpy(kbuf, key, sizeof(kbuf) - 1); - kbuf[sizeof(kbuf) - 1] = '\0'; + strcpy(kbuf, key); h = art_search(kv, kv->hdr->art_root, (const uint8_t *)kbuf, (int)strlen(kbuf)); } @@ -3188,8 +3294,10 @@ int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, uint64_t hv = art_val_load(h); if (!AV_HAS(hv)) return -1; - if ((!fetched && read_value(kv, AV_OFF(hv), &raw, &rl) < 0) || rl <= 0) - return -1; /* None 或读失败 → 强制走 NewPlace */ + /* Re-bound by physical box capacity before returning writable pointer. */ + (void)fetched; + if (read_value(kv, AV_OFF(hv), &raw, &rl) < 0 || rl <= 0) + return -1; /* None or read failure -> require WriteNewPlace */ kvspaceXvalueHead wire; if (head_of(raw, rl, &wire) == 0) { if (kvspaceXvalueHeadLen(&wire) != (uint64_t)body_len) @@ -3232,10 +3340,16 @@ int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, free(value); return -1; } - /* 固定头 8 字节:body 偏移恒 = 8;langtype 尾部 @8+body_len 整体单写。 */ - uint32_t headlen = KVSPACE_XVALUE_HEAD_LEN; - (void)ptr; - int rc = shm_set_raw(kv, key, value, (int32_t)total); + /* Reserve produces an inline header; patch ptr byte for ref=1/2. */ + kvspaceXvalueHead hd; + memcpy(&hd, value, sizeof hd); + hd.ptr = ptr; + memcpy(value, &hd, sizeof hd); + /* Fixed 8-byte header (body offset = 8) + attr at 8+body_len in a single write. + * Reserve physical box capacity (8 + body_cap + attr) so body can grow in place. */ + uint64_t attr_len = total - KVSPACE_XVALUE_HEAD_LEN - (uint64_t)body_len; + size_t reserve = (size_t)(KVSPACE_XVALUE_HEAD_LEN + body_cap + attr_len); + int rc = shm_set_raw(kv, key, value, (int32_t)total, reserve); free(value); if (rc != 0) return -1; @@ -3246,23 +3360,21 @@ int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, uint64_t nv = art_val_load(node); if (!AV_HAS(nv)) return -1; - *body = kv->sbo_data + AV_OFF(nv) + headlen; + *body = kv->sbo_data + AV_OFF(nv) + KVSPACE_XVALUE_HEAD_LEN; return 0; } -int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool ex, int resolve, - int32_t *out_count) { - char **names; - int32_t count; - if (kvspaceShmList(kv, prefix, ex, resolve, &names, &count) != 0) { - *out_count = 0; +/* Delete a single key directly (without following links) and without bumping + * list_gen. Returns 1 = deleted, 0 = missing, -1 = hard error (tree unchanged). + * Caller has called kv_sync and verified strlen(key) < 1024. */ +static int shm_del_key(kvspace_t *kv, const char *key) { + bool d = false; + int32_t root = art_del(kv, kv->hdr->art_root, (const uint8_t *)key, + (int)strlen(key), 0, &d); + if (root < 0) return -1; - } - for (int32_t i = 0; i < count; i++) - free(names[i]); - free(names); - *out_count = count; - return 0; + kv->hdr->art_root = root; + return d ? 1 : 0; } int kvspaceShmDel(kvspace_t *kv, const char *key) { @@ -3270,81 +3382,12 @@ int kvspaceShmDel(kvspace_t *kv, const char *key) { return -1; if (kv_sync(kv) != 0) return -1; - char kbuf[1024]; - strncpy(kbuf, key, sizeof(kbuf) - 1); - kbuf[sizeof(kbuf) - 1] = '\0'; /* 删键本身,不穿透 link——显式解引用由 runtime 掌控 */ - bool d = false; - int32_t root = art_del(kv, kv->hdr->art_root, (const uint8_t *)kbuf, - (int)strlen(kbuf), 0, &d); - if (root < 0) - return -1; /* 表满等硬错误:树未变 */ - kv->hdr->art_root = root; - return d ? 0 : -1; -} - -int kvspaceShmDeltree(kvspace_t *kv, const char *prefix) { - if (!kv || !prefix || reserved_meta_path(prefix)) - return -1; - if (kv_sync(kv) != 0) + if (strlen(key) >= 1024) + return -1; /* Reject instead of truncating into another key */ + /* Delete the key itself without following links; explicit deref belongs to runtime */ + if (shm_del_key(kv, key) != 1) return -1; - // if prefix itself is a link, only delete the link - art_hdr_t *h = art_search(kv, kv->hdr->art_root, (const uint8_t *)prefix, - (int)strlen(prefix)); - uint64_t hv = h ? art_val_load(h) : 0; - if (h && AV_HAS(hv)) { - uint8_t *raw; - int32_t rl; - read_value(kv, AV_OFF(hv), &raw, &rl); - kvspaceXvalueHead wire; - int is_ptr = rl > 0 && head_of(raw, rl, &wire) == 0 && - wire.ptr == KVSPACE_XVALUE_PTR_PTR; - if (is_ptr) { - return kvspaceShmDel(kv, prefix); - } - } - char *e = edir(prefix); // ensure trailing / for listing children - char **ns; - int32_t nc; - kvspaceShmList(kv, e, false, 1, &ns, &nc); - for (int i = 0; i < nc; i++) { - char *c = pjoin(e, ns[i]); - kvspaceShmDeltree(kv, c); - free(c); - } - for (int i = 0; i < nc; i++) - free(ns[i]); - free(ns); - - /* 成员目录 marker(prefix·,U+00B7):成员 key = - prefix·,直接拼接后递归删除。 slash 版 e=prefix/ 覆盖不到 · - 成员(json 的 stringkeymap 落盘形态)。 */ - size_t pl = strlen(prefix); - char *m = malloc(pl + 3); - memcpy(m, prefix, pl); - m[pl] = (char)0xC2; - m[pl + 1] = (char)0xB7; - m[pl + 2] = 0; - char **ms; - int32_t mc; - kvspaceShmList(kv, m, false, 1, &ms, &mc); - for (int i = 0; i < mc; i++) { - size_t ml = strlen(m), nl = strlen(ms[i]); - char *c = malloc(ml + nl + 1); - memcpy(c, m, ml); - memcpy(c + ml, ms[i], nl + 1); - kvspaceShmDeltree(kv, c); - free(c); - } - for (int i = 0; i < mc; i++) - free(ms[i]); - free(ms); - kvspaceShmDel(kv, m); - free(m); - - kvspaceShmDel(kv, prefix); - if (strcmp(e, prefix) != 0) - kvspaceShmDel(kv, e); - free(e); + list_gen_bump(kv); /* Key disappeared: bump after CAS */ return 0; } @@ -3359,6 +3402,89 @@ static char *memdir(const char *p) { return r; } +/* Whether key belongs to base's subtree: base itself, or base followed by '/' + * (hierarchy), U+00B7 (member), or U+2025 (runtime/attr key). Empty base = root + * (all keys starting with '/'). Matches kvspace-durable store::is_subtree_key. */ +static int subtree_key(const char *base, size_t bl, const char *key) { + if (bl == 0) + return key[0] == '/'; + if (strncmp(key, base, bl) != 0) + return 0; + const unsigned char *r = (const unsigned char *)key + bl; + return r[0] == 0 || r[0] == '/' || (r[0] == 0xC2 && r[1] == 0xB7) || + (r[0] == 0xE2 && r[1] == 0x80 && r[2] == 0xA5); +} + +static int is_ptr_key(kvspace_t *kv, const char *key) { + art_hdr_t *h = art_search(kv, kv->hdr->art_root, (const uint8_t *)key, + (int)strlen(key)); + uint64_t hv = h ? art_val_load(h) : 0; + if (!h || !AV_HAS(hv)) + return 0; + uint8_t *raw = NULL; + int32_t rl = 0; + kvspaceXvalueHead wire; + return read_value(kv, AV_OFF(hv), &raw, &rl) == 0 && rl > 0 && + head_of(raw, rl, &wire) == 0 && wire.ptr == KVSPACE_XVALUE_PTR_PTR; +} + +/* Delete the entire subtree under prefix. A single trailing '/' or U+00B7 on + * prefix normalizes to base ("/d/" and "/d" both delete "/d"; "/" is root). + * Deletes base, base/..., base..., and base... (including hidden + * U+2025 runtime keys). Does not follow links (POSIX rm semantics): if prefix + * or base is a pointer, only the pointer is deleted. */ +int kvspaceShmDeltree(kvspace_t *kv, const char *prefix) { + if (!kv || !prefix || reserved_meta_path(prefix)) + return -1; + if (kv_sync(kv) != 0) + return -1; + size_t pl = strlen(prefix); + if (pl >= 1024) + return -1; /* Reject instead of truncating into another prefix */ + char base[1024]; + memcpy(base, prefix, pl + 1); + size_t bl = pl; + if (strcmp(prefix, "/") == 0) + bl = 0; + else if (bl && base[bl - 1] == '/') + bl--; + else if (bl >= 2 && (unsigned char)base[bl - 2] == 0xC2 && + (unsigned char)base[bl - 1] == 0xB7) + bl -= 2; + base[bl] = 0; + if (bl && reserved_meta_path(base)) + return -1; + /* prefix itself is a link: delete only the link. */ + if (pl && is_ptr_key(kv, prefix)) + return kvspaceShmDel(kv, prefix); + if (bl && bl != pl && is_ptr_key(kv, base)) { + int rc = kvspaceShmDel(kv, base); + kvspaceShmDel(kv, prefix); /* Companion key such as "/p/", may not exist */ + return rc; + } + keyscan_t keys = {0}; + if (art_scan_keys(kv, bl ? base : "/", bl ? (int)bl : 1, &keys) != 0) { + keyscan_free(&keys); + return -1; + } + int rc = 0, any = 0; + for (int32_t i = 0; i < keys.count; i++) { + const char *k = keys.keys[i]; + if (!subtree_key(base, bl, k) || reserved_meta_path(k) || + strlen(k) >= 1024) + continue; + int d = shm_del_key(kv, k); + if (d < 0) + rc = -1; + else if (d) + any = 1; + } + keyscan_free(&keys); + if (any) + list_gen_bump(kv); /* Keys disappeared: bump once after batch delete */ + return rc; +} + /* Copy one value before Set can move its slab. */ int kvspaceShmCp(kvspace_t *kv, const char *src, const char *dst) { if (!kv || !src || !dst) @@ -3556,43 +3682,51 @@ static int same_child(const char *a, const char *b) { return na == nb && memcmp(a, b, na) == 0; } -int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, - char ***on, int32_t *oc) { - if (!kv || !prefix || !on || !oc) - return -1; - *on = NULL; - *oc = 0; +/* Resolve List input: prefix -> target directory (following links when resolve=1) + * and mounted extindex source ext (when ex=1 and mounted; "" otherwise). + * Returns 0 = scan, 1 = reserved path (empty result), -1 = error. + * target buffer >= 4096 bytes, ext buffer >= 1024 bytes. */ +static int list_target(kvspace_t *kv, const char *prefix, bool ex, int resolve, + char *target, size_t tcap, char *ext, size_t ecap) { + ext[0] = 0; if (reserved_meta_path(prefix)) - return 0; + return 1; if (kv_sync(kv) != 0 || bad_dir_prefix(prefix)) return -1; - const char *pfx = prefix; - char tbuf[1024]; if (resolve) { - resolve_path(kv, prefix, tbuf, sizeof tbuf); - pfx = tbuf; + if (strlen(prefix) >= 1024) + return -1; /* Reject instead of truncating into another dir */ + resolve_path(kv, prefix, target, 1024); + } else { + size_t l = strlen(prefix); + if (l >= tcap) + return -1; + memcpy(target, prefix, l + 1); } - if (reserved_meta_path(pfx)) - return 0; + if (reserved_meta_path(target)) + return 1; + if (ex) { + char *dir = edir(target); + if (!dir) + return -1; + if (!dir_ext_path(kv, dir, ext, (int)ecap)) + ext[0] = 0; + free(dir); + } + return 0; +} +/* Scan direct children of target (and mounted ext): sort, deduplicate (prefer + * directory entry on name collision), and hide runtime entries. */ +static int list_scan(kvspace_t *kv, const char *pfx, const char *ext, + char ***on, int32_t *oc) { keyscan_t children = {0}; char buf[4096]; if (kv->hdr->art_root >= 0 && art_scan_pfx(kv, pfx, (int)strlen(pfx), buf, (int)sizeof buf, &children) != 0) goto fail; - - if (ex) { - char extpath[1024]; - char *dir = edir(pfx); - if (!dir) - goto fail; - int has_ext = dir_ext_path(kv, dir, extpath, sizeof extpath); - free(dir); - if (has_ext) { - if (art_scan_pfx(kv, extpath, (int)strlen(extpath), buf, - (int)sizeof buf, &children) != 0) - goto fail; - } - } + if (ext[0] && art_scan_pfx(kv, ext, (int)strlen(ext), buf, (int)sizeof buf, + &children) != 0) + goto fail; if (children.count > 1) qsort(children.keys, (size_t)children.count, sizeof *children.keys, child_cmp); @@ -3603,20 +3737,10 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, free(children.keys[i]); continue; } - /* 成员名含 ‥(\xE2\x80\xA5)是系统变量/attr key(`0‥langtype` 等), - List 时隐藏——对齐 durable 后端(RUNTIME_MEMBER_SEP 隐藏)。 */ - { - const char *p = children.keys[i]; - for (; *p; p++) { - if ((unsigned char)p[0] == 0xE2 && (unsigned char)p[1] == 0x80 && - (unsigned char)p[2] == 0xA5) { - free(children.keys[i]); - goto hidden; - } - } - } - if (0) { -hidden: + /* Child names containing U+2025 (\xE2\x80\xA5) are runtime system keys + (e.g. status, pc); hide from List to match the durable backend. */ + if (strstr(children.keys[i], "\xE2\x80\xA5")) { + free(children.keys[i]); continue; } if (unique && same_child(children.keys[unique - 1], children.keys[i])) { @@ -3634,17 +3758,126 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, *on = children.keys; *oc = children.count; return 0; - fail: keyscan_free(&children); return -1; } - +int kvspaceShmList(kvspace_t *kv, const char *prefix, bool ex, int resolve, + char ***on, int32_t *oc) { + if (!kv || !prefix || !on || !oc) + return -1; + *on = NULL; + *oc = 0; + char target[4096], ext[1024]; + int rc = list_target(kv, prefix, ex, resolve, target, sizeof target, ext, + sizeof ext); + if (rc != 0) + return rc > 0 ? 0 : -1; + return list_scan(kv, target, ext, on, oc); +} +/* Caller holds lc_mtx (or handle is unshared, e.g. Close). */ +static void list_cache_drop(kvspace_t *kv) { + for (int32_t i = 0; i < kv->lc_count; i++) + free(kv->lc_names[i]); + free(kv->lc_names); + free(kv->lc_prefix); + free(kv->lc_target); + free(kv->lc_ext); + kv->lc_names = NULL; + kv->lc_prefix = kv->lc_target = kv->lc_ext = NULL; + kv->lc_count = 0; +} +/* Cached name list for ListLen/ListAt; caller holds lc_mtx. On return 0, + * kv->lc_names/lc_count hold the result. A hit requires matching (prefix, ex, + * resolve), unchanged resolved target/ext, and unchanged list_gen (read before + * resolve/scan so any concurrent key-set mutation forces a rescan next time). */ +static int list_cached(kvspace_t *kv, const char *prefix, bool ex, int resolve) { + uint64_t g = __atomic_load_n(&kv->hdr->list_gen, __ATOMIC_ACQUIRE); + char target[4096], ext[1024]; + int rc = list_target(kv, prefix, ex, resolve, target, sizeof target, ext, + sizeof ext); + if (rc < 0) + return -1; + if (rc > 0) { + list_cache_drop(kv); + return 0; + } + if (kv->lc_prefix && kv->lc_gen == g && kv->lc_ex == (uint8_t)ex && + kv->lc_res == (uint8_t)(resolve != 0) && + strcmp(kv->lc_prefix, prefix) == 0 && + strcmp(kv->lc_target, target) == 0 && strcmp(kv->lc_ext, ext) == 0) + return 0; + list_cache_drop(kv); + char **names = NULL; + int32_t count = 0; + if (list_scan(kv, target, ext, &names, &count) != 0) + return -1; + char *p = strdup(prefix), *t = strdup(target), *e = strdup(ext); + if (!p || !t || !e) { + free(p); + free(t); + free(e); + for (int32_t i = 0; i < count; i++) + free(names[i]); + free(names); + return -1; + } + kv->lc_prefix = p; + kv->lc_target = t; + kv->lc_ext = e; + kv->lc_names = names; + kv->lc_count = count; + kv->lc_ex = (uint8_t)ex; + kv->lc_res = (uint8_t)(resolve != 0); + kv->lc_gen = g; + return 0; +} +int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool ex, int resolve, + int32_t *out_count) { + if (!out_count) + return -1; + *out_count = 0; + if (!kv || !prefix) + return -1; + pthread_mutex_lock(&kv->lc_mtx); + int rc = list_cached(kv, prefix, ex, resolve); + if (rc == 0) + *out_count = kv->lc_count; + pthread_mutex_unlock(&kv->lc_mtx); + return rc; +} +/* Copy the idx-th direct child name into buf (without NUL). *out_len is set to + * the name length; returns non-zero if out of range or buf is too small. Shares + * cache with ListLen so ListLen + indexed ListAt scans the directory once. */ +int kvspaceShmListAt(kvspace_t *kv, const char *prefix, bool ex, int resolve, + int32_t idx, uint8_t *buf, uint32_t buf_cap, + uint32_t *out_len) { + if (!out_len) + return -1; + *out_len = 0; + if (!kv || !prefix) + return -1; + pthread_mutex_lock(&kv->lc_mtx); + int rc = list_cached(kv, prefix, ex, resolve); + if (rc == 0) { + rc = -1; + if (idx >= 0 && idx < kv->lc_count) { + size_t l = strlen(kv->lc_names[idx]); + *out_len = (uint32_t)l; + if (buf && l <= buf_cap) { + memcpy(buf, kv->lc_names[idx], l); + rc = 0; + } + } + } + pthread_mutex_unlock(&kv->lc_mtx); + return rc; +} int kvspaceShmExtindex(kvspace_t *kv, const char *p, const char *ep, char *err, uint32_t err_cap) { if (!kv || !p || !ep) return -1; - /* 对齐 durable ext_index:path 与 ext_path 必须是目录形键(/ 或 · 结尾)。 */ + /* Match durable ext_index: path and ext_path must be directory keys ('/' or U+00B7 suffix). */ if (bad_dir_prefix(p) || bad_dir_prefix(ep)) { if (err && err_cap) snprintf(err, err_cap, @@ -3675,7 +3908,8 @@ int kvspaceShmExtindex(kvspace_t *kv, const char *p, const char *ep, } if (ltl > (uint16_t)(sizeof lt - 1)) ltl = (uint16_t)(sizeof lt - 1); - memcpy(lt, ltd, ltl); + if (ltl) + memcpy(lt, ltd, ltl); lt[ltl] = 0; uint8_t *v = NULL; uint64_t vl = 0; diff --git a/src/kvspace_shm.h b/src/kvspace_shm.h index 68f7884..1a48e85 100644 --- a/src/kvspace_shm.h +++ b/src/kvspace_shm.h @@ -5,8 +5,10 @@ * 对齐 kvspace-go KVSpace 接口. */ -#ifndef KVSPACE_H -#define KVSPACE_H +/* Guard must not be KVSPACE_H: that is the canonical ABI header + * guard, and colliding would silently skip whichever header is included second. */ +#ifndef KVSPACE_SHM_H +#define KVSPACE_SHM_H #include "coord.h" #include @@ -15,9 +17,13 @@ typedef struct kvspace kvspace_t; -/* shm store 格式版本:4 = ART val 原子字 + 垃圾环 + 单写者降级 + 8B 节点对齐。 - * 不兼容旧版本:open 硬失败并提示重建(不迁移)。 */ -#define KVSPACE_FORMAT_VERSION 5 +/* SHM store format version: + * 4 = ART atomic val word + garbage ring + single-writer demotion + 8B node align; + * 5 = ART 112-byte block; + * 6 = XValue fixed 8-byte header + inline attr region (#566/#571, incompatible + * with format 5) plus header list_gen for cross-process List cache invalidation. + * Incompatible older versions fail hard on open with a rebuild hint (no migration). */ +#define KVSPACE_FORMAT_VERSION 6 /* 对齐 kvspace/include/kvspace/kvspace.h:叶子 + 目录祖先。 */ typedef struct { @@ -58,9 +64,8 @@ uint8_t *kvspaceShmGetByRef(kvspace_t *kv, kvspaceRef_t *ref, /* shm slot view:返回映射内 head/body 与整 box 物理容量。0=成功,-1=不可视槽。 */ int kvspaceShmXvalueBodyView(kvspace_t *kv, const kvspaceRef_t *ref, const char *key_fallback, kvspaceXvalueBodyView_t *out); -/* 定位写的前置条件:只允许改 body [8, 8+len),head 与 attr 一律不可写。 */ -int kvspaceShmPartInBody(const uint8_t *raw, int32_t total, uint32_t offset, - uint32_t buf_len); +int kvspaceShmSetPart(kvspace_t *kv, const char *key, uint32_t offset, + const uint8_t *buf, uint32_t buf_len); int kvspaceShmSetPartByRef(kvspace_t *kv, kvspaceRef_t *ref, const char *key_fallback, uint32_t offset, const uint8_t *buf, uint32_t buf_len); @@ -80,15 +85,21 @@ int kvspaceShmSetValueByRef(kvspace_t *kv, kvspaceRef_t *ref, // 覆写);否则返回非 0。 int kvspaceShmWriteInPlace(kvspace_t *kv, const char *key, int resolve, int32_t body_len, uint8_t **body); -// WriteNewPlace: 按 (ref, storetype, body_len, langtype) 分配新 box、写固定头 + -// langtype 尾部(整体单写)、维护父索引,返回 body 指针。 +// WriteNewPlace: allocate a box for (ref, storetype, body_len, body_cap, langtype) +// with physical capacity >= 8 + body_cap + attr, write fixed header (ptr from ref) +// and attr region in one atomic store, update parent index, and return body pointer. int kvspaceShmWriteNewPlace(kvspace_t *kv, const char *key, uint8_t ref, uint8_t storetype, int32_t body_len, uint64_t body_cap, const char *langtype, uint8_t **body); -// ListLen: 只返回前缀下子项计数,无缓冲。 +// ListLen: direct child count under prefix. ListAt: copy idx-th child name into buf. +// Both share a per-handle cache so ListLen + indexed ListAt scans the directory once, +// invalidating on key-set, link-target, or extindex changes (including cross-process). int kvspaceShmListLen(kvspace_t *kv, const char *prefix, bool expand_ext, int resolve, int32_t *out_count); +int kvspaceShmListAt(kvspace_t *kv, const char *prefix, bool expand_ext, + int resolve, int32_t idx, uint8_t *buf, uint32_t buf_cap, + uint32_t *out_len); /* ================================================================ * 目录操作 @@ -99,6 +110,9 @@ int kvspaceShmList(kvspace_t *kv, const char *prefix, bool expand_ext, int resolve, char ***out_names, int32_t *out_count); int kvspaceShmDel(kvspace_t *kv, const char *key); +// Deltree: delete base subtree (normalizing one trailing '/' or U+00B7 on prefix +// to base; "/" = root): base plus base/..., base..., and base... +// (including hidden U+2025 keys). Deletes only the pointer if prefix/base is a link. int kvspaceShmDeltree(kvspace_t *kv, const char *prefix); int kvspaceShmCp(kvspace_t *kv, const char *src, const char *dst); // 单 key 拷贝 @@ -161,6 +175,5 @@ int64_t kvspaceShmArtAlignCheck(kvspace_t *kv); void kvspaceShmTestSetFormatVersion(kvspace_t *kv, uint32_t v); #endif int kvspaceShmViewSweep(kvspace_t *kv); -int kvspaceShmViewFlushTomb(kvspace_t *kv); #endif diff --git a/src/xvalue_head.c b/src/xvalue_head.c index 24ed5f3..7deed6d 100644 --- a/src/xvalue_head.c +++ b/src/xvalue_head.c @@ -3,17 +3,26 @@ #include #include -/* V2 最终态(#571):head 固定 8 字节 [len:u48][storetype:u8][ptr:u8], - body 偏移恒 = 8;attr 区 @8+len(长度自描述,含 langtype 记录)。整体单 key - 一次写,天然原子——无 attr key。langtype 空 = 无记录(空 attr)。 */ +/* V2 final layout (#571): fixed 8-byte header [len:u48 LE@0-5][storetype:u8@6][ptr:u8@7], + body always at offset 8, self-delimited attr region at 8+len (carrying langtype). + Single-key atomic write — no separate attr key. Empty langtype = empty attr. */ #define UINT48_MAX UINT64_C(0xFFFFFFFFFFFF) +/* Failure helper: clear *out per header contract. */ +static int fail(uint8_t **out) { + if (out) + *out = NULL; + return -1; +} + +/* Single wire builder. Zero-fills body when body is NULL (Reserve placeholder); + public entry points validate whether non-zero len requires non-NULL body. */ static int build(uint8_t storetype, uint8_t ptr, const uint8_t *body, uint64_t len, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (!out || !out_len || (len && !body)) - return -1; + if (!out || !out_len) + return fail(out); *out = NULL; if (storetype > KVSPACE_XVALUE_ST_EXT || ptr > KVSPACE_XVALUE_PTR_EXT) return -1; @@ -32,7 +41,7 @@ static int build(uint8_t storetype, uint8_t ptr, const uint8_t *body, kvspaceXvalueHead h = {.storetype = storetype, .ptr = ptr}; kvspaceXvalueHeadSetLen(&h, len); memcpy(p, &h, sizeof h); - if (len) + if (len && body) memcpy(p + KVSPACE_XVALUE_HEAD_LEN, body, len); uint8_t *a = p + KVSPACE_XVALUE_HEAD_LEN + len; a[0] = (uint8_t)alen; @@ -57,7 +66,7 @@ int kvspaceXvalueHeadNewScalar(uint8_t storetype, const uint8_t *raw, uint32_t raw_len, const char *langtype, uint8_t **out, uint64_t *out_len) { if (raw_len && !raw) - return -1; + return fail(out); return build(storetype, KVSPACE_XVALUE_PTR_INLINE, raw, raw_len, langtype, out, out_len); } @@ -65,9 +74,8 @@ int kvspaceXvalueHeadNewScalar(uint8_t storetype, const uint8_t *raw, int kvspaceXvalueHeadNewSlack(const uint8_t *data, uint64_t data_len, uint64_t cap, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (cap < data_len || data_len > UINT48_MAX || (data_len && !data)) - return -1; - (void)cap; + if (cap < data_len || (data_len && !data)) + return fail(out); return build(KVSPACE_XVALUE_ST_SLACK, KVSPACE_XVALUE_PTR_INLINE, data, data_len, langtype, out, out_len); } @@ -76,12 +84,9 @@ int kvspaceXvalueHeadNewTensor(const uint64_t *dims, uint32_t ndim, const uint8_t *raw, uint64_t raw_len, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (!dims || ndim == 0 || ndim > 64 || raw_len > UINT48_MAX || - (raw_len && !raw)) - return -1; - (void)dims; - (void)ndim; - /* 形状在 langtype 尾部;body 字节数 = raw_len(明文层校验 numel×width) */ + /* Shape lives in leading [dims] of langtype; body byte length is raw_len. */ + if (!dims || ndim == 0 || ndim > 64 || (raw_len && !raw)) + return fail(out); return build(KVSPACE_XVALUE_ST_LARGE, KVSPACE_XVALUE_PTR_INLINE, raw, raw_len, langtype, out, out_len); } @@ -90,7 +95,7 @@ int kvspaceXvalueHeadNewPtr(const uint8_t *path, uint32_t path_len, const char *langtype, uint8_t **out, uint64_t *out_len) { if (!path_len || !path) - return -1; + return fail(out); return build(KVSPACE_XVALUE_ST_SLACK, KVSPACE_XVALUE_PTR_PTR, path, path_len, langtype, out, out_len); } @@ -99,7 +104,7 @@ int kvspaceXvalueHeadNewExt(const uint8_t *locator, uint32_t locator_len, const char *langtype, uint8_t **out, uint64_t *out_len) { if (!locator_len || !locator) - return -1; + return fail(out); return build(KVSPACE_XVALUE_ST_EXT, KVSPACE_XVALUE_PTR_EXT, locator, locator_len, langtype, out, out_len); } @@ -107,47 +112,17 @@ int kvspaceXvalueHeadNewExt(const uint8_t *locator, uint32_t locator_len, int kvspaceXvalueHeadNewCode(const uint8_t *body, uint64_t body_len, uint64_t cap, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (body_len > cap || body_len > UINT48_MAX || (body_len && !body)) - return -1; - (void)cap; - return build(KVSPACE_XVALUE_ST_SLACK, KVSPACE_XVALUE_PTR_INLINE, body, - body_len, langtype, out, out_len); + return kvspaceXvalueHeadNewSlack(body, body_len, cap, langtype, out, + out_len); } int kvspaceXvalueHeadReserve(uint8_t storetype, uint64_t body_len, uint64_t cap, const char *langtype, uint8_t **out, uint64_t *out_len) { - if (body_len > UINT48_MAX || cap < body_len) - return -1; /* 固定头无容量字段:预留 = 产 body_len 字节零 body 的占位(后由调用方填充), attr 区(含 langtype 记录)一并写入。 */ - (void)cap; - uint64_t ltl = langtype ? strlen(langtype) : 0; - uint64_t alen = 2 + (ltl ? 3 + ltl : 0); - if (alen > UINT16_MAX) - return -1; - if (body_len > SIZE_MAX - KVSPACE_XVALUE_HEAD_LEN - alen) - return -1; - uint64_t n = (uint64_t)KVSPACE_XVALUE_HEAD_LEN + body_len + alen; - if (n > SIZE_MAX) - return -1; - uint8_t *p = (uint8_t *)calloc(1, (size_t)n); - if (!p) - return -1; - kvspaceXvalueHead h = {.storetype = storetype, - .ptr = KVSPACE_XVALUE_PTR_INLINE}; - kvspaceXvalueHeadSetLen(&h, body_len); - memcpy(p, &h, sizeof h); - uint8_t *a = p + KVSPACE_XVALUE_HEAD_LEN + body_len; - a[0] = (uint8_t)alen; - a[1] = (uint8_t)(alen >> 8); - if (ltl) { - a[2] = KVSPACE_XVALUE_ATTR_TAG_LANGTYPE; - a[3] = (uint8_t)ltl; - a[4] = (uint8_t)(ltl >> 8); - memcpy(a + 5, langtype, ltl); - } - *out = p; - *out_len = n; - return 0; + if (cap < body_len) + return fail(out); + return build(storetype, KVSPACE_XVALUE_PTR_INLINE, NULL, body_len, + langtype, out, out_len); } diff --git a/src/xvalue_head.h b/src/xvalue_head.h index 4c56f92..3b12a12 100644 --- a/src/xvalue_head.h +++ b/src/xvalue_head.h @@ -26,8 +26,8 @@ extern "C" { #define KVSPACE_XVALUE_PTR_PTR 1u #define KVSPACE_XVALUE_PTR_EXT 2u -/* 固定 8 字节布局(直接 memcpy struct + body)。 - wire:len:u48@0-5(小端)、storetype@6、ptr@7、body@8、langtype@8+len。 */ +/* Fixed 8-byte layout (direct memcpy struct + body). + wire: len:u48@0-5 (little-endian), storetype@6, ptr@7, body@8. */ typedef struct kvspaceXvalueHead { uint8_t len[6]; /* u48 小端(offset 0-5) */ uint8_t storetype; @@ -51,14 +51,18 @@ static inline void kvspaceXvalueHeadSetLen(kvspaceXvalueHead *h, uint64_t v) { /* attr 区常量:record tag 1 = langtype;2+ 预留扩展(ro/vid 等,未实现)。 */ #define KVSPACE_XVALUE_ATTR_TAG_LANGTYPE 1u -/* 读 attr 总长(含自身 2 字节;空 attr = 2)。 */ +/* Read total attr length (including its own 2-byte length prefix; empty attr = 2). + Unbounded: caller must ensure wire has at least 8+body_len+2 bytes; use + kvspaceXvalueHeadLangtypeBounded for untrusted or backend-borrowed inputs. */ static inline uint16_t kvspaceXvalueHeadAttrLen(const uint8_t *wire, uint64_t body_len) { const uint8_t *p = wire + KVSPACE_XVALUE_HEAD_LEN + body_len; return (uint16_t)((uint16_t)p[0] | ((uint16_t)p[1] << 8)); } -/* 扫 attr records 取 tag=1(langtype)记录 data;未找到/坏 attr → 返回 0。 */ +/* Scan attr records for tag=1 (langtype) data; returns 0 if missing or malformed. + Unbounded: caller must ensure wire has at least 8+body_len+attr_len bytes; use + kvspaceXvalueHeadLangtypeBounded when bounds are not yet verified. */ static inline uint16_t kvspaceXvalueHeadLangtype(const uint8_t *wire, uint64_t body_len, const uint8_t **out) { @@ -82,8 +86,31 @@ static inline uint16_t kvspaceXvalueHeadLangtype(const uint8_t *wire, return 0; } -/* 构造(固定头 + body + attr 区,单 key 整体)。langtype 空/NULL = 无 langtype - 记录(空 attr = [attr_len:2])。0 on success. -1 and *out cleared on failure. */ +/* Bounded variant: verifies 8+len+2 <= wire_len and 8+len+attr_len <= wire_len + before scanning records. Returns 0 and sets *out=NULL on missing/malformed/truncated values. */ +static inline uint16_t kvspaceXvalueHeadLangtypeBounded(const uint8_t *wire, + uint64_t wire_len, + const uint8_t **out) { + *out = NULL; + if (!wire || wire_len < KVSPACE_XVALUE_HEAD_LEN + 2u) + return 0; + uint64_t body_len = 0; + for (int i = 5; i >= 0; i--) + body_len = (body_len << 8) | wire[i]; + const uint64_t room = wire_len - KVSPACE_XVALUE_HEAD_LEN; + if (body_len > room - 2u) + return 0; + if (kvspaceXvalueHeadAttrLen(wire, body_len) > room - body_len) + return 0; + return kvspaceXvalueHeadLangtype(wire, body_len, out); +} + +/* Constructors (fixed 8-byte header + body + attr region in a single value). + Empty/NULL langtype omits the langtype record (empty attr = [attr_len:2]). + Returns 0 on success, -1 and clears *out on failure (when out is non-NULL). + Fixed header has no capacity/shape fields: cap only checks cap >= len, and + dims/ndim only check non-empty (shape lives in leading [dims] of langtype). + NewCode is an alias of NewSlack (SLACK/INLINE) kept for ABI compatibility. */ int kvspaceXvalueHeadNewNone(uint8_t **out, uint64_t *out_len); int kvspaceXvalueHeadNewScalar(uint8_t storetype, const uint8_t *raw, uint32_t raw_len, const char *langtype, diff --git a/tests/test_abi_contract.c b/tests/test_abi_contract.c new file mode 100644 index 0000000..b9d5420 --- /dev/null +++ b/tests/test_abi_contract.c @@ -0,0 +1,522 @@ +/* ABI contract regressions for v0.3.23 review findings: + * 1) WriteNewPlace ref sets ptr byte (ref=1 link, ref=2 ext); + * 2) WriteNewPlace body_cap reserves physical box capacity (view.body_cap >= requested); + * 3) SetPart / SetPartByRef bounds checks reject u32 wraparound; + * 4) Keys >= 1024 bytes are rejected without truncating into shorter keys; + * 5) SetValue rejects values with missing or truncated attr regions; + * 6) ViewResolvePrefix stops cleanly at prefix_len==0 nodes (/vthread/1/ beside /vthread/10/); + * 7) ViewWrite accepts only equal-length writes without overwriting attr; + * 8) ListLen/ListAt cache invalidates immediately on key-set changes (including cross-process); + * 9) DSN scheme must be exact "shm"; + * 10) DelTree deletes full subtrees (including U+2025 keys and trailing '/' or U+00B7) + * without following pointer targets. */ +#define _GNU_SOURCE +#include +#include "test_util.h" +#include "xvalue_head.h" +#include +#include +#include +#include +#include + +static char g_dir[] = "/tmp/kvspace-abi-contract-XXXXXX"; + +static void *open_db(const char *name) { + char dsn[256]; + snprintf(dsn, sizeof dsn, "shm://%s/%s", g_dir, name); + return kvspaceConnect(dsn); +} + +static int set_i64(void *h, const char *key, int64_t v) { + uint8_t *val = NULL; + uint32_t n = 0; + if (kvspaceNewInt64(v, "int64", &val, &n) != 0) + return -1; + int rc = kvspaceSetValue(h, key, val, n, NULL, 0); + free(val); + return rc; +} + +static int set_chars(void *h, const char *key, const char *s) { + uint8_t *val = NULL; + uint32_t n = 0; + if (kvspaceNewChar((const uint8_t *)s, (uint32_t)strlen(s), "char/utf8", + &val, &n) != 0) + return -1; + int rc = kvspaceSetValue(h, key, val, n, NULL, 0); + free(val); + return rc; +} + +static uint8_t ptr_byte(void *h, const char *key) { + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, key, 0, &out, &n); + return out && n >= KVSPACE_XVALUE_HEAD_LEN ? out[7] : 0xFF; +} + +static void test_writenewplace_ref(void) { + void *h = open_db("ref"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/t", 7) == 0, "set /t"); + uint8_t *body = NULL; + REQUIRE(kvspaceWriteNewPlace(h, "/p", KVSPACE_REF_PTR, KVSPACE_STORETYPE_SLACK, + 2, 2, "int64", &body, NULL, 0) == 0 && body, + "WriteNewPlace ref=PTR"); + memcpy(body, "/t", 2); + CHECK(ptr_byte(h, "/p") == KVSPACE_XVALUE_PTR_PTR, "ref=PTR -> ptr byte %u", + ptr_byte(h, "/p")); + /* Link is dereferenced when resolve=1. */ + uint8_t *lnk = NULL, *tgt = NULL; + uint32_t ln = 0, tn = 0; + kvspaceGet(h, "/p", 1, &lnk, &ln); + kvspaceGet(h, "/t", 0, &tgt, &tn); + CHECK(lnk && tgt && ln == tn && memcmp(lnk, tgt, tn) == 0, + "resolve=1 follows the WriteNewPlace link"); + + REQUIRE(kvspaceWriteNewPlace(h, "/e", KVSPACE_REF_EXT, KVSPACE_STORETYPE_EXT, 5, + 5, "", &body, NULL, 0) == 0 && body, + "WriteNewPlace ref=EXT"); + memcpy(body, "s3://", 5); + CHECK(ptr_byte(h, "/e") == KVSPACE_XVALUE_PTR_EXT, "ref=EXT -> ptr byte %u", + ptr_byte(h, "/e")); + CHECK(ptr_byte(h, "/t") == KVSPACE_XVALUE_PTR_INLINE, "inline stays inline"); + /* Invalid (ref, storetype) combination is still rejected. */ + CHECK(kvspaceWriteNewPlace(h, "/bad", KVSPACE_REF_PTR, + KVSPACE_STORETYPE_FIXED_SMALL, 8, 8, "", &body, + NULL, 0) != 0, + "ref=PTR + FIXED_SMALL rejected"); + kvspaceClose(h); +} + +static void test_writenewplace_body_cap(void) { + void *h = open_db("cap"); + REQUIRE(h, "connect"); + uint8_t *body = NULL; + REQUIRE(kvspaceWriteNewPlace(h, "/pc", KVSPACE_REF_INLINE, + KVSPACE_STORETYPE_SLACK, 10, 1024, "", &body, + NULL, 0) == 0 && body, + "WriteNewPlace cap=1024"); + memcpy(body, "0123456789", 10); + kvspaceRef_t ref; + REQUIRE(kvspaceResolveRef(h, "/pc", &ref) == 0, "resolve /pc"); + kvspaceXvalueBodyView_t v; + REQUIRE(kvspaceXvalueBodyView(h, &ref, "/pc", &v) == 0, "body view"); + CHECK(v.body_cap >= 1024, "view.body_cap=%llu, want >= 1024", + (unsigned long long)v.body_cap); + /* Logical body length remains 10 bytes on readback. */ + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/pc", 0, &out, &n); + CHECK(out && n == 8 + 10 + 2 && memcmp(out + 8, "0123456789", 10) == 0, + "value reads back (len=%u)", n); + /* Rewriting within reserved capacity reuses the existing box in place. */ + uint8_t *body2 = NULL; + REQUIRE(kvspaceWriteNewPlace(h, "/pc", KVSPACE_REF_INLINE, + KVSPACE_STORETYPE_SLACK, 20, 1024, "", &body2, + NULL, 0) == 0 && body2, + "WriteNewPlace again"); + CHECK(body2 == body, "box reused when reserve already fits"); + /* body_cap < body_len is still rejected. */ + CHECK(kvspaceWriteNewPlace(h, "/x", KVSPACE_REF_INLINE, KVSPACE_STORETYPE_SLACK, + 10, 5, "", &body, NULL, 0) != 0, + "body_cap < body_len rejected"); + kvspaceClose(h); +} + +static void test_setpart_overflow(void) { + void *h = open_db("part"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/v", 1) == 0, "set /v"); + uint8_t buf[32]; + memset(buf, 0xAB, sizeof buf); + /* 0xFFFFFFF0 + 32 wraps to 16 in u32; must be rejected without OOB write. */ + CHECK(kvspaceSetPart(h, "/v", 0xFFFFFFF0u, buf, 32, NULL, 0) != 0, + "SetPart wrapped range rejected"); + kvspaceRef_t ref; + REQUIRE(kvspaceResolveRef(h, "/v", &ref) == 0, "resolve /v"); + CHECK(kvspaceSetPartByRef(h, &ref, "/v", 0xFFFFFFF0u, buf, 32, NULL, 0) != 0, + "SetPartByRef wrapped range rejected"); + /* Valid range succeeds. */ + uint8_t nine[8] = {9, 0, 0, 0, 0, 0, 0, 0}; + CHECK(kvspaceSetPart(h, "/v", 8, nine, 8, NULL, 0) == 0, "SetPart in range"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/v", 0, &out, &n); + CHECK(out && out[8] == 9, "in-range SetPart landed"); + kvspaceClose(h); +} + +static void test_long_key(void) { + void *h = open_db("long"); + REQUIRE(h, "connect"); + char k[1024], longk[1100]; + k[0] = '/'; + memset(k + 1, 'a', 1022); + k[1023] = 0; /* 1023 bytes: longest key accepted by Set */ + snprintf(longk, sizeof longk, "%s/other", k); + REQUIRE(set_i64(h, k, 7) == 0, "set 1023-byte key"); + CHECK(set_i64(h, longk, 8) != 0, "Set rejects >= 1024-byte key"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, longk, 0, &out, &n); + CHECK(!out && n == 0, "Get(long key) is a miss, not its 1023-byte prefix"); + kvspaceGet(h, longk, 1, &out, &n); + CHECK(!out && n == 0, "Get(long key, resolve) is a miss"); + const char *ks[1] = {longk}; + kvspaceDel(h, ks, 1, NULL, 0); + kvspaceGet(h, k, 0, &out, &n); + CHECK(out != NULL, "Del(long key) leaves the 1023-byte key alone"); + uint8_t *body = NULL; + CHECK(kvspaceWriteInPlace(h, longk, 0, 8, &body, NULL, 0) != 0, + "WriteInPlace(long key) rejected"); + char longdir[1200]; + snprintf(longdir, sizeof longdir, "%s/", longk); + int32_t cnt = -1; + CHECK(kvspaceListLen(h, longdir, 0, 1, &cnt) != 0 || cnt == 0, + "ListLen(long prefix, resolve) does not list a truncated dir"); + kvspaceClose(h); +} + +static void test_setvalue_attr(void) { + void *h = open_db("attr"); + REQUIRE(h, "connect"); + /* 8B head + 8B body without attr region must be rejected. */ + uint8_t noattr[16] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0}; + CHECK(kvspaceSetValue(h, "/a", noattr, sizeof noattr, NULL, 0) != 0, + "value without attr region rejected"); + /* attr_len points past end of value. */ + uint8_t badattr[18] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 50, 0}; + CHECK(kvspaceSetValue(h, "/b", badattr, sizeof badattr, NULL, 0) != 0, + "attr_len past end rejected"); + /* attr_len < 2. */ + uint8_t shortattr[18] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 1, 0}; + CHECK(kvspaceSetValue(h, "/c", shortattr, sizeof shortattr, NULL, 0) != 0, + "attr_len < 2 rejected"); + /* Valid value with trailing padding bytes: accepted and trimmed to 8+len+attr. */ + uint8_t padded[20] = {8, 0, 0, 0, 0, 0, 0, 0, 42, 0, 0, 0, 0, 0, 0, 0, 2, 0, + 0xEE, 0xEE}; + CHECK(kvspaceSetValue(h, "/d", padded, sizeof padded, NULL, 0) == 0, + "valid value with trailing bytes accepted"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/d", 0, &out, &n); + CHECK(out && n == 18, "trailing bytes not part of the value (len=%u)", n); + kvspaceClose(h); +} + +static void test_resolve_prefix(void) { + void *h = open_db("prefix"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/vthread/1/x", 11) == 0 && set_i64(h, "/vthread/1/y", 12) == 0 && + set_i64(h, "/vthread/10/x", 13) == 0, + "seed keys"); + REQUIRE(kvspaceViewQuiesce(h, 0) == 0, "unquiesce"); + kvspaceRef_t pref; + CHECK(kvspaceViewResolvePrefix(h, "/vthread/1/", &pref) == 0, + "ResolvePrefix(/vthread/1/) with sibling /vthread/10/"); + kvspaceViewToken_t tok; + if (kvspaceViewResolveFrom(h, &pref, "y", &tok) == 0) + CHECK(tok.value_len == 8 && tok.body[0] == 12, "prefix ref resolves /vthread/1/y"); + else + CHECK(0, "ViewResolveFrom(prefix, y)"); + CHECK(kvspaceViewResolvePrefix(h, "/vthread/10/", &pref) == 0, + "ResolvePrefix(/vthread/10/)"); + CHECK(kvspaceViewResolvePrefix(h, "/vthread/2/", &pref) != 0, + "ResolvePrefix of a missing dir fails"); + kvspaceClose(h); +} + +static void test_viewwrite_len(void) { + void *h = open_db("view"); + REQUIRE(h, "connect"); + REQUIRE(set_chars(h, "/s", "ab") == 0, "set /s"); + REQUIRE(kvspaceViewQuiesce(h, 0) == 0, "unquiesce"); + kvspaceViewToken_t tok; + REQUIRE(kvspaceViewResolve(h, "/s", &tok) == 0, "view resolve"); + CHECK(kvspaceViewWrite(h, &tok, (const uint8_t *)"abcdefgh", 8) == 1, + "longer ViewWrite falls back"); + kvspaceHead_t hd; + CHECK(kvspaceGetHead(h, "/s", &hd) == 0 && hd.body_len == 2 && + strcmp(hd.langtype, "char/utf8") == 0, + "value and langtype intact after rejected ViewWrite"); + CHECK(kvspaceViewWrite(h, &tok, (const uint8_t *)"xy", 2) == 0, + "same-length ViewWrite"); + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, "/s", 0, &out, &n); + CHECK(out && memcmp(out + 8, "xy", 2) == 0, "same-length ViewWrite landed"); + CHECK(kvspaceGetHead(h, "/s", &hd) == 0 && strcmp(hd.langtype, "char/utf8") == 0, + "langtype intact after same-length ViewWrite"); + kvspaceClose(h); +} + +/* With per-handle ListLen + ListAt caching, any key appearance/disappearance + * must be immediately visible (including across processes). */ +static int list_names(void *h, const char *dir, char out[][32], int cap) { + int32_t n = -1; + if (kvspaceListLen(h, dir, 0, 0, &n) != 0) + return -1; + for (int32_t i = 0; i < n && i < cap; i++) { + uint32_t ol = 0; + if (kvspaceListAt(h, dir, 0, 0, i, (uint8_t *)out[i], 31, &ol) != 0) + return -2; + out[i][ol] = 0; + } + return n; +} + +static void test_list_cache(void) { + void *h = open_db("list"); + REQUIRE(h, "connect"); + REQUIRE(set_i64(h, "/d/a", 1) == 0 && set_i64(h, "/d/b", 2) == 0, "seed"); + char nm[8][32]; + CHECK(list_names(h, "/d/", nm, 8) == 2 && !strcmp(nm[0], "a") && !strcmp(nm[1], "b"), + "initial list a,b"); + REQUIRE(set_i64(h, "/d/c", 3) == 0, "add c"); + CHECK(list_names(h, "/d/", nm, 8) == 3 && !strcmp(nm[2], "c"), "insert visible"); + const char *ka[1] = {"/d/a"}; + kvspaceDel(h, ka, 1, NULL, 0); + CHECK(list_names(h, "/d/", nm, 8) == 2 && !strcmp(nm[0], "b"), "delete visible"); + REQUIRE(set_i64(h, "/d/a", 4) == 0, "revive a"); + CHECK(list_names(h, "/d/", nm, 8) == 3 && !strcmp(nm[0], "a"), "tombstone revive visible"); + /* Same-key overwrite leaves key set and listing unchanged. */ + REQUIRE(set_i64(h, "/d/a", 5) == 0, "overwrite a"); + CHECK(list_names(h, "/d/", nm, 8) == 3, "overwrite keeps list"); + /* View delete (tombstone) must also invalidate the cache. */ + REQUIRE(kvspaceViewQuiesce(h, 0) == 0, "unquiesce"); + kvspaceViewToken_t tok; + REQUIRE(kvspaceViewResolve(h, "/d/c", &tok) == 0, "view resolve c"); + CHECK(kvspaceViewDel(h, &tok, "/d/c") == 0, "view del c"); + CHECK(list_names(h, "/d/", nm, 8) == 2, "view delete visible"); + /* Out-of-range index and short buffer fail as before, reporting required length in *out_len. */ + uint8_t b1[1]; + uint32_t ol = 0; + CHECK(kvspaceListAt(h, "/d/", 0, 0, 9, b1, 1, &ol) != 0, "idx out of range"); + REQUIRE(set_i64(h, "/d/longname", 6) == 0, "add longname"); + int32_t n = 0; + kvspaceListLen(h, "/d/", 0, 0, &n); + int found = 0; + for (int32_t i = 0; i < n; i++) { + if (kvspaceListAt(h, "/d/", 0, 0, i, b1, 1, &ol) != 0 && ol == 8) + found = 1; + } + CHECK(found, "short buffer reports needed length"); + /* Key-set mutation by another process invalidates this handle's cache. */ + CHECK(list_names(h, "/d/", nm, 8) == 3, "list before child (a,b,longname)"); + fflush(NULL); + pid_t pid = fork(); + if (pid == 0) { + void *c = open_db("list"); + if (!c) + _exit(2); + int rc = set_i64(c, "/d/z", 9); + const char *kb[1] = {"/d/b"}; + kvspaceDel(c, kb, 1, NULL, 0); + kvspaceClose(c); + _exit(rc == 0 ? 0 : 3); + } + int st = 0; + waitpid(pid, &st, 0); + CHECK(WIFEXITED(st) && WEXITSTATUS(st) == 0, "child wrote"); + int cnt = list_names(h, "/d/", nm, 8); + int has_z = 0, has_b = 0; + for (int i = 0; i < cnt; i++) { + has_z |= !strcmp(nm[i], "z"); + has_b |= !strcmp(nm[i], "b"); + } + CHECK(cnt == 3 && has_z && !has_b, "cross-process insert/delete visible (n=%d)", cnt); + kvspaceClose(h); +} + +/* DelTree deletes base, base/..., base..., and base... (including + * hidden U+2025 keys), normalizes trailing '/' or U+00B7 on prefix, preserves + * sibling prefixes (/dx, /d2), and invalidates the List cache. */ +static int exists(void *h, const char *key) { + uint8_t *out = NULL; + uint32_t n = 0; + kvspaceGet(h, key, 0, &out, &n); + return out != NULL; +} + +static const char *const dt_in[] = { + "/d", "/d/a", "/d/\xE2\x80\xA5status", "/d/sub/x", "/d/sub/\xE2\x80\xA5pc", + "/d\xC2\xB7k", "/d\xC2\xB7k/y", "/d\xE2\x80\xA5x", +}; +static const char *const dt_out[] = {"/dx", "/d2/a", "/e/\xE2\x80\xA5status"}; + +static int dt_seed(void *h) { + if (kvspaceMkindex(h, "/d/", 0, NULL, 0) != 0) + return -1; + for (size_t i = 0; i < sizeof dt_in / sizeof *dt_in; i++) + if (set_i64(h, dt_in[i], (int64_t)i) != 0) + return -1; + for (size_t i = 0; i < sizeof dt_out / sizeof *dt_out; i++) + if (set_i64(h, dt_out[i], 100 + (int64_t)i) != 0) + return -1; + return 0; +} + +static void dt_check(void *h, const char *how) { + CHECK(!exists(h, "/d/"), "%s: /d/ index deleted", how); + for (size_t i = 0; i < sizeof dt_in / sizeof *dt_in; i++) + CHECK(!exists(h, dt_in[i]), "%s: %s deleted", how, dt_in[i]); + for (size_t i = 0; i < sizeof dt_out / sizeof *dt_out; i++) + CHECK(exists(h, dt_out[i]), "%s: %s kept", how, dt_out[i]); +} + +static void test_deltree(void) { + void *h = open_db("deltree"); + void *h2 = open_db("deltree"); + REQUIRE(h && h2, "connect"); + const char *forms[] = {"/d", "/d/", "/d\xC2\xB7"}; + for (size_t f = 0; f < sizeof forms / sizeof *forms; f++) { + REQUIRE(dt_seed(h) == 0, "seed for DelTree(%s)", forms[f]); + CHECK(exists(h, "/d/\xE2\x80\xA5status"), "seeded runtime key readable"); + /* Warm List caches for /d/ and /d/sub/ across both handles. */ + int32_t n1 = -1, n2 = -1, n3 = -1; + kvspaceListLen(h, "/d/", 0, 0, &n1); + kvspaceListLen(h2, "/d/", 0, 0, &n2); + kvspaceListLen(h, "/d/sub/", 0, 0, &n3); + CHECK(n1 == 2 && n2 == 2 && n3 == 1, "list before (%d,%d,%d)", n1, n2, n3); + CHECK(kvspaceDelTree(h, forms[f], NULL, 0) == 0, "DelTree(%s)", forms[f]); + dt_check(h, forms[f]); + kvspaceListLen(h, "/d/", 0, 0, &n1); + kvspaceListLen(h2, "/d/", 0, 0, &n2); + kvspaceListLen(h, "/d/sub/", 0, 0, &n3); + CHECK(n1 == 0 && n2 == 0 && n3 == 0, "list cache invalidated (%d,%d,%d)", n1, + n2, n3); + uint32_t ol = 0; + uint8_t b[8]; + CHECK(kvspaceListAt(h2, "/d/", 0, 0, 0, b, sizeof b, &ol) != 0, + "ListAt after DelTree misses"); + } + /* Pointer: delete only the pointer itself, leaving target intact (POSIX rm). */ + REQUIRE(set_i64(h, "/t/a", 1) == 0 && set_i64(h, "/t/\xE2\x80\xA5pc", 2) == 0, + "seed /t"); + uint8_t *lv = NULL; + uint32_t ln = 0; + /* REQUIRE evaluates its condition twice; keep allocating call outside to avoid leaking. */ + int prc = kvspaceNewPtr("", "/t", &lv, &ln); + REQUIRE(prc == 0, "NewPtr"); + int sp = kvspaceSetValue(h, "/p", lv, ln, NULL, 0); + int sq = kvspaceSetValue(h, "/q", lv, ln, NULL, 0); + free(lv); + REQUIRE(sp == 0 && sq == 0, "set links /p /q"); + CHECK(kvspaceDelTree(h, "/p", NULL, 0) == 0 && !exists(h, "/p"), "DelTree(link)"); + CHECK(kvspaceDelTree(h, "/q/", NULL, 0) == 0 && !exists(h, "/q"), "DelTree(link/)"); + CHECK(exists(h, "/t/a") && exists(h, "/t/\xE2\x80\xA5pc"), "link target intact"); + /* Root: Clear removes everything (including U+2025 keys). */ + CHECK(kvspaceClear(h, NULL, 0) == 0, "Clear"); + CHECK(!exists(h, "/t/a") && !exists(h, "/t/\xE2\x80\xA5pc") && !exists(h, "/dx") && + !exists(h, "/e/\xE2\x80\xA5status"), + "Clear removed everything"); + kvspaceClose(h2); + kvspaceClose(h); +} + +static void test_dsn_scheme(void) { + char dsn[256]; + snprintf(dsn, sizeof dsn, "s://%s/x", g_dir); + CHECK(kvspaceConnect(dsn) == NULL, "s:// rejected"); + snprintf(dsn, sizeof dsn, "://%s/x", g_dir); + CHECK(kvspaceConnect(dsn) == NULL, ":// rejected"); + CHECK(kvspaceConnect(NULL) == NULL, "NULL dsn rejected"); + CHECK(kvspaceConnect("shm://") == NULL, "empty path rejected"); +} + +static void test_abi_err_reporting(void) { + void *h = open_db("err"); + REQUIRE(h, "connect"); + char err[128]; + + /* kvspaceDel: invalid args return non-zero + non-empty err; missing key stays 0 */ + const char *ks[1] = {"/missing"}; + memset(err, 0, sizeof err); + CHECK(kvspaceDel(NULL, ks, 1, err, sizeof err) != 0 && err[0] != '\0', + "Del(NULL h) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceDel(h, NULL, 1, err, sizeof err) != 0 && err[0] != '\0', + "Del(NULL keys, n>0) fails with err='%s'", err); + const char *null_key[1] = {NULL}; + memset(err, 0, sizeof err); + CHECK(kvspaceDel(h, null_key, 1, err, sizeof err) != 0 && err[0] != '\0', + "Del(NULL key[0]) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceDel(h, ks, 1, err, sizeof err) == 0 && err[0] == '\0', + "Del(missing key) is idempotent 0"); + + /* kvspaceDelTree / kvspaceCp / kvspaceCpTree / kvspaceCpList / kvspaceMkindex / + * kvspaceRmindexExt / kvspaceClear populate err on failure */ + memset(err, 0, sizeof err); + CHECK(kvspaceDelTree(h, "/.kvspace-meta", err, sizeof err) != 0 && err[0] != '\0', + "DelTree(reserved) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceCp(h, "/no-such-src", "/dst", err, sizeof err) != 0 && err[0] != '\0', + "Cp(missing src) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceCpTree(h, "/a", "/a/sub", err, sizeof err) != 0 && err[0] != '\0', + "CpTree(into child) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceCpList(h, "/.kvspace-meta", "/dst", err, sizeof err) != 0 && err[0] != '\0', + "CpList(reserved) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceMkindex(NULL, "/idx/", 0, err, sizeof err) != 0 && err[0] != '\0', + "Mkindex(NULL h) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceRmindexExt(h, "/no-such-ext/", err, sizeof err) != 0 && err[0] != '\0', + "RmindexExt(missing) fails with err='%s'", err); + memset(err, 0, sizeof err); + CHECK(kvspaceClear(NULL, err, sizeof err) != 0 && err[0] != '\0', + "Clear(NULL h) fails with err='%s'", err); + + /* Watch(target=None) on a missing/deleted key must wake immediately without waiting out tick_ns. */ + REQUIRE(set_chars(h, "/watch/del", "pc0") == 0, "seed /watch/del"); + const char *del_k[1] = {"/watch/del"}; + REQUIRE(kvspaceDel(h, del_k, 1, err, sizeof err) == 0, "del /watch/del"); + struct timespec wt0, wt1; + clock_gettime(CLOCK_MONOTONIC, &wt0); + uint8_t *wout = (uint8_t *)1; + uint32_t wlen = 99; + CHECK(kvspaceWatch(h, "/watch/del", (const uint8_t *)"", 0, 5000000000ULL, + &wout, &wlen) == 0 && + wout == NULL && wlen == 0, + "Watch(None) wakes immediately on deleted key"); + clock_gettime(CLOCK_MONOTONIC, &wt1); + uint64_t welapsed_ms = + (uint64_t)(wt1.tv_sec - wt0.tv_sec) * 1000ULL + + (uint64_t)(wt1.tv_nsec - wt0.tv_nsec) / 1000000ULL; + CHECK(welapsed_ms < 500ULL, "Watch(None) elapsed %llums < 500ms", + (unsigned long long)welapsed_ms); + + kvspaceClose(h); +} + +int main(void) { + if (!mkdtemp(g_dir)) { + perror("mkdtemp"); + return 1; + } + test_writenewplace_ref(); + test_writenewplace_body_cap(); + test_setpart_overflow(); + test_long_key(); + test_setvalue_attr(); + test_resolve_prefix(); + test_viewwrite_len(); + test_list_cache(); + test_deltree(); + test_dsn_scheme(); + test_abi_err_reporting(); + char cmd[128]; + snprintf(cmd, sizeof cmd, "rm -rf %s", g_dir); + if (system(cmd) != 0) + fprintf(stderr, "cleanup failed: %s\n", g_dir); + if (failures) { + fprintf(stderr, "test_abi_contract: %d failure(s)\n", failures); + return 1; + } + printf("test_abi_contract: ok\n"); + return 0; +} diff --git a/tests/test_art_alignment.c b/tests/test_art_alignment.c index 502f0ee..1ecc2f0 100644 --- a/tests/test_art_alignment.c +++ b/tests/test_art_alignment.c @@ -3,7 +3,8 @@ * 1) 每个 ART 块的数据起点 8 字节对齐,且 art_hdr_t.val(偏移 16)不跨 64B * cache line。否则 8 字节原子字在 x86 上退化为 split lock,在 AArch64 上 * 可能直接陷入(LSE/LLSC 要求自然对齐)——这是正确性前提,不只是性能。 - * 2) format_version 不是当前版本的 store 必须 open 失败(不迁移、不兼容)。 + * 2) Opening a store whose format_version != current version must fail (no + * migration or backward compatibility) and print a rebuild hint to stderr. */ #define _GNU_SOURCE @@ -13,6 +14,7 @@ #include #include #include +#include #include #define CHECK(x) \ @@ -53,10 +55,38 @@ int main(void) { CHECK(kvspaceShmArtAlignCheck(kv) == -1); - /* 旧格式版本(任何非当前版本):close 后 open 必须失败。 */ + /* Older format version (any non-current version): reopen after close must fail + * and print a rebuild hint to stderr (checked before mapping full current header). */ kvspaceShmTestSetFormatVersion(kv, KVSPACE_FORMAT_VERSION - 1); kvspaceShmClose(kv); - CHECK(kvspaceShmOpen(path, 8UL * 64 * 64 * 64 * 64) == NULL); + /* A real format-5 store header had no list_gen (8 bytes shorter), making the + * ART file 8 bytes shorter as well; mapping the current header first would fail + * inside region_attach before reaching a post-mmap version check. */ + struct stat st; + CHECK(stat(path, &st) == 0 && truncate(path, st.st_size - 8) == 0); + char errpath[160]; + snprintf(errpath, sizeof errpath, "%s/stderr", dir); + fflush(stderr); + int saved = dup(STDERR_FILENO); + FILE *ef = fopen(errpath, "w+"); + CHECK(saved >= 0 && ef != NULL); + dup2(fileno(ef), STDERR_FILENO); + kvspace_t *old = kvspaceShmOpen(path, 8UL * 64 * 64 * 64 * 64); + fflush(stderr); + dup2(saved, STDERR_FILENO); + close(saved); + CHECK(old == NULL); + char msg[512] = {0}; + rewind(ef); + size_t ml = fread(msg, 1, sizeof msg - 1, ef); + fclose(ef); + msg[ml] = 0; + char want[64]; + snprintf(want, sizeof want, "version=%u", (unsigned)(KVSPACE_FORMAT_VERSION - 1)); + if (!strstr(msg, "rebuild") || !strstr(msg, want)) + fprintf(stderr, "open stderr: [%s]\n", msg); + CHECK(strstr(msg, "rebuild") != NULL); + CHECK(strstr(msg, want) != NULL); printf("PASS art_alignment\n"); return 0; diff --git a/tests/test_codec_abi.c b/tests/test_codec_abi.c index e17f63d..ed7a721 100644 --- a/tests/test_codec_abi.c +++ b/tests/test_codec_abi.c @@ -6,6 +6,9 @@ #include #include #include +#ifndef _WIN32 +#include +#endif #define CHECK(x) do { if (!(x)) { fprintf(stderr, "failed: %s\n", #x); return 1; } } while (0) diff --git a/tests/test_inplace_body_only.c b/tests/test_inplace_body_only.c index fae88d1..9ac52c1 100644 --- a/tests/test_inplace_body_only.c +++ b/tests/test_inplace_body_only.c @@ -111,24 +111,12 @@ static void run(void) { free(wi1.raw); } - /* 3. SetPart 写 head:硬错,整值不变。 */ + /* 3. SetPart 写 body:接受;head/attr 逐字节不变,body 变。 */ put_i64(h, "/sp"); struct Value sp0; snap(h, "/sp", &sp0); REQUIRE(sp0.raw != NULL && sp0.len == 26, "base /sp"); - uint8_t patch[8] = {0xde, 0xad, 0xbe, 0xef, 0, 0, 0, 0}; - char err[128] = {0}; - const int sp_rc = kvspaceSetPart(h, "/sp", 0, patch, 8, err, sizeof err); - CHECK(sp_rc != 0, "SetPart(offset=0) must fail"); - struct Value sp1; - snap(h, "/sp", &sp1); - CHECK(sp1.raw != NULL, "value intact after rejected head write"); - if (sp1.raw) - CHECK(sp1.len == sp0.len && memcmp(sp1.raw, sp0.raw, sp0.len) == 0, - "head write changed value"); - - /* 4. SetPart 写 body:接受;head/attr 逐字节不变,body 变。 */ - if (sp1.raw) { + { int64_t nine = 99; CHECK(kvspaceSetPart(h, "/sp", 8, (const uint8_t *)&nine, 8, NULL, 0) == 0, @@ -145,7 +133,7 @@ static void run(void) { free(vw0.raw); free(vw1.raw); free(wi0.raw); - free(sp0.raw); free(sp1.raw); + free(sp0.raw); kvspaceClose(h); } diff --git a/tests/test_xvalue_head.c b/tests/test_xvalue_head.c index cd3c279..f2814b2 100644 --- a/tests/test_xvalue_head.c +++ b/tests/test_xvalue_head.c @@ -168,6 +168,44 @@ static void test_reserve_code(void) { "code cap