diff --git a/docs/setup.md b/docs/setup.md index 94f5c72..0833b9a 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -215,7 +215,9 @@ Important settings: - `FCGI_SOCKET_MODE` and `CLI_SOCKET_MODE` are octal permission modes applied after socket bind. Prefer tightening `FCGI_SOCKET_MODE` to `0660` when nginx/Apache can share a trusted group with the UCE worker. - `SITE_DIRECTORY` is the public site tree to scan for `.uce` files. Use `/var/www/html` when the web root is outside the runtime tree; relative paths are resolved from the runtime working directory. Installed regression gate scripts derive their temporary test root from this setting unless `UCE_TEST_SITE_DIRECTORY` is explicitly provided. - `HTTP_DOCUMENT_ROOT` is the root used by the built-in HTTP/WebSocket listener when it resolves upgrade requests. Set it to the same web root as nginx/Apache. -- `BIN_DIRECTORY` stores generated C++, wasm artifacts, compile output, and runtime caches. +- `BIN_DIRECTORY` stores runtime state plus ABI-scoped unit generations. Unit + C++, wasm, serialized modules, source maps, and compile diagnostics live in + `units-c-w` so an upgrade cannot mix generations. - `TMP_UPLOAD_PATH` and `SESSION_PATH` must be writable by the runtime. - `SESSION_COOKIE_SECURE=1` adds the `Secure` attribute to UCE-managed session cookies and should be used for HTTPS-only deployments. Leave it `0` only for local/plain-HTTP development. - `MYSQL_PERSISTENT_POOL_SIZE` caps credential-keyed connections retained by each Wasm worker. The default `8` is clamped to `64`; set it to `0` to restore request-lifetime connections. Cached sessions are reset before reuse. @@ -697,7 +699,14 @@ Common compile footguns: - `WASMTIME_HOME` does not point at a tree with Wasmtime headers and `libwasmtime.so`. - A previous failed compile left stale `.compile.txt`, `.wasm-check.txt`, or partial `.wasm` files under `BIN_DIRECTORY`. -Failed compile output is persisted under the unit's generated path in `BIN_DIRECTORY` and may be reused until the source or compiler inputs change. First fix the source/toolchain issue and reload the page. If the cache itself is suspect, stop UCE, move only the affected unit artifact files or directory aside, and restart so the runtime recompiles from source. Avoid deleting the whole `BIN_DIRECTORY` unless you intentionally want a full rebuild. +Failed compile output is persisted under the unit's ABI-generation path in +`BIN_DIRECTORY` and may be reused until the source or compiler inputs change. +The managed `restart` command first runs the new binary's `--precompile` mode as +the configured service user while the old service remains live. It restarts +systemd only after every scanned unit compiles and serializes successfully. +First fix source/toolchain failures and retry; do not remove the prior generation, +which remains the rollback path. Avoid deleting the whole `BIN_DIRECTORY` unless +you intentionally want a full rebuild and have accepted losing rollback artifacts. ### CLI commands fail diff --git a/docs/wasi-sdk-toolchain.md b/docs/wasi-sdk-toolchain.md index bb19bc7..4448ccb 100644 --- a/docs/wasi-sdk-toolchain.md +++ b/docs/wasi-sdk-toolchain.md @@ -47,7 +47,9 @@ Treat WASI SDK upgrades like runtime dependency upgrades: 2. Record the new release and checksum here. 3. Rebuild `bin/wasm/core.wasm` with `scripts/build_core_wasm.sh`. 4. Rebuild the native runtime with `scripts/build_linux.sh`. -5. Clear or invalidate stale unit wasm artifacts by bumping `UCE_UNIT_ABI_VERSION` when required, or by removing affected generated artifacts under `BIN_DIRECTORY`. +5. Bump the compiler or core ABI constant in `src/wasm/abi.h` when required. + The managed restart precompiles the resulting isolated unit generation before + switching workers; do not overwrite or delete the previous generation. 6. Run the full CLI suite including wasm kill tests: ```bash diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index c9ed59e..25b3d06 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -232,6 +232,14 @@ refreshes the epoch deadline before its first guest call. Otherwise a component whose compilation outlasted the guest CPU budget would immediately trap in the following allocator/relocation call even though no guest loop consumed it. +Unit artifacts live beneath an ABI-generation directory such as +`BIN_DIRECTORY/units-c12-w7`: `c12` is the compiler/unit-metadata ABI and `w7` +is the runtime core ABI. Old and new service binaries therefore never publish +or read the same unit path during an ABI transition. The managed restart builds +and serializes the complete next generation before stopping the old service; +failure aborts the switch and leaves the running generation intact. Old +generation directories are retained as an explicit rollback path. + The proactive compiler and request workers coordinate through per-unit file locks and a lock-protected demand-priority queue under `BIN_DIRECTORY`. A small priority-only compiler process drains that queue independently of the full-site diff --git a/scripts/build_linux.sh b/scripts/build_linux.sh index 4e6d467..9c6b947 100755 --- a/scripts/build_linux.sh +++ b/scripts/build_linux.sh @@ -1,4 +1,5 @@ #!/bin/bash +set -euo pipefail cd "$(dirname "$0")" cd .. @@ -6,11 +7,14 @@ BUILDMODE=${2:-"debug"} OPT_FLAG="O0" GF="uce_fastcgi" -mkdir bin > /dev/null 2>&1 -mkdir bin/tmp > /dev/null 2>&1 -mkdir bin/assets > /dev/null 2>&1 -mkdir bin/wasm > /dev/null 2>&1 -mkdir work > /dev/null 2>&1 +mkdir -p bin/tmp bin/assets bin/wasm work +exec 9>bin/.build.lock +flock 9 +build_tmp_files=() +cleanup_build_tmp() { + ((${#build_tmp_files[@]} == 0)) || rm -f "${build_tmp_files[@]}" +} +trap cleanup_build_tmp EXIT COMPILER="clang++" # -rdynamic is a link-time flag; the -c compiles below do not need it. @@ -52,13 +56,16 @@ fi # SQLite: vendored C, depends only on its own source (not our headers). if needs_rebuild bin/sqlite3.o src/3rdparty/sqlite/sqlite3.c src/3rdparty/sqlite/sqlite3.h; then echo "Compiling SQLite..." + tmp="bin/sqlite3.o.tmp.$$" + build_tmp_files+=("$tmp") clang -g -O2 -fPIC \ -DSQLITE_THREADSAFE=1 \ -DSQLITE_OMIT_LOAD_EXTENSION=1 \ -DSQLITE_DQS=0 \ -DSQLITE_DEFAULT_FOREIGN_KEYS=1 \ -DSQLITE_DEFAULT_WAL_SYNCHRONOUS=1 \ - -c src/3rdparty/sqlite/sqlite3.c -o bin/sqlite3.o 2>&1 || exit 1 + -c src/3rdparty/sqlite/sqlite3.c -o "$tmp" 2>&1 + mv "$tmp" bin/sqlite3.o else echo "Reusing bin/sqlite3.o" fi @@ -67,7 +74,10 @@ fi # declarations (not the lib .cpp — those are compiled into main.o). if needs_rebuild bin/wasm.o src/wasm src/lib/*.h; then echo "Compiling wasm backend..." - time -p $COMPILER -c src/wasm/wasm_module.cpp $SRCFLAGS $FLAGS $WASM_FLAGS -o bin/wasm.o 2>&1 || exit 1 + tmp="bin/wasm.o.tmp.$$" + build_tmp_files+=("$tmp") + time -p $COMPILER -c src/wasm/wasm_module.cpp $SRCFLAGS $FLAGS $WASM_FLAGS -o "$tmp" 2>&1 + mv "$tmp" bin/wasm.o else echo "Reusing bin/wasm.o" fi @@ -77,18 +87,19 @@ fi # (its only view of the wasm object) — but not the wasm .cpp sources. if needs_rebuild bin/main.o src/linux_fastcgi.cpp src/lib src/fastcgi src/wasm/backend.h; then echo "Compiling main..." - time -p $COMPILER -c src/linux_fastcgi.cpp $SRCFLAGS $FLAGS -o bin/main.o 2>&1 || exit 1 + tmp="bin/main.o.tmp.$$" + build_tmp_files+=("$tmp") + time -p $COMPILER -c src/linux_fastcgi.cpp $SRCFLAGS $FLAGS -o "$tmp" 2>&1 + mv "$tmp" bin/main.o else echo "Reusing bin/main.o" fi echo "Linking..." -$COMPILER -rdynamic bin/main.o bin/wasm.o bin/sqlite3.o $FLAGS $LIBS -o bin/$GF.linux.bin 2>&1 - -if [ $? -eq 0 ] -then - ls -lh bin/ | grep $GF - exit 0 -else - exit 1 -fi +binary_tmp="bin/$GF.linux.bin.tmp.$$" +build_tmp_files+=("$binary_tmp") +$COMPILER -rdynamic bin/main.o bin/wasm.o bin/sqlite3.o $FLAGS $LIBS -o "$binary_tmp" 2>&1 +test -s "$binary_tmp" +chmod 0755 "$binary_tmp" +mv "$binary_tmp" "bin/$GF.linux.bin" +ls -lh "bin/$GF.linux.bin" diff --git a/scripts/check_unit_wasm.py b/scripts/check_unit_wasm.py index afaef9b..346bc7f 100755 --- a/scripts/check_unit_wasm.py +++ b/scripts/check_unit_wasm.py @@ -160,7 +160,13 @@ def defined_symbols(path: Path, llvm_nm: str) -> list[str]: def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("wasm", type=Path) - ap.add_argument("--abi-version", default="7") + default_abi = "7" + abi_header = Path(__file__).resolve().parents[1] / "src" / "wasm" / "abi.h" + for line in abi_header.read_text().splitlines(): + if line.startswith("#define UCE_WASM_CORE_ABI_VERSION "): + default_abi = line.rsplit(" ", 1)[1] + break + ap.add_argument("--abi-version", default=default_abi) ap.add_argument("--llvm-nm", default=None) ap.add_argument("--verbose", action="store_true") args = ap.parse_args() diff --git a/scripts/compile_wasm_unit b/scripts/compile_wasm_unit index 89e6519..675bccb 100755 --- a/scripts/compile_wasm_unit +++ b/scripts/compile_wasm_unit @@ -11,7 +11,7 @@ PP_FN="$4" WASM_FN="$5" SDK=${WASI_SDK:-/opt/wasi-sdk} -ABI_VERSION=${UCE_UNIT_ABI_VERSION:-7} +ABI_VERSION=${UCE_UNIT_ABI_VERSION:-$(awk '/^#define UCE_WASM_CORE_ABI_VERSION / {print $3; exit}' src/wasm/abi.h)} ROOT=$(pwd) OBJ_FN="$DEST_DIR/$PP_FN.wasm.o" ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt" diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 31521fa..c49403a 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -67,6 +67,7 @@ if [[ "$action" == "run" ]]; then curl -sS --max-time "$curl_timeout" --fail-with-body --unix-socket "$socket_path" "${base_url}&group=${group}" done scripts/test_dependency_invalidation.sh + scripts/test_abi_generation_rollout.sh scripts/test_cold_component_deadline.sh scripts/test_nested_component_props.sh scripts/test_component_once_prefetch.sh diff --git a/scripts/systemd/manage-uce-service.sh b/scripts/systemd/manage-uce-service.sh index a483eb0..39e99ad 100755 --- a/scripts/systemd/manage-uce-service.sh +++ b/scripts/systemd/manage-uce-service.sh @@ -56,6 +56,15 @@ case "$action" in if [[ "$REPO_ROOT" != "/usr/lib/uce" && "$REPO_ROOT" != "/opt/uce" ]]; then "$REPO_ROOT/scripts/build_linux.sh" fi + ( + cd "$REPO_ROOT" + service_user=$(systemctl show "$UNIT_NAME" -p User --value) + if [[ -n "$service_user" && "$(id -u)" == "0" ]]; then + runuser -u "$service_user" -- "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile + else + "$REPO_ROOT/bin/uce_fastcgi.linux.bin" --precompile + fi + ) systemctl restart "$UNIT_NAME" ;; start|stop|status) diff --git a/scripts/systemd/wait-ready.sh b/scripts/systemd/wait-ready.sh index d2427dd..dabcc3e 100755 --- a/scripts/systemd/wait-ready.sh +++ b/scripts/systemd/wait-ready.sh @@ -10,7 +10,7 @@ if [[ -r /etc/uce/settings.cfg ]]; then fi for ((attempt = 0; attempt < 200; attempt++)); do - if [[ -S "$socket_path" ]]; then + if [[ -S "$socket_path" ]] && [[ "$(curl -sS --max-time 0.2 --unix-socket "$socket_path" http://localhost/ping 2>/dev/null || true)" == "uce-cli: ok" ]]; then exit 0 fi sleep 0.05 diff --git a/scripts/test_abi_generation_rollout.sh b/scripts/test_abi_generation_rollout.sh new file mode 100755 index 0000000..f3ef496 --- /dev/null +++ b/scripts/test_abi_generation_rollout.sh @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="abi-generation-test-$$" +site_directory="${UCE_TEST_SITE_DIRECTORY:-site}" +if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r /etc/uce/settings.cfg ]]; then + configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg) + [[ -z "${configured_site_directory:-}" ]] || site_directory="$configured_site_directory" +fi +source_dir="$site_directory/$test_name" +http_host="${UCE_TEST_HTTP_HOST:-uce.openfu.com}" +body_file="/tmp/uce-abi-generation-body-$$" +ready_file="/tmp/uce-abi-generation-ready-$$" + +cleanup() { + rm -rf "$source_dir" + rm -f "$body_file" "$ready_file" + [[ -z "${artifact_dir:-}" ]] || rm -rf "$artifact_dir" +} +trap cleanup EXIT +mkdir -p "$source_dir" + +printf '%s\n' \ + 'RENDER(Request& context) { print("entry-current"); }' \ + 'CLI(Request& context) {' \ + ' if(context.get["artifact"] == "entry") print(unit_info(context.params["SCRIPT_FILENAME"])["wasm_name"].to_string());' \ + ' else print("entry-current");' \ + '}' >"$source_dir/entry.uce" +printf '%s\n' \ + 'COMPONENT(Request& context) { print("component-current"); }' >"$source_dir/child.uce" +printf '%s\n' \ + 'RENDER(Request& context) { DValue props; print(component("child", props, context)); }' \ + 'CLI(Request& context) {' \ + ' DValue props; component("child", props, context);' \ + ' print(unit_info("child.uce")["wasm_name"].to_string());' \ + '}' >"$source_dir/component-parent.uce" + +entry_url="http://127.0.0.1/$test_name/entry.uce" +component_url="http://127.0.0.1/$test_name/component-parent.uce" +[[ "$(curl -fsS -H "Host: $http_host" "$entry_url")" == "entry-current" ]] +entry_wasm=$(scripts/uce-cli --get "/$test_name/entry.uce" artifact=entry) +component_wasm=$(scripts/uce-cli "/$test_name/component-parent.uce") +artifact_dir=$(dirname "$entry_wasm") +generation_name=$(basename "$(scripts/unit_cache_directory /tmp/uce-generation-probe)") +if [[ "$entry_wasm" != *"/$generation_name/"* || "$component_wasm" != *"/$generation_name/"* ]]; then + echo "unit artifacts are not isolated by compiler/core ABI generation: $entry_wasm $component_wasm" >&2 + exit 1 +fi + +publish_incompatible_artifact_while_locked() { + local source_file="$1" + local wasm_file="$2" + local marker="$3" + local destination + destination=$(dirname "$wasm_file") + ( + exec 8>"$wasm_file.lock" + flock 8 + UCE_UNIT_ABI_VERSION=6 scripts/compile_wasm_unit \ + "$(dirname "$source_file")" "$destination" "$source_file" \ + "$(basename "$source_file").cpp" "$(basename "$wasm_file")" + sed -i \ + -e 's/^unit_abi_version=.*/unit_abi_version=0/' \ + -e 's/^wasm_core_abi_version=.*/wasm_core_abi_version=0/' \ + "${wasm_file%.wasm}.meta.txt" + rm -f "${wasm_file%.wasm}.cwasm" + printf '%s\n' "$marker" >"$ready_file" + sleep 2 + ) & + lock_pid=$! + deadline=$((SECONDS + 20)) + while [[ ! -e "$ready_file" && $SECONDS -lt $deadline ]]; do sleep 0.05; done + if [[ ! -e "$ready_file" || "$(cat "$ready_file")" != "$marker" ]]; then + echo "incompatible artifact was not published under lock: $marker" >&2 + exit 1 + fi +} + +publish_incompatible_artifact_while_locked "$source_dir/entry.uce" "$entry_wasm" entry +started_at=$(date +%s%N) +entry_status=$(curl -sS --max-time 30 -o "$body_file" -w '%{http_code}' -H "Host: $http_host" "$entry_url") +entry_elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 )) +wait "$lock_pid" +if [[ "$entry_status" != "200" || "$(cat "$body_file")" != "entry-current" || $entry_elapsed_ms -lt 1500 ]]; then + echo "ABI-incompatible entry artifact was served instead of joining its rebuild: status=$entry_status elapsed=${entry_elapsed_ms}ms body=$(cat "$body_file")" >&2 + exit 1 +fi + +rm -f "$ready_file" +publish_incompatible_artifact_while_locked "$source_dir/child.uce" "$component_wasm" component +started_at=$(date +%s%N) +component_status=$(curl -sS --max-time 30 -o "$body_file" -w '%{http_code}' -H "Host: $http_host" "$component_url") +component_elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 )) +wait "$lock_pid" +if [[ "$component_status" != "200" || "$(cat "$body_file")" != "component-current" || $component_elapsed_ms -lt 1500 ]]; then + echo "ABI-incompatible component artifact was served instead of joining its rebuild: status=$component_status elapsed=${component_elapsed_ms}ms body=$(cat "$body_file")" >&2 + exit 1 +fi + +echo "ABI generation rollout passed" diff --git a/scripts/test_cold_component_deadline.sh b/scripts/test_cold_component_deadline.sh index 625f023..d2ccf1e 100755 --- a/scripts/test_cold_component_deadline.sh +++ b/scripts/test_cold_component_deadline.sh @@ -26,7 +26,7 @@ cleanup() { } trap cleanup EXIT mkdir -p "$source_dir" -cache_dir="$bin_directory$(realpath "$source_dir")" +cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")" printf '%s\n' \ 'CLI(Request& context) { DValue props; print(component("child", props, context)); }' >"$source_dir/parent.uce" diff --git a/scripts/test_dependency_invalidation.sh b/scripts/test_dependency_invalidation.sh index dde483d..1bcf2e5 100755 --- a/scripts/test_dependency_invalidation.sh +++ b/scripts/test_dependency_invalidation.sh @@ -40,7 +40,7 @@ cleanup() { } trap cleanup EXIT mkdir -p "$source_dir" -cache_dir="$bin_directory$(realpath "$source_dir")" +cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")" printf '%s\n' \ '#ifndef UCE_DEPENDENCY_CACHE_CHILD' \ diff --git a/scripts/test_log_timeliness.sh b/scripts/test_log_timeliness.sh index d824032..7359fc7 100755 --- a/scripts/test_log_timeliness.sh +++ b/scripts/test_log_timeliness.sh @@ -26,7 +26,7 @@ cleanup() { } trap cleanup EXIT mkdir -p "$source_dir" -cache_dir="$bin_directory$(realpath "$source_dir")" +cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")" printf '%s\n' 'CLI(Request& context) { deliberate_log_timeliness_compile_failure }' >"$source_dir/probe.uce" rm -rf "$cache_dir" diff --git a/scripts/test_mysql_epoch_refresh.sh b/scripts/test_mysql_epoch_refresh.sh index 13ad07a..5f36d6e 100755 --- a/scripts/test_mysql_epoch_refresh.sh +++ b/scripts/test_mysql_epoch_refresh.sh @@ -38,7 +38,7 @@ cleanup() { } trap cleanup EXIT mkdir -p "$source_dir" -cache_dir="$bin_directory$(realpath "$source_dir")" +cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")" mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'" test_user_created=true diff --git a/scripts/test_mysql_persistent_pool.sh b/scripts/test_mysql_persistent_pool.sh index 336e2d5..0179c71 100755 --- a/scripts/test_mysql_persistent_pool.sh +++ b/scripts/test_mysql_persistent_pool.sh @@ -30,7 +30,7 @@ cleanup() { } trap cleanup EXIT mkdir -p "$source_dir" -cache_dir="$bin_directory$(realpath "$source_dir")" +cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")" mariadb -e "DROP DATABASE IF EXISTS \`$test_database\`; DROP DATABASE IF EXISTS \`$test_database_other\`; DROP USER IF EXISTS '$test_user'@'127.0.0.1'; CREATE DATABASE \`$test_database\`; CREATE DATABASE \`$test_database_other\`; CREATE USER '$test_user'@'127.0.0.1' IDENTIFIED BY '$test_password'; GRANT ALL ON \`$test_database\`.* TO '$test_user'@'127.0.0.1'; GRANT ALL ON \`$test_database_other\`.* TO '$test_user'@'127.0.0.1'; CREATE TABLE \`$test_database\`.pool_identity (label VARCHAR(16) NOT NULL); INSERT INTO \`$test_database\`.pool_identity VALUES ('primary'); CREATE TABLE \`$test_database_other\`.pool_identity (label VARCHAR(16) NOT NULL); INSERT INTO \`$test_database_other\`.pool_identity VALUES ('other')" printf '%s\n' \ diff --git a/scripts/test_password_hashing.sh b/scripts/test_password_hashing.sh index cdc3e9c..82307c7 100755 --- a/scripts/test_password_hashing.sh +++ b/scripts/test_password_hashing.sh @@ -26,7 +26,7 @@ cleanup() { } trap cleanup EXIT mkdir -p "$source_dir" -cache_dir="$bin_directory$(realpath "$source_dir")" +cache_dir="$(scripts/unit_cache_directory "$bin_directory")$(realpath "$source_dir")" printf '%s\n' \ 'CLI(Request& context) {' \ diff --git a/scripts/test_unit_export_surface.sh b/scripts/test_unit_export_surface.sh index 86eb9ad..00b585b 100755 --- a/scripts/test_unit_export_surface.sh +++ b/scripts/test_unit_export_surface.sh @@ -27,7 +27,7 @@ cleanup() { trap cleanup EXIT mkdir -p "$source_dir" absolute_source_dir=$(realpath "$source_dir") -artifact_dir="$bin_directory$absolute_source_dir" +artifact_dir="$(scripts/unit_cache_directory "$bin_directory")$absolute_source_dir" printf '%s\n' \ 'String visibility_used() { return("private-used"); }' \ diff --git a/scripts/test_wasm_source_locations.sh b/scripts/test_wasm_source_locations.sh index c8c2b94..c506400 100755 --- a/scripts/test_wasm_source_locations.sh +++ b/scripts/test_wasm_source_locations.sh @@ -22,7 +22,7 @@ cleanup() { trap cleanup EXIT mkdir -p "$source_dir" absolute_source_dir=$(realpath "$source_dir") -artifact_dir="$bin_directory$absolute_source_dir" +artifact_dir="$(scripts/unit_cache_directory "$bin_directory")$absolute_source_dir" printf '%s\n' 'CLI(Request& context) { __builtin_trap(); }' >"$source_dir/entry.uce" set +e diff --git a/scripts/unit_cache_directory b/scripts/unit_cache_directory new file mode 100755 index 0000000..fe8a1f1 --- /dev/null +++ b/scripts/unit_cache_directory @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +base="${1:-/tmp/uce/work}" +compiler_abi=$(awk '/^#define UCE_COMPILER_UNIT_ABI_VERSION / {print $3; exit}' src/wasm/abi.h) +core_abi=$(awk '/^#define UCE_WASM_CORE_ABI_VERSION / {print $3; exit}' src/wasm/abi.h) +printf '%s/units-c%s-w%s\n' "${base%/}" "$compiler_abi" "$core_abi" diff --git a/src/lib/compiler.cpp b/src/lib/compiler.cpp index d6a6021..fbce0b2 100644 --- a/src/lib/compiler.cpp +++ b/src/lib/compiler.cpp @@ -1,6 +1,7 @@ #include "compiler.h" #include "compiler-parser.h" #include "hash.h" +#include "../wasm/abi.h" #include #include #include @@ -16,7 +17,7 @@ namespace { -const u64 UCE_UNIT_ABI_VERSION = 11; +const u64 UCE_UNIT_ABI_VERSION = UCE_COMPILER_UNIT_ABI_VERSION; struct SharedUnitFilesystemState { @@ -35,6 +36,8 @@ struct SharedUnitFilesystemState time_t required_time = 0; u64 metadata_abi_version = 0; u64 runtime_abi_version = UCE_UNIT_ABI_VERSION; + u64 metadata_wasm_core_abi_version = 0; + u64 runtime_wasm_core_abi_version = UCE_WASM_CORE_ABI_VERSION; String metadata_content; String compile_output_content; String metadata_build_token; @@ -262,7 +265,8 @@ String compiler_unit_input_signature(Request* context, SharedUnit* su, bool allo return( compiler_unit_source_signature(su->file_name, allow_recent_source_stat) + ":" + gen_sha1(file_get_contents(setup_template)) + ":" + - std::to_string(UCE_UNIT_ABI_VERSION) + std::to_string(UCE_UNIT_ABI_VERSION) + ":" + + std::to_string(UCE_WASM_CORE_ABI_VERSION) ); } @@ -281,6 +285,7 @@ String compiler_unit_metadata_text(Request* context, SharedUnit* su, String inpu return( "format=uce-unit-metadata-v1\n" "unit_abi_version=" + std::to_string(UCE_UNIT_ABI_VERSION) + "\n" + "wasm_core_abi_version=" + std::to_string(UCE_WASM_CORE_ABI_VERSION) + "\n" "source_path=" + su->file_name + "\n" "input_signature=" + input_signature + "\n" "build_token=" + compiler_unit_build_token() + "\n" @@ -387,7 +392,7 @@ time_t compiler_runtime_abi_time(Request* context) String compiler_registry_file_name(Request* context) { - return(context->server->config["BIN_DIRECTORY"] + "/known-uce-files.txt"); + return(compiler_unit_bin_directory(context) + "/known-uce-files.txt"); } String compiler_registry_lock_file_name(Request* context) @@ -397,12 +402,12 @@ String compiler_registry_lock_file_name(Request* context) String compiler_priority_file_name(Request* context) { - return(context->server->config["BIN_DIRECTORY"] + "/proactive-priority.txt"); + return(compiler_unit_bin_directory(context) + "/proactive-priority.txt"); } String compiler_source_generation_file_name(Request* context) { - return(context->server->config["BIN_DIRECTORY"] + "/source-generation.txt"); + return(compiler_unit_bin_directory(context) + "/source-generation.txt"); } int compiler_open_lock_file(String file_name, String purpose, bool nonblocking = false) @@ -553,6 +558,9 @@ SharedUnitFilesystemState inspect_shared_unit_filesystem(Request* context, Share auto input_it = metadata.find("input_signature"); if(input_it != metadata.end()) state.metadata_input_signature = input_it->second; + auto core_abi_it = metadata.find("wasm_core_abi_version"); + if(core_abi_it != metadata.end() && compiler_is_u64_string(core_abi_it->second)) + state.metadata_wasm_core_abi_version = (u64)atoll(core_abi_it->second.c_str()); auto source_it = metadata.find("source_path"); if(source_it != metadata.end()) state.metadata_source_path = source_it->second; @@ -562,7 +570,11 @@ SharedUnitFilesystemState inspect_shared_unit_filesystem(Request* context, Share } if(state.compile_output_exists) state.compile_output_content = file_get_contents(su->compile_output_file_name); - state.abi_compatible = (state.metadata_parsed && state.metadata_abi_version == state.runtime_abi_version); + state.abi_compatible = ( + state.metadata_parsed && + state.metadata_abi_version == state.runtime_abi_version && + state.metadata_wasm_core_abi_version == state.runtime_wasm_core_abi_version + ); state.input_signature_matches = ( state.metadata_parsed && state.metadata_input_signature != "" && @@ -791,7 +803,23 @@ String compiler_generated_cpp_path(Request* context, String source_file) { if(!context || !context->server || source_file == "") return(""); - return(context->server->config["BIN_DIRECTORY"] + dirname(source_file) + "/" + basename(source_file) + ".cpp"); + return(compiler_unit_bin_directory(context) + dirname(source_file) + "/" + basename(source_file) + ".cpp"); +} + +String compiler_unit_bin_directory(Request* context) +{ + if(!context || !context->server) + return(""); + return(path_join( + context->server->config["BIN_DIRECTORY"], + "units-c" + std::to_string(UCE_UNIT_ABI_VERSION) + + "-w" + std::to_string(UCE_WASM_CORE_ABI_VERSION) + )); +} + +String compiler_unit_wasm_path(Request* context, String source_file) +{ + return(compiler_unit_bin_directory(context) + source_file + ".wasm"); } String compiler_generated_cpp_path(SharedUnit* su) @@ -809,8 +837,8 @@ void setup_unit_paths(Request* context, SharedUnit* su, String file_name) return; su->src_path = dirname(file_name); - su->bin_path = context->server->config["BIN_DIRECTORY"] + su->src_path; - su->pre_path = context->server->config["BIN_DIRECTORY"] + su->src_path; + su->bin_path = compiler_unit_bin_directory(context) + su->src_path; + su->pre_path = compiler_unit_bin_directory(context) + su->src_path; su->src_file_name = basename(file_name); su->wasm_file_name = su->src_file_name + ".wasm"; @@ -1023,7 +1051,7 @@ SharedUnit* compiler_get_shared_unit_internal(Request* context, String file_name SharedUnit* su = new SharedUnit(); setup_unit_paths(context, su, file_name); - bool can_serve_stale = !force_recompile && compiler_request_can_serve_stale_artifact(context); + bool can_serve_stale = !force_recompile && compiler_unit_can_serve_stale_artifact(context, file_name); int fdlock = compiler_open_lock_file(su->wasm_name + ".lock", "shared-unit:" + file_name, can_serve_stale); if(fdlock == -2 && file_exists(su->wasm_name)) { @@ -1178,6 +1206,26 @@ bool compiler_request_can_serve_stale_artifact(Request* context) float_val(context->server->config["PROACTIVE_COMPILE_CHECK_INTERVAL"]) > 0); } +bool compiler_unit_can_serve_stale_artifact(Request* context, String file_name) +{ + if(!compiler_request_can_serve_stale_artifact(context)) + return(false); + file_name = compiler_normalize_unit_path(context, file_name); + if(file_name == "") + return(false); + SharedUnit su; + setup_unit_paths(context, &su, file_name); + auto state = inspect_shared_unit_filesystem(context, &su, true); + return( + state.source_exists && + state.compiled_time != 0 && + state.metadata_exists && + state.metadata_parsed && + state.abi_compatible && + state.metadata_source_path == file_name + ); +} + void compiler_prioritize_unit(Request* context, String file_name) { if(!compiler_request_can_serve_stale_artifact(context)) @@ -1493,6 +1541,8 @@ DValue unit_info(String path) info["required_mtime"] = (f64)fs_state.required_time; info["runtime_abi_version"] = (f64)fs_state.runtime_abi_version; info["metadata_abi_version"] = (f64)fs_state.metadata_abi_version; + info["runtime_wasm_core_abi_version"] = (f64)fs_state.runtime_wasm_core_abi_version; + info["metadata_wasm_core_abi_version"] = (f64)fs_state.metadata_wasm_core_abi_version; info["current_input_signature"] = fs_state.current_input_signature; info["metadata_input_signature"] = fs_state.metadata_input_signature; info["metadata_build_token"] = fs_state.metadata_build_token; diff --git a/src/lib/compiler.h b/src/lib/compiler.h index 9f4bd10..ba8d6eb 100644 --- a/src/lib/compiler.h +++ b/src/lib/compiler.h @@ -18,6 +18,10 @@ String preprocess_shared_unit(Request* context, SharedUnit* su); String compiler_generated_cpp_path(Request* context, String source_file); String compiler_generated_cpp_path(SharedUnit* su); +#ifndef __UCE_WASM_UNIT__ +String compiler_unit_bin_directory(Request* context); +String compiler_unit_wasm_path(Request* context, String source_file); +#endif void setup_unit_paths(Request* context, SharedUnit* su, String file_name); void compile_shared_unit(Request* context, SharedUnit* su); SharedUnit* get_shared_unit(Request* context, String file_name); @@ -25,6 +29,7 @@ String compiler_error_page_unit(Request* context, String config_key); bool compiler_unit_compile_pending(Request* context, String file_name); bool compiler_unit_compile_in_progress(Request* context, String file_name); bool compiler_request_can_serve_stale_artifact(Request* context); +bool compiler_unit_can_serve_stale_artifact(Request* context, String file_name); void compiler_prioritize_unit(Request* context, String file_name); StringList compiler_take_priority_units(Request* context); String compiler_source_generation(Request* context); diff --git a/src/linux_fastcgi.cpp b/src/linux_fastcgi.cpp index 80d23db..e98fadd 100644 --- a/src/linux_fastcgi.cpp +++ b/src/linux_fastcgi.cpp @@ -1221,7 +1221,7 @@ void proactive_compile_queue_push(StringList& queue, String file_name) bool proactive_compile_unit(Request& context, String file_name, bool& source_missing) { bool failed = false; - String wasm_path = server_state.config["BIN_DIRECTORY"] + file_name + ".wasm"; + String wasm_path = compiler_unit_wasm_path(&context, file_name); if(compiler_unit_needs_recompile(&context, file_name, &source_missing)) { printf("(i) proactive compile %s\n", file_name.c_str()); @@ -1582,6 +1582,36 @@ void init_base_process() srand(time()); } +int precompile_unit_generation() +{ + f64 started_at = time_precise(); + server_state.config = make_server_settings(); + server_state.config["COMPILER_SYS_PATH"] = cwd_get(); + Request background_context; + background_context.server = &server_state; + mkdir(server_state.config["BIN_DIRECTORY"]); + mkdir(compiler_unit_bin_directory(&background_context)); + + set_active_request(background_context); + auto files = compiler_scan_site_units(&background_context); + compiler_set_known_units(&background_context, files); + u64 failed = 0; + u64 compiled = 0; + for(auto& file_name : files) + { + bool source_missing = false; + if(compiler_unit_needs_recompile(&background_context, file_name, &source_missing)) + compiled++; + if(proactive_compile_unit(background_context, file_name, source_missing)) + failed++; + } + printf("Precompiled unit generation %s: %zu units, %llu compiled, %llu failed in %.3f s\n", + compiler_unit_bin_directory(&background_context).c_str(), files.size(), + (unsigned long long)compiled, (unsigned long long)failed, + time_precise() - started_at); + return(failed == 0 ? 0 : 1); +} + int main(int argc, char** argv) { // systemd connects stdout to a pipe, which otherwise block-buffers child @@ -1592,6 +1622,8 @@ int main(int argc, char** argv) // after a fault does not allocate. backtrace(request_fault_frames, 4); process_start_directory(); + if(argc == 2 && String(argv[1]) == "--precompile") + return(precompile_unit_generation()); init_base_process(); ensure_proactive_compiler(); diff --git a/src/wasm/abi.h b/src/wasm/abi.h new file mode 100644 index 0000000..ee71219 --- /dev/null +++ b/src/wasm/abi.h @@ -0,0 +1,4 @@ +#pragma once + +#define UCE_WASM_CORE_ABI_VERSION 7 +#define UCE_COMPILER_UNIT_ABI_VERSION 12 diff --git a/src/wasm/backend.cpp b/src/wasm/backend.cpp index 257d577..a181652 100644 --- a/src/wasm/backend.cpp +++ b/src/wasm/backend.cpp @@ -42,7 +42,7 @@ static String wasm_backend_ensure_started(Request* context) wc.core_wasm_path = first(cfg["WASM_CORE_PATH"], path_join(cfg["COMPILER_SYS_PATH"], "bin/wasm/core.wasm")); wc.site_root = path_join(cfg["COMPILER_SYS_PATH"], cfg["SITE_DIRECTORY"]); - wc.cache_root = cfg["BIN_DIRECTORY"]; + wc.cache_root = compiler_unit_bin_directory(context); // write membrane allowlist: the site tree plus the runtime scratch dirs // pages legitimately write to (matches native reachable write targets). wc.write_roots = { wc.site_root, "/tmp" }; @@ -114,7 +114,7 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit) { if(entry_unit == "") return(false); - String wasm_path = context->server->config["BIN_DIRECTORY"] + entry_unit + ".wasm"; + String wasm_path = compiler_unit_wasm_path(context, entry_unit); struct stat wasm_st; f64 phase_started = time_precise(); context->stats.wasm_ready_check_count++; @@ -133,7 +133,7 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit) { context->stats.wasm_ready_freshness_us += (u64)((time_precise() - phase_started) * 1000000.0); compiler_prioritize_unit(context, entry_unit); - return(compiler_request_can_serve_stale_artifact(context)); + return(compiler_unit_can_serve_stale_artifact(context, entry_unit)); } context->stats.wasm_ready_freshness_us += (u64)((time_precise() - phase_started) * 1000000.0); if(source_missing) diff --git a/src/wasm/core.cpp b/src/wasm/core.cpp index 5283094..a6e33b5 100644 --- a/src/wasm/core.cpp +++ b/src/wasm/core.cpp @@ -6,6 +6,7 @@ // into core.wasm. #define __UCE_WASM_CORE__ 1 +#include "abi.h" #include "../lib/uce_lib.cpp" #include "../lib/mysql-connector.h" @@ -809,7 +810,7 @@ void uce_free(void* ptr) u32 uce_wasm_core_abi_version() { - return(7); + return(UCE_WASM_CORE_ABI_VERSION); } int uce_wasm_core_init() diff --git a/src/wasm/w1_smoke.cpp b/src/wasm/w1_smoke.cpp index cc2a0b3..226dcf9 100644 --- a/src/wasm/w1_smoke.cpp +++ b/src/wasm/w1_smoke.cpp @@ -1,6 +1,7 @@ // W1 smoke driver for the production UCE core.wasm. #include +#include "abi.h" #include #include @@ -254,7 +255,7 @@ int main(int argc, char** argv) CHECK(call_i32(core, "uce_wasm_core_init") == 0, "core init failed"); call_i32(core, "uce_wasm_core_reset_request"); - CHECK(call_i32(core, "uce_wasm_core_abi_version") == 7, "unexpected ABI version"); + CHECK(call_i32(core, "uce_wasm_core_abi_version") == UCE_WASM_CORE_ABI_VERSION, "unexpected ABI version"); wasm_memory_t* memory = core.memory(); int32_t root = call_i32(core, "uce_dv_root"); @@ -302,7 +303,7 @@ int main(int argc, char** argv) int32_t output_ptr = call_i32(core, "uce_wasm_output_data"); CHECK(read_bytes(memory, output_ptr, output_len) == out, "output plumbing mismatch"); - printf("W1 core.wasm smoke: abi=7 encoded=%d output=%d\n", encoded_len, output_len); + printf("W1 core.wasm smoke: abi=%d encoded=%d output=%d\n", UCE_WASM_CORE_ABI_VERSION, encoded_len, output_len); printf("W1 EXIT CRITERION: PASS\n"); return(0); } diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index 910f25e..56d059d 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -2615,8 +2615,8 @@ 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); - if(can_serve_stale && read_request && !component_source_generation_checked) + bool stale_policy = compiler_request_can_serve_stale_artifact(context); + if(stale_policy && read_request && !component_source_generation_checked) { component_source_generation = compiler_source_generation(context); component_source_generation_checked = true; @@ -2629,7 +2629,7 @@ private: // 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 || + bool check_freshness = !stale_policy || !read_request || cached_freshness == worker.component_freshness.end() || (generation_available ? cached_freshness->second.source_generation != component_source_generation : @@ -2637,13 +2637,14 @@ private: 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); + stale = compiler_unit_needs_recompile(context, resolved, 0, stale_policy && read_request, true); worker.component_freshness[resolved] = { now, stale, component_source_generation }; } else stale = cached_freshness->second.stale; } - if(stale && can_serve_stale) + bool can_serve_stale = stale && compiler_unit_can_serve_stale_artifact(context, resolved); + if(stale && stale_policy) { compiler_prioritize_unit(context, resolved); if(!read_request)