Invalidate warmed wasm modules precisely

This commit is contained in:
udo
2026-07-13 08:36:55 +00:00
parent ada01e34f4
commit 73a2671112
3 changed files with 45 additions and 16 deletions
+7 -2
View File
@@ -128,6 +128,9 @@ without instantiating it.
backend is initialized and the requested artifact/handler is currently backend is initialized and the requested artifact/handler is currently
available. If an artifact is cold or stale, dispatch compiles it on demand via 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. `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 ## 10. Testing
- **Regression gate**: `scripts/run_cli_tests.sh --include-wasm-kill` runs the - **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 - **WebSocket end-to-end**: a headless client performs a raw WS handshake to
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving `:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full `SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
broker → worker → broker → client chain across process boundaries. broker → worker → broker → client chain across process boundaries.
+27 -10
View File
@@ -32,17 +32,34 @@ printf '%s\n' \
'CLI(Request& context) { print(dependency_cache_marker()); }' \ 'CLI(Request& context) { print(dependency_cache_marker()); }' \
'RENDER(Request& context) { print(dependency_cache_marker()); }' >"$source_dir/parent.uce" 'RENDER(Request& context) { print(dependency_cache_marker()); }' >"$source_dir/parent.uce"
first=$(scripts/uce-cli "/$test_name/parent.uce") assert_marker() {
if [[ "$first" != *dependency-marker-a* ]]; then local path="$1"
echo "dependency invalidation setup failed: $first" >&2 local expected="$2"
exit 1 local output
fi 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" sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce"
second=$(scripts/uce-cli "/$test_name/parent.uce") assert_marker parent dependency-marker-b
if [[ "$second" != *dependency-marker-b* ]]; then
echo "loaded dependency change did not recompile parent: $second" >&2 # Warm every configured worker, then replace the artifact while retaining its
exit 1 # whole-second mtime. The worker cache must notice the nanosecond/ctime change.
fi 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" echo "dependency invalidation passed"
+11 -4
View File
@@ -75,7 +75,9 @@ struct WasmUnitModule
{ {
String source_path; // absolute .uce path String source_path; // absolute .uce path
String wasm_path; // artifact in the unit cache 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; WasmDylinkInfo dylink;
WasmAbiInfo abi; WasmAbiInfo abi;
std::optional<wasmtime::Module> module; std::optional<wasmtime::Module> module;
@@ -640,13 +642,17 @@ public:
return(nullptr); return(nullptr);
} }
auto cached = module_cache.find(wasm_path); 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); return(cached->second);
auto unit = std::make_shared<WasmUnitModule>(); auto unit = std::make_shared<WasmUnitModule>();
unit->source_path = source_path; unit->source_path = source_path;
unit->wasm_path = wasm_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<u8> bytes; std::vector<u8> bytes;
if(!wasm_read_file(wasm_path, bytes)) if(!wasm_read_file(wasm_path, bytes))
{ {
@@ -696,7 +702,8 @@ private:
struct stat cached_stat; struct stat cached_stat;
if(stat(cached_path.c_str(), &cached_stat) == 0 && stat(wasm_path.c_str(), &wasm_stat) == 0) 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); auto deserialized = wasmtime::Module::deserialize_file(engine, cached_path);
if(deserialized) if(deserialized)