diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 3d2db5d..e9e176e 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -75,5 +75,6 @@ if [[ "$action" == "run" ]]; then scripts/test_mysql_persistent_pool.sh scripts/test_log_timeliness.sh scripts/test_raw_http_request_log.sh + scripts/test_component_resolution_ttl.sh scripts/test_socket_activation.sh fi diff --git a/scripts/test_component_resolution_ttl.sh b/scripts/test_component_resolution_ttl.sh new file mode 100755 index 0000000..bbc47c8 --- /dev/null +++ b/scripts/test_component_resolution_ttl.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="component-resolution-ttl-test-$$" +site_directory="${UCE_TEST_SITE_DIRECTORY:-site}" +worker_count="${UCE_TEST_WORKER_COUNT:-4}" +if [[ -r /etc/uce/settings.cfg ]]; then + if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" ]]; then + configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + site_directory="${configured_site_directory:-$site_directory}" + fi + if [[ -z "${UCE_TEST_WORKER_COUNT:-}" ]]; then + configured_worker_count=$(awk -F= '/^[[:space:]]*WORKER_COUNT[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + worker_count="${configured_worker_count:-$worker_count}" + fi +fi +http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}" +source_dir="$site_directory/$test_name" + +cleanup() { + rm -rf "$source_dir" +} +trap cleanup EXIT +mkdir -p "$source_dir" + +printf '%s\n' 'RENDER(Request& context) { print("parent:", request_perf()["worker_pid"].to_string(), ":", component("chosen", context)); }' >"$source_dir/parent.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("a"); }' >"$source_dir/a.uce" +printf '%s\n' 'COMPONENT(Request& context) { print("b"); }' >"$source_dir/b.uce" +ln -s a.uce "$source_dir/chosen.uce" + +declare -A seen +collect_workers() { + local method="$1" + local marker="$2" + local path="${3:-parent.uce}" + local deadline=$((SECONDS + 30)) + seen=() + while (( SECONDS < deadline && ${#seen[@]} < worker_count )); do + local response status body pid + response=$(curl -sS --max-time 10 -X "$method" -H "Host: $http_host" -w $'\n%{http_code}' "http://127.0.0.1/$test_name/$path") + status=${response##*$'\n'} + body=${response%$'\n'*} + if [[ "$status" == "503" ]]; then + sleep 0.1 + continue + fi + if [[ "$status" != "200" || "$body" != parent:*":$marker" ]]; then + echo "component resolution TTL probe failed: method=$method status=$status expected=$marker body=$body" >&2 + exit 1 + fi + pid=${body#parent:} + pid=${pid%%:*} + seen["$pid"]=1 + done + if (( ${#seen[@]} != worker_count )); then + echo "component resolution TTL probe reached ${#seen[@]}/$worker_count workers" >&2 + exit 1 + fi +} + +collect_missing_workers() { + local deadline=$((SECONDS + 30)) + seen=() + while (( SECONDS < deadline && ${#seen[@]} < worker_count )); do + local response status body pid + response=$(curl -sS --max-time 10 -H "Host: $http_host" -w $'\n%{http_code}' "http://127.0.0.1/$test_name/missing.uce") + status=${response##*$'\n'} + body=${response%$'\n'*} + if [[ "$status" == "503" ]]; then + sleep 0.1 + continue + fi + if [[ "$status" != "200" || "$body" != parent:* || "$body" != *"component not found: later"* ]]; then + echo "missing component TTL probe failed: status=$status body=$body" >&2 + exit 1 + fi + pid=${body#parent:} + pid=${pid%%:*} + seen["$pid"]=1 + done + if (( ${#seen[@]} != worker_count )); then + echo "missing component TTL probe reached ${#seen[@]}/$worker_count workers" >&2 + exit 1 + fi +} + +collect_workers GET a +ln -sfn b.uce "$source_dir/chosen.uce" +collect_workers GET a +collect_workers POST b +collect_workers GET a +sleep 10.2 +collect_workers GET b + +printf '%s\n' 'RENDER(Request& context) { print("parent:", request_perf()["worker_pid"].to_string(), ":", component("later", context)); }' >"$source_dir/missing.uce" +collect_missing_workers +printf '%s\n' 'COMPONENT(Request& context) { print("created"); }' >"$source_dir/later.uce" +collect_workers GET created missing.uce + +echo "component resolution TTL passed" diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 2a9555b..833e47e 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -1167,7 +1167,13 @@ private: bool stale = false; String source_generation; }; + struct ComponentResolutionState + { + std::chrono::steady_clock::time_point checked_at; + String resolved; + }; std::map component_freshness; + std::map component_resolutions; static String cached_wasm_path(const String& wasm_path) { @@ -2412,6 +2418,9 @@ private: String error; size_t unit_index = 0; bool loaded_reuse = false; + String method = context ? to_upper(trim(context->params["REQUEST_METHOD"])) : String(""); + bool cacheable_read = method == "GET" || method == "HEAD"; + bool read_request = cacheable_read || method == "OPTIONS"; auto loaded = units_by_source.find(file_name); if(loaded == units_by_source.end()) { @@ -2427,7 +2436,30 @@ private: component_loaded_reuse_count++; } else - resolved = resolve_source_path(file_name, current_unit); + { + String cache_key = entry_dir + "\t" + resolve_key; + auto cached = worker.component_resolutions.find(cache_key); + auto now = std::chrono::steady_clock::now(); + if(cacheable_read && cached != worker.component_resolutions.end() && + std::chrono::duration_cast(now - cached->second.checked_at).count() < 10) + resolved = cached->second.resolved; + else + { + resolved = resolve_source_path(file_name, current_unit); + if(cacheable_read && resolved != "") + { + if(cached == worker.component_resolutions.end() && worker.component_resolutions.size() >= 4096) + { + auto oldest = worker.component_resolutions.begin(); + for(auto it = worker.component_resolutions.begin(); it != worker.component_resolutions.end(); ++it) + if(it->second.checked_at < oldest->second.checked_at) + oldest = it; + worker.component_resolutions.erase(oldest); + } + worker.component_resolutions[cache_key] = { now, resolved }; + } + } + } component_path_total_us += (u64)std::chrono::duration_cast( std::chrono::steady_clock::now() - probe_start).count(); if(resolved == "") @@ -2447,8 +2479,6 @@ private: auto artifact_start = std::chrono::steady_clock::now(); bool artifact_exists = file_exists_host(worker.unit_wasm_path(resolved)); 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);