avoid request stalls during proactive rebuilds

This commit is contained in:
udo
2026-07-13 12:12:14 +00:00
parent c1039f5094
commit 5195ebeb25
7 changed files with 80 additions and 12 deletions
+13 -1
View File
@@ -136,6 +136,15 @@ refreshes the epoch deadline before its first guest call. Otherwise a component
whose compilation outlasted the guest CPU budget would immediately trap in the whose compilation outlasted the guest CPU budget would immediately trap in the
following allocator/relocation call even though no guest loop consumed it. following allocator/relocation call even though no guest loop consumed it.
The proactive compiler and request workers coordinate through a per-unit file
lock. Unit compilation writes and validates a process-unique temporary wasm
file, then publishes it with an atomic rename. While another process holds the
lock for a stale unit, a request may therefore keep using the last complete
artifact instead of waiting across a transitive rebuild. Once the lock is
released, normal freshness checks require the new artifact; a failed rebuild
removes availability and surfaces the compiler error rather than serving the
old unit indefinitely.
--- ---
## 4. The workspace runtime ## 4. The workspace runtime
@@ -346,7 +355,10 @@ header free-functions are `inline`. The wasm backend exposes only declarations
recycling. recycling.
`scripts/test_cold_component_deadline.sh` separately compiles a deliberately `scripts/test_cold_component_deadline.sh` separately compiles a deliberately
cold component that exceeds the development epoch window and proves the cold component that exceeds the development epoch window and proves the
parent request still renders it. parent request still renders it. The dependency-invalidation gate also holds
parent and child compile locks across a transitive source edit, proves a
warmed request returns the last atomically published result without waiting,
and then proves the new dependency result appears after rebuild completion.
- **WebSocket end-to-end**: a headless client performs a raw WS handshake to - **WebSocket end-to-end**: a headless client performs a raw WS handshake to
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving `:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
+6 -3
View File
@@ -15,6 +15,7 @@ ABI_VERSION=${UCE_UNIT_ABI_VERSION:-6}
ROOT=$(pwd) ROOT=$(pwd)
OBJ_FN="$DEST_DIR/$PP_FN.wasm.o" OBJ_FN="$DEST_DIR/$PP_FN.wasm.o"
ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt" ABI_TMP="$DEST_DIR/$PP_FN.uce-abi.txt"
WASM_TMP="$DEST_DIR/$WASM_FN.tmp.$$"
PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1} PCH_ENABLED=${UCE_WASM_UNIT_PCH:-1}
PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch} PCH_DIR=${UCE_WASM_PCH_DIR:-/tmp/uce/wasm-w2/pch}
COMMON_FLAGS=( COMMON_FLAGS=(
@@ -44,6 +45,7 @@ PCH_KEY=$(printf '%s\n%s\n%s\n%s\n' "$ABI_VERSION" "$TOOLCHAIN_ID" "$HEADER_HASH
PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch" PCH_FN="$PCH_DIR/uce_lib-wasm-unit-$PCH_KEY.pch"
mkdir -p "$DEST_DIR" >/dev/null 2>&1 mkdir -p "$DEST_DIR" >/dev/null 2>&1
trap 'rm -f "$OBJ_FN" "$ABI_TMP" "$WASM_TMP"' EXIT
build_pch_if_needed() { build_pch_if_needed() {
if [ "$PCH_ENABLED" = "0" ]; then if [ "$PCH_ENABLED" = "0" ]; then
@@ -80,7 +82,7 @@ fi
"$SDK/bin/wasm-ld" -shared --experimental-pic \ "$SDK/bin/wasm-ld" -shared --experimental-pic \
--unresolved-symbols=import-dynamic \ --unresolved-symbols=import-dynamic \
--Bsymbolic \ --Bsymbolic \
"$OBJ_FN" -o "$DEST_DIR/$WASM_FN" \ "$OBJ_FN" -o "$WASM_TMP" \
--export-if-defined=__uce_set_current_request \ --export-if-defined=__uce_set_current_request \
--export-if-defined=__uce_render \ --export-if-defined=__uce_render \
--export-if-defined=__uce_component \ --export-if-defined=__uce_component \
@@ -90,8 +92,9 @@ fi
--export-if-defined=__uce_once \ --export-if-defined=__uce_once \
--export-if-defined=__uce_init --export-if-defined=__uce_init
"$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$DEST_DIR/$WASM_FN" "$SDK/bin/llvm-objcopy" --add-section=uce.abi="$ABI_TMP" "$WASM_TMP"
python3 scripts/check_unit_wasm.py "$DEST_DIR/$WASM_FN" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm" python3 scripts/check_unit_wasm.py "$WASM_TMP" --abi-version "$ABI_VERSION" --llvm-nm "$SDK/bin/llvm-nm"
mv "$WASM_TMP" "$DEST_DIR/$WASM_FN"
rm -f "$OBJ_FN" "$ABI_TMP" rm -f "$OBJ_FN" "$ABI_TMP"
+27 -3
View File
@@ -48,13 +48,37 @@ assert_marker parent dependency-marker-a
sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce" sed -i 's/dependency-marker-a/dependency-marker-b/' "$source_dir/child.uce"
assert_marker parent dependency-marker-b assert_marker parent dependency-marker-b
# A proactive rebuild owns these same per-unit locks. While it publishes fresh
# artifacts, requests must use the last complete artifacts instead of waiting
# across the transitive graph. Atomic publication keeps those artifacts safe.
parent_wasm="$cache_dir/parent.uce.wasm"
child_wasm="$cache_dir/child.uce.wasm"
(
exec 8>"$parent_wasm.lock"
exec 9>"$child_wasm.lock"
flock 8
flock 9
sleep 3
) &
rebuild_lock_pid=$!
sleep 0.2
sed -i 's/dependency-marker-b/dependency-marker-d/' "$source_dir/child.uce"
started_at=$(date +%s%N)
assert_marker parent dependency-marker-b
elapsed_ms=$(( ($(date +%s%N) - started_at) / 1000000 ))
if (( elapsed_ms >= 2000 )); then
echo "request waited ${elapsed_ms}ms for an active transitive rebuild" >&2
exit 1
fi
wait "$rebuild_lock_pid"
assert_marker parent dependency-marker-d
# Warm every configured worker, then replace the artifact while retaining its # Warm every configured worker, then replace the artifact while retaining its
# whole-second mtime. The worker cache must notice the nanosecond/ctime change. # whole-second mtime. The worker cache must notice the nanosecond/ctime change.
for _ in {1..16}; do assert_marker parent dependency-marker-b; done for _ in {1..16}; do assert_marker parent dependency-marker-d; done
sed 's/dependency-marker-b/dependency-marker-c/' "$source_dir/child.uce" >"$source_dir/alternate-child.uce" sed 's/dependency-marker-d/dependency-marker-c/' "$source_dir/child.uce" >"$source_dir/alternate-child.uce"
sed 's/child.uce/alternate-child.uce/' "$source_dir/parent.uce" >"$source_dir/alternate.uce" sed 's/child.uce/alternate-child.uce/' "$source_dir/parent.uce" >"$source_dir/alternate.uce"
assert_marker alternate dependency-marker-c assert_marker alternate dependency-marker-c
parent_wasm="$cache_dir/parent.uce.wasm"
alternate_wasm="$cache_dir/alternate.uce.wasm" alternate_wasm="$cache_dir/alternate.uce.wasm"
parent_mtime=$(stat -c %Y "$parent_wasm") parent_mtime=$(stat -c %Y "$parent_wasm")
cp "$alternate_wasm" "$parent_wasm" cp "$alternate_wasm" "$parent_wasm"
+31 -3
View File
@@ -296,7 +296,7 @@ String compiler_registry_lock_file_name(Request* context)
return(compiler_registry_file_name(context) + ".lock"); return(compiler_registry_file_name(context) + ".lock");
} }
int compiler_open_lock_file(String file_name, String purpose) int compiler_open_lock_file(String file_name, String purpose, bool nonblocking = false)
{ {
(void)purpose; (void)purpose;
auto lock_dir = dirname(file_name); auto lock_dir = dirname(file_name);
@@ -309,8 +309,13 @@ int compiler_open_lock_file(String file_name, String purpose)
return(fdlock); return(fdlock);
} }
fcntl(fdlock, F_SETFD, FD_CLOEXEC); fcntl(fdlock, F_SETFD, FD_CLOEXEC);
if(flock(fdlock, LOCK_EX) != 0) if(flock(fdlock, LOCK_EX | (nonblocking ? LOCK_NB : 0)) != 0)
{ {
if(nonblocking && (errno == EWOULDBLOCK || errno == EAGAIN))
{
close(fdlock);
return(-2);
}
close(fdlock); close(fdlock);
printf("(!) Could not lock file %s\n", file_name.c_str()); printf("(!) Could not lock file %s\n", file_name.c_str());
return(-1); return(-1);
@@ -845,7 +850,19 @@ SharedUnit* compiler_get_shared_unit_internal(Request* context, String file_name
SharedUnit* su = new SharedUnit(); SharedUnit* su = new SharedUnit();
setup_unit_paths(context, su, file_name); setup_unit_paths(context, su, file_name);
int fdlock = compiler_open_lock_file(su->wasm_name + ".lock", "shared-unit:" + file_name); int fdlock = compiler_open_lock_file(su->wasm_name + ".lock", "shared-unit:" + file_name, !force_recompile);
if(fdlock == -2 && file_exists(su->wasm_name))
{
auto state = inspect_shared_unit_filesystem(context, su);
su->api_declarations = split(file_get_contents(su->api_file_name), "\n");
su->last_compiled = state.compiled_time;
su->compile_status = "rebuilding";
compiler_record_observed_filesystem_state(su, state);
context->server->units[file_name] = su;
return(su);
}
if(fdlock == -2)
fdlock = compiler_open_lock_file(su->wasm_name + ".lock", "shared-unit:" + file_name);
if(fdlock == -1) if(fdlock == -1)
{ {
su->compiler_messages = "could not open compile lock"; su->compiler_messages = "could not open compile lock";
@@ -947,6 +964,17 @@ bool compiler_unit_compile_pending(Request* context, String file_name)
return(true); return(true);
} }
bool compiler_unit_compile_in_progress(Request* context, String file_name)
{
SharedUnit su;
setup_unit_paths(context, &su, compiler_normalize_unit_path(context, file_name));
int fdlock = compiler_open_lock_file(su.wasm_name + ".lock", "compile-probe", true);
if(fdlock == -2)
return(true);
compiler_close_lock_file(fdlock);
return(false);
}
void unit_render(String file_name) void unit_render(String file_name)
{ {
unit_render(file_name, *context); unit_render(file_name, *context);
+1
View File
@@ -17,6 +17,7 @@ void compile_shared_unit(Request* context, SharedUnit* su);
SharedUnit* get_shared_unit(Request* context, String file_name); SharedUnit* get_shared_unit(Request* context, String file_name);
String compiler_error_page_unit(Request* context, String config_key); String compiler_error_page_unit(Request* context, String config_key);
bool compiler_unit_compile_pending(Request* context, String file_name); bool compiler_unit_compile_pending(Request* context, String file_name);
bool compiler_unit_compile_in_progress(Request* context, String file_name);
String compiler_site_directory(Request* context); String compiler_site_directory(Request* context);
StringList compiler_scan_site_units(Request* context); StringList compiler_scan_site_units(Request* context);
StringList compiler_list_known_units(Request* context); StringList compiler_list_known_units(Request* context);
+1 -1
View File
@@ -100,7 +100,7 @@ static bool wasm_artifact_exists(Request* context, const String& entry_unit)
// metadata mismatches, which can leave stale wasm with old imports. // metadata mismatches, which can leave stale wasm with old imports.
bool source_missing = false; bool source_missing = false;
if(compiler_unit_needs_recompile(context, entry_unit, &source_missing)) if(compiler_unit_needs_recompile(context, entry_unit, &source_missing))
return(false); return(compiler_unit_compile_in_progress(context, entry_unit));
if(source_missing) if(source_missing)
return(false); return(false);
return(true); return(true);
+1 -1
View File
@@ -1606,7 +1606,7 @@ private:
return(1); return(1);
} }
if(!file_exists_host(worker.unit_wasm_path(resolved)) || compiler_unit_needs_recompile(context, resolved, 0)) if(!file_exists_host(worker.unit_wasm_path(resolved)) || (compiler_unit_needs_recompile(context, resolved, 0) && !compiler_unit_compile_in_progress(context, resolved)))
get_shared_unit(context, resolved); get_shared_unit(context, resolved);
size_t unit_index = 0; size_t unit_index = 0;