diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 77ba8e3..9108591 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -128,6 +128,9 @@ without instantiating it. backend is initialized and the requested artifact/handler is currently available. If an artifact is cold or stale, dispatch compiles it on demand via `get_shared_unit()` and rechecks. There is no native unit-execution fallback. +Workers identify cached unit artifacts by nanosecond mtime, ctime, and size. +Whole-second mtime alone is insufficient because a dependency-triggered rebuild +can replace a wasm artifact within the same second as its prior build. --- @@ -323,11 +326,13 @@ header free-functions are `inline`. The wasm backend exposes only declarations ## 10. Testing - **Regression gate**: `scripts/run_cli_tests.sh --include-wasm-kill` runs the - in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages. + in-runtime CLI suite (`site/tests/cli_runner.uce`) plus the site test pages and + `scripts/test_dependency_invalidation.sh`. The latter changes a transitive + `#load`, then replaces a warmed worker artifact while preserving its + whole-second mtime to prove both compiler and worker caches invalidate it. - **WebSocket end-to-end**: a headless client performs a raw WS handshake to `:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving `SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full broker → worker → broker → client chain across process boundaries. - diff --git a/scripts/test_dependency_invalidation.sh b/scripts/test_dependency_invalidation.sh index 25d318c..5e72b38 100755 --- a/scripts/test_dependency_invalidation.sh +++ b/scripts/test_dependency_invalidation.sh @@ -32,17 +32,34 @@ printf '%s\n' \ 'CLI(Request& context) { print(dependency_cache_marker()); }' \ 'RENDER(Request& context) { print(dependency_cache_marker()); }' >"$source_dir/parent.uce" -first=$(scripts/uce-cli "/$test_name/parent.uce") -if [[ "$first" != *dependency-marker-a* ]]; then - echo "dependency invalidation setup failed: $first" >&2 - exit 1 -fi +assert_marker() { + local path="$1" + local expected="$2" + local output + output=$(scripts/uce-cli "/$test_name/$path.uce") + if [[ "$output" != *"$expected"* ]]; then + echo "$path returned stale module; expected $expected: $output" >&2 + exit 1 + fi +} + +assert_marker parent dependency-marker-a sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce" -second=$(scripts/uce-cli "/$test_name/parent.uce") -if [[ "$second" != *dependency-marker-b* ]]; then - echo "loaded dependency change did not recompile parent: $second" >&2 - exit 1 -fi +assert_marker parent dependency-marker-b + +# Warm every configured worker, then replace the artifact while retaining its +# whole-second mtime. The worker cache must notice the nanosecond/ctime change. +for _ in {1..16}; do assert_marker parent dependency-marker-b; done +sed 's/dependency-marker-b/dependency-marker-c/' "$source_dir/child.uce" >"$source_dir/alternate-child.uce" +sed 's/child.uce/alternate-child.uce/' "$source_dir/parent.uce" >"$source_dir/alternate.uce" +assert_marker alternate dependency-marker-c +parent_wasm="$cache_dir/parent.uce.wasm" +alternate_wasm="$cache_dir/alternate.uce.wasm" +parent_mtime=$(stat -c %Y "$parent_wasm") +cp "$alternate_wasm" "$parent_wasm" +touch -d "@$parent_mtime" "$parent_wasm" +rm -f "$cache_dir/parent.uce.cwasm" +for _ in {1..16}; do assert_marker parent dependency-marker-c; done echo "dependency invalidation passed" diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 26945fa..fef30af 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -75,7 +75,9 @@ struct WasmUnitModule { String source_path; // absolute .uce path String wasm_path; // artifact in the unit cache - time_t mtime = 0; + u64 modified_ns = 0; + u64 changed_ns = 0; + u64 size = 0; WasmDylinkInfo dylink; WasmAbiInfo abi; std::optional module; @@ -640,13 +642,17 @@ public: return(nullptr); } auto cached = module_cache.find(wasm_path); - if(cached != module_cache.end() && cached->second->mtime == st.st_mtime) + u64 modified_ns = (u64)st.st_mtim.tv_sec * 1000000000ull + (u64)st.st_mtim.tv_nsec; + u64 changed_ns = (u64)st.st_ctim.tv_sec * 1000000000ull + (u64)st.st_ctim.tv_nsec; + if(cached != module_cache.end() && cached->second->modified_ns == modified_ns && cached->second->changed_ns == changed_ns && cached->second->size == (u64)st.st_size) return(cached->second); auto unit = std::make_shared(); unit->source_path = source_path; unit->wasm_path = wasm_path; - unit->mtime = st.st_mtime; + unit->modified_ns = modified_ns; + unit->changed_ns = changed_ns; + unit->size = (u64)st.st_size; std::vector bytes; if(!wasm_read_file(wasm_path, bytes)) { @@ -696,7 +702,8 @@ private: struct stat cached_stat; if(stat(cached_path.c_str(), &cached_stat) == 0 && stat(wasm_path.c_str(), &wasm_stat) == 0) { - if(cached_stat.st_mtime > wasm_stat.st_mtime) + if(cached_stat.st_mtim.tv_sec > wasm_stat.st_mtim.tv_sec || + (cached_stat.st_mtim.tv_sec == wasm_stat.st_mtim.tv_sec && cached_stat.st_mtim.tv_nsec > wasm_stat.st_mtim.tv_nsec)) { auto deserialized = wasmtime::Module::deserialize_file(engine, cached_path); if(deserialized)