From ee022f039897dc5421499f24724bbac3c94f9c46 Mon Sep 17 00:00:00 2001 From: udo Date: Wed, 15 Jul 2026 00:43:23 +0000 Subject: [PATCH] Ensure nested CLI components are fresh --- docs/wasm-runtime-architecture.md | 7 ++-- scripts/test_dependency_invalidation.sh | 44 ++++++++++++++++++++++++- src/wasm/worker.cpp | 5 +-- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 6cf7a68..556098c 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -146,9 +146,10 @@ a stale entry artifact: they return `503 Service Unavailable` with `Retry-After: 1`, allowing the client to retry after the priority rebuild. CLI and explicit compile paths remain synchronous. A failed rebuild removes availability and surfaces the compiler error rather than serving the old unit -indefinitely. The per-unit lock also keeps concurrent synchronous compilers -from waiting across a transitive graph when a last complete artifact is -available. +indefinitely. This freshness contract includes components resolved lazily from +an otherwise-current CLI entry unit: if the proactive compiler already owns a +stale child's lock, the CLI request joins that compile and waits for the fresh +artifact. Read-only HTTP may use the child's last complete artifact instead. Before preprocessing, the compiler verifies that the worker can actually read the source. An unreadable path is reported as a source-read failure with a diff --git a/scripts/test_dependency_invalidation.sh b/scripts/test_dependency_invalidation.sh index f6f9d8f..dc3c355 100755 --- a/scripts/test_dependency_invalidation.sh +++ b/scripts/test_dependency_invalidation.sh @@ -21,6 +21,7 @@ mutation_file="/tmp/uce-dependency-mutation-$$" post_body="/tmp/uce-dependency-post-body-$$" post_headers="/tmp/uce-dependency-post-headers-$$" lock_ready_file="/tmp/uce-dependency-lock-ready-$$" +nested_lock_ready_file="/tmp/uce-nested-lock-ready-$$" race_output="/tmp/uce-source-race-output-$$" race_error="/tmp/uce-source-race-error-$$" race_status="/tmp/uce-source-race-status-$$" @@ -31,7 +32,7 @@ cleanup() { if [[ -n "$cache_dir" ]]; then rm -rf "$cache_dir" fi - rm -f "$mutation_file" "$post_body" "$post_headers" "$lock_ready_file" "$race_output" "$race_error" "$race_status" + rm -f "$mutation_file" "$post_body" "$post_headers" "$lock_ready_file" "$nested_lock_ready_file" "$race_output" "$race_error" "$race_status" } trap cleanup EXIT mkdir -p "$source_dir" @@ -175,6 +176,47 @@ if (( elapsed_ms < 2000 )); then fi wait "$rebuild_lock_pid" +# CLI freshness applies to every nested component, not only the request entry +# unit. Hold the changed child's compile lock as a proactive rebuild would: the +# parent CLI request must wait and render the new child, never its stale wasm. +printf '%s\n' 'COMPONENT(Request& context) { print("nested-component-marker-a"); }' >"$source_dir/nested-child.uce" +printf '%s\n' \ + 'CLI(Request& context) { DValue props; print(component("nested-child", props, context)); }' >"$source_dir/nested-parent.uce" +assert_marker nested-parent nested-component-marker-a +nested_child_wasm="$cache_dir/nested-child.uce.wasm" +( + exec 8>"$nested_child_wasm.lock" + flock 8 + : >"$nested_lock_ready_file" + sleep 3 +) & +nested_lock_pid=$! +deadline=$((SECONDS + 5)) +while [[ ! -e "$nested_lock_ready_file" && $SECONDS -lt $deadline ]]; do + if ! kill -0 "$nested_lock_pid" 2>/dev/null; then + echo "nested component lock exited before acquisition" >&2 + exit 1 + fi + sleep 0.05 +done +if [[ ! -e "$nested_lock_ready_file" ]]; then + echo "nested component lock was not acquired before deadline" >&2 + exit 1 +fi +sed -i 's/nested-component-marker-a/nested-component-marker-b/' "$source_dir/nested-child.uce" +started_at=$(date +%s%N) +nested_output=$(scripts/uce-cli "/$test_name/nested-parent.uce") +elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 )) +wait "$nested_lock_pid" +if [[ "$nested_output" != *"nested-component-marker-b"* ]]; then + echo "CLI nested component returned stale wasm: $nested_output" >&2 + exit 1 +fi +if (( elapsed_ms < 2000 )); then + echo "CLI nested component did not wait for the active rebuild (${elapsed_ms}ms)" >&2 + exit 1 +fi + # 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-d; done diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index fc31802..a7e40bb 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -1612,9 +1612,10 @@ private: } bool stale = compiler_unit_needs_recompile(context, resolved, 0); - if(stale && compiler_request_can_serve_stale_artifact(context)) + bool can_serve_stale = compiler_request_can_serve_stale_artifact(context); + if(stale && can_serve_stale) compiler_prioritize_unit(context, resolved); - if(!file_exists_host(worker.unit_wasm_path(resolved)) || (stale && !compiler_request_can_serve_stale_artifact(context) && !compiler_unit_compile_in_progress(context, resolved))) + if(!file_exists_host(worker.unit_wasm_path(resolved)) || (stale && !can_serve_stale)) get_shared_unit(context, resolved); size_t unit_index = 0;