From e176ff09d9319a9f7af8756b3328281692627cba Mon Sep 17 00:00:00 2001 From: udo Date: Fri, 17 Jul 2026 23:14:42 +0000 Subject: [PATCH] Invalidate component freshness by source generation --- scripts/test_dependency_invalidation.sh | 18 +++++++++++++++++ src/lib/compiler.cpp | 27 +++++++++++++++++++++++++ src/lib/compiler.h | 2 ++ src/linux_fastcgi.cpp | 7 +++++++ src/wasm/worker.cpp | 17 +++++++++++++--- 5 files changed, 68 insertions(+), 3 deletions(-) diff --git a/scripts/test_dependency_invalidation.sh b/scripts/test_dependency_invalidation.sh index 47f5ae5..f13da44 100755 --- a/scripts/test_dependency_invalidation.sh +++ b/scripts/test_dependency_invalidation.sh @@ -118,6 +118,24 @@ if [[ "$http_component_output" != *"http-component-marker-b"* ]]; then exit 1 fi +printf '%s\n' 'String http_component_dependency_marker() { return("http-component-dependency-a"); }' >"$source_dir/http-component-dependency.uce" +printf '%s\n' '#load "http-component-dependency.uce"' 'COMPONENT(Request& context) { print(http_component_dependency_marker()); }' >"$source_dir/http-transitive-child.uce" +printf '%s\n' 'RENDER(Request& context) { DValue props; print(component("http-transitive-child", props, context)); }' >"$source_dir/http-transitive-parent.uce" +http_transitive_marker() { + curl -fsS -H "Host: $http_host" "http://127.0.0.1/$test_name/http-transitive-parent.uce" +} +if [[ "$(http_transitive_marker)" != *"http-component-dependency-a"* ]]; then + echo "HTTP transitive component warm-up did not return dependency-a" >&2 + exit 1 +fi +sed -i 's/http-component-dependency-a/http-component-dependency-b/' "$source_dir/http-component-dependency.uce" +deadline=$((SECONDS + 20)) +while [[ "$(http_transitive_marker)" != *"http-component-dependency-b"* && $SECONDS -lt $deadline ]]; do sleep 0.2; done +if [[ "$(http_transitive_marker)" != *"http-component-dependency-b"* ]]; then + echo "HTTP dynamic component did not converge after its loaded dependency changed" >&2 + exit 1 +fi + # Mutations must never execute a stale dynamically resolved component, even # inside the short read-only freshness memo window. http_component_wasm="$cache_dir/http-component-child.uce.wasm" diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index 648bf22..ac2af5e 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -385,6 +385,11 @@ String compiler_priority_file_name(Request* context) return(context->server->config["BIN_DIRECTORY"] + "/proactive-priority.txt"); } +String compiler_source_generation_file_name(Request* context) +{ + return(context->server->config["BIN_DIRECTORY"] + "/source-generation.txt"); +} + int compiler_open_lock_file(String file_name, String purpose, bool nonblocking = false) { (void)purpose; @@ -906,6 +911,7 @@ void compile_shared_unit(Request* context, SharedUnit* su) file_put_contents(su->compile_output_file_name, su->compiler_messages + "\n"); compiler_untrack_known_unit(context, su->file_name); compiler_record_compile_result(su, time_precise() - comp_start, false, "missing_source", su->compiler_messages); + compiler_mark_source_generation(context); return; } struct stat source_info; @@ -922,6 +928,7 @@ void compile_shared_unit(Request* context, SharedUnit* su) file_unlink(su->wasm_name); compiler_record_compile_result(su, time_precise() - comp_start, false, "compile_error", su->compiler_messages); printf("%s \n", compiler_format_source_read_failure(context, su, su->compiler_messages).c_str()); + compiler_mark_source_generation(context); return; } @@ -982,6 +989,7 @@ void compile_shared_unit(Request* context, SharedUnit* su) (su->pre_path + "/" + su->pre_file_name).c_str(), time_precise() - comp_start); } + compiler_mark_source_generation(context); } SharedUnit* compiler_get_shared_unit_internal(Request* context, String file_name, bool force_recompile) @@ -1074,6 +1082,25 @@ SharedUnit* get_shared_unit(Request* context, String file_name) return(compiler_get_shared_unit_internal(context, file_name, false)); } +String compiler_source_generation(Request* context) +{ + if(!context || !context->server) + return(""); + return(trim(file_get_contents(compiler_source_generation_file_name(context)))); +} + +void compiler_mark_source_generation(Request* context) +{ + if(!context || !context->server) + return; + String file_name = compiler_source_generation_file_name(context); + int fdlock = compiler_open_lock_file(file_name + ".lock", "source-generation"); + if(fdlock < 0) + return; + file_put_contents(file_name, std::to_string(getpid()) + ":" + std::to_string((u64)(time_precise() * 1000000.0)) + "\n"); + compiler_close_lock_file(fdlock); +} + String compiler_error_page_unit(Request* context, String config_key) { if(!context || !context->server) diff --git a/src/lib/compiler.h b/src/lib/compiler.h index 97c8a30..84115d1 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -21,6 +21,8 @@ bool compiler_unit_compile_in_progress(Request* context, String file_name); bool compiler_request_can_serve_stale_artifact(Request* context); void compiler_prioritize_unit(Request* context, String file_name); StringList compiler_take_priority_units(Request* context); +String compiler_source_generation(Request* context); +void compiler_mark_source_generation(Request* context); String compiler_site_directory(Request* context); StringList compiler_scan_site_units(Request* context); StringList compiler_list_known_units(Request* context); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 11ba149..0f3136f 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -1263,6 +1263,7 @@ void run_proactive_compiler() { auto tracked_units = compiler_list_known_units(&background_context); StringList existing_units; + bool source_generation_changed = false; for(auto& file_name : tracked_units) { @@ -1271,9 +1272,13 @@ void run_proactive_compiler() bool retry_allowed = (retry_it == retry_after.end() || time_precise() >= retry_it->second); bool needs_compile = compiler_unit_needs_recompile(&background_context, file_name, &source_missing); if(needs_compile && retry_allowed) + { proactive_compile_queue_push(compile_queue, file_name); + source_generation_changed = true; + } if(source_missing) { + source_generation_changed = true; printf("(i) proactive compiler forget removed unit %s\n", file_name.c_str()); retry_after.erase(file_name); continue; @@ -1283,6 +1288,8 @@ void run_proactive_compiler() if(existing_units.size() != tracked_units.size()) compiler_set_known_units(&background_context, existing_units); + if(source_generation_changed) + compiler_mark_source_generation(&background_context); next_scan_at = time_precise() + check_interval; } diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 9c6244a..535cd0c 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -1121,6 +1121,7 @@ private: { std::chrono::steady_clock::time_point checked_at; bool stale = false; + String source_generation; }; std::map component_freshness; @@ -1528,6 +1529,8 @@ private: std::vector host_funcs; // keep host imports alive bool stale_component_mutation_blocked = false; String stale_component_mutation_status; + bool component_source_generation_checked = false; + String component_source_generation; wasmtime::Store::Context ctx() { @@ -2197,6 +2200,11 @@ private: bool can_serve_stale = compiler_request_can_serve_stale_artifact(context); String method = context ? to_upper(trim(context->params["REQUEST_METHOD"])) : String(""); bool read_request = method == "GET" || method == "HEAD" || method == "OPTIONS"; + if(can_serve_stale && read_request && !component_source_generation_checked) + { + component_source_generation = compiler_source_generation(context); + component_source_generation_checked = true; + } bool stale = false; if(artifact_exists) { @@ -2204,14 +2212,17 @@ private: auto cached_freshness = worker.component_freshness.find(resolved); // HTTP reads may serve a complete stale artifact, so bound their graph // stat work; CLI and mutations always check the current graph. + bool generation_available = component_source_generation != ""; bool check_freshness = !can_serve_stale || !read_request || cached_freshness == worker.component_freshness.end() || - std::chrono::duration_cast( - now - cached_freshness->second.checked_at).count() >= 1000; + (generation_available ? + cached_freshness->second.source_generation != component_source_generation : + std::chrono::duration_cast( + now - cached_freshness->second.checked_at).count() >= 1000); if(check_freshness) { stale = compiler_unit_needs_recompile(context, resolved, 0, can_serve_stale && read_request, true); - worker.component_freshness[resolved] = { now, stale }; + worker.component_freshness[resolved] = { now, stale, component_source_generation }; } else stale = cached_freshness->second.stale;