From d4ae51f54b3e4e12d600b2bd63fc508ccfbdb1df Mon Sep 17 00:00:00 2001 From: udo Date: Sat, 18 Jul 2026 19:12:53 +0000 Subject: [PATCH] Isolate managed precompile race validation --- docs/setup.md | 12 ++++++- scripts/systemd/manage-uce-service.sh | 5 +-- scripts/test_parallel_precompile.sh | 48 ++++++++++++++++----------- src/linux_fastcgi.cpp | 6 ++++ 4 files changed, 48 insertions(+), 23 deletions(-) diff --git a/docs/setup.md b/docs/setup.md index a4bdf59..03a991a 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -256,7 +256,17 @@ Managed restart builds and precompiles the complete candidate ABI generation before it switches the service. Precompile uses two low-priority processes by default; set `PRECOMPILE_JOBS` in `/etc/uce/settings.cfg` to tune the bounded 1–16 process count for the host. A failed worker, compile, serialization, or -result report aborts the switch and leaves the current service running. +result report aborts the switch and leaves the current service running. The +managed invocation is bounded to 900 seconds by default; set +`UCE_PRECOMPILE_TIMEOUT` on `manage-uce-service.sh restart` to select another +GNU `timeout` duration. Timeout exit 124 also aborts before the service switch. + +A trusted direct `--precompile` process may set `UCE_PRECOMPILE_FILES_IN` and +`UCE_PRECOMPILE_BIN_DIRECTORY` to isolate that invocation's source scan and +artifact registry. Both overrides apply only to precompile mode; normal server +workers continue to use `/etc/uce/settings.cfg`. The parallel precompile +regression uses private roots so a running proactive compiler cannot consume +or publish its controlled race fixtures. Equivalent manual systemd service for a source checkout (`` = checkout root): diff --git a/scripts/systemd/manage-uce-service.sh b/scripts/systemd/manage-uce-service.sh index 1dc624b..1767c33 100755 --- a/scripts/systemd/manage-uce-service.sh +++ b/scripts/systemd/manage-uce-service.sh @@ -18,6 +18,7 @@ CONFIG_SOURCE="$REPO_ROOT/etc/uce/settings.cfg" CONFIG_DEST="/etc/uce/settings.cfg" action="${1:-setup}" +precompile_timeout="${UCE_PRECOMPILE_TIMEOUT:-900s}" render_runtime_file() { local source="$1" @@ -60,9 +61,9 @@ case "$action" in cd "$REPO_ROOT" service_user=$(systemctl show "$UNIT_NAME" -p User --value) if [[ -n "$service_user" && "$(id -u)" == "0" ]]; then - runuser -u "$service_user" -- nice -n 10 "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile + runuser -u "$service_user" -- timeout --signal=TERM --kill-after=10s "$precompile_timeout" nice -n 10 "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile else - nice -n 10 "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile + timeout --signal=TERM --kill-after=10s "$precompile_timeout" nice -n 10 "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile fi ) systemctl restart "$UNIT_NAME" diff --git a/scripts/test_parallel_precompile.sh b/scripts/test_parallel_precompile.sh index fac3f25..ba6cc53 100755 --- a/scripts/test_parallel_precompile.sh +++ b/scripts/test_parallel_precompile.sh @@ -3,34 +3,32 @@ set -euo pipefail cd "$(dirname "$0")/.." test_name="parallel-precompile-test-$$" -site_directory="site" -bin_directory="/tmp/uce/work" -if [[ -r /etc/uce/settings.cfg ]]; then - configured_precompile=$(awk -F= '/^[[:space:]]*PRECOMPILE_FILES_IN[[:space:]]*=/ {sub(/^[^=]*=/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}' /etc/uce/settings.cfg) - configured_site=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {sub(/^[^=]*=/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}' /etc/uce/settings.cfg) - configured_bin=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {sub(/^[^=]*=/, ""); gsub(/^[[:space:]]+|[[:space:]]+$/, ""); print; exit}' /etc/uce/settings.cfg) - site_directory="${configured_precompile:-${configured_site:-$site_directory}}" - bin_directory="${configured_bin:-$bin_directory}" -fi - -source_dir="$site_directory/$test_name" +test_root="/tmp/$test_name" +source_dir="$test_root/site" +bin_directory="$test_root/work" absolute_source_dir="" artifact_dir="" generation_log="" +generation_pid="" precompile_timeout="${UCE_TEST_PRECOMPILE_TIMEOUT:-120s}" precompile() { - timeout "$precompile_timeout" env "$@" bin/uce_fastcgi.linux.bin --precompile + timeout "$precompile_timeout" env UCE_PRECOMPILE_FILES_IN="$source_dir" UCE_PRECOMPILE_BIN_DIRECTORY="$bin_directory" "$@" bin/uce_fastcgi.linux.bin --precompile } cleanup() { - rm -rf "$source_dir" - if [[ -x bin/uce_fastcgi.linux.bin ]]; then - precompile UCE_PRECOMPILE_JOBS=1 >/dev/null 2>&1 || true - fi - if [[ -n "$artifact_dir" ]]; then - rm -rf "$artifact_dir" + if [[ -n "$generation_pid" ]] && kill -0 "$generation_pid" 2>/dev/null; then + kill "$generation_pid" 2>/dev/null || true + cleanup_deadline=$((SECONDS + 10)) + while kill -0 "$generation_pid" 2>/dev/null && (( SECONDS < cleanup_deadline )); do + sleep 0.05 + done + if kill -0 "$generation_pid" 2>/dev/null; then + kill -KILL "$generation_pid" 2>/dev/null || true + fi + wait "$generation_pid" 2>/dev/null || true fi + rm -rf "$test_root" if [[ -n "$generation_log" ]]; then rm -f "$generation_log" fi @@ -124,18 +122,23 @@ set +e wait "$generation_pid" generation_rc=$? set -e +generation_pid="" if [[ $generation_rc -eq 0 ]] || ! grep -q 'source generation changed during precompile; candidate generation rejected' "$generation_log"; then echo "parallel precompile accepted a source generation that changed during compilation" >&2 cat "$generation_log" >&2 exit 1 fi +if [[ -e "$artifact_dir/generation-new.uce.wasm" || -e "$artifact_dir/generation-new.uce.cwasm" ]]; then + echo "parallel precompile published an added unit outside the rejected candidate source set" >&2 + exit 1 +fi generation_retry_output=$(precompile UCE_PRECOMPILE_JOBS=2 UCE_WASM_PCH_DIR="$artifact_dir/pch-shared") if ! grep -Eq 'with 2 jobs: .* 2 compiled, 0 failed, worker status ok' <<<"$generation_retry_output"; then echo "parallel precompile did not reconcile the changed and added units on retry" >&2 echo "$generation_retry_output" >&2 exit 1 fi -rm -f "$generation_log" "$source_dir"/generation-*.uce +rm -f "$generation_log" printf 'CLI(Request& context) { print(deliberate_parallel_precompile_failure); }\n' >"$source_dir/broken.uce" set +e @@ -158,13 +161,18 @@ if [[ $repeat_failure_rc -eq 0 ]] || ! grep -Eq 'with 2 jobs: .* 1 failed, worke fi rm "$source_dir/broken.uce" +rm -f "$source_dir"/unit-*.uce +for unit in $(seq -w 0 4); do + printf 'CLI(Request& context) { print("parallel-precompile-bounds-%s"); }\n' "$unit" >"$source_dir/bounds-$unit.uce" +done negative_jobs_output=$(precompile UCE_PRECOMPILE_JOBS=-1) malformed_jobs_output=$(precompile UCE_PRECOMPILE_JOBS=invalid) oversized_jobs_output=$(precompile UCE_PRECOMPILE_JOBS=17) -if ! grep -q 'with 1 job:' <<<"$negative_jobs_output" || ! grep -q 'with 2 jobs:' <<<"$malformed_jobs_output" || ! grep -q 'with 16 jobs:' <<<"$oversized_jobs_output"; then +if ! grep -q 'with 1 job: 16 units,' <<<"$negative_jobs_output" || ! grep -q 'with 2 jobs: 16 units,' <<<"$malformed_jobs_output" || ! grep -q 'with 16 jobs: 16 units,' <<<"$oversized_jobs_output"; then echo "precompile job bounds did not map negative/malformed/oversized input to 1/2/16" >&2 exit 1 fi +rm -f "$source_dir"/bounds-*.uce "$source_dir"/generation-*.uce printf 'parallel precompile passed: serial %.3fs, parallel %.3fs\n' \ "$(awk -v ns="$serial_ns" 'BEGIN { print ns / 1000000000 }')" \ diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 73815b4..871da8b 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -1644,6 +1644,12 @@ int precompile_unit_generation() { f64 started_at = time_precise(); server_state.config = make_server_settings(); + const char* precompile_files_in = getenv("UCE_PRECOMPILE_FILES_IN"); + if(precompile_files_in && precompile_files_in[0] != '\0') + server_state.config["PRECOMPILE_FILES_IN"] = precompile_files_in; + const char* precompile_bin_directory = getenv("UCE_PRECOMPILE_BIN_DIRECTORY"); + if(precompile_bin_directory && precompile_bin_directory[0] != '\0') + server_state.config["BIN_DIRECTORY"] = precompile_bin_directory; server_state.config["COMPILER_SYS_PATH"] = cwd_get(); Request background_context; background_context.server = &server_state;