Exclude cold module loads from guest deadlines

This commit is contained in:
udo 2026-07-13 08:51:46 +00:00
parent 73a2671112
commit f7e9c5a4ec
4 changed files with 54 additions and 7 deletions

View File

@ -131,6 +131,10 @@ available. If an artifact is cold or stale, dispatch compiles it on demand via
Workers identify cached unit artifacts by nanosecond mtime, ctime, and size. Workers identify cached unit artifacts by nanosecond mtime, ctime, and size.
Whole-second mtime alone is insufficient because a dependency-triggered rebuild Whole-second mtime alone is insufficient because a dependency-triggered rebuild
can replace a wasm artifact within the same second as its prior build. can replace a wasm artifact within the same second as its prior build.
Cold module compilation and deserialization are host work, so `load_unit()`
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.
--- ---
@ -330,9 +334,11 @@ header free-functions are `inline`. The wasm backend exposes only declarations
`scripts/test_dependency_invalidation.sh`. The latter changes a transitive `scripts/test_dependency_invalidation.sh`. The latter changes a transitive
`#load`, then replaces a warmed worker artifact while preserving its `#load`, then replaces a warmed worker artifact while preserving its
whole-second mtime to prove both compiler and worker caches invalidate it. whole-second mtime to prove both compiler and worker caches invalidate it.
`scripts/test_cold_component_deadline.sh` separately compiles a deliberately
cold component that exceeds the development epoch window and proves the
parent request still renders it.
- **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
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full `SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
broker → worker → broker → client chain across process boundaries. broker → worker → broker → client chain across process boundaries.

View File

@ -51,4 +51,5 @@ url="http://localhost/tests/cli_runner.uce?action=${action}&include_kill=${inclu
curl -sS --fail-with-body --unix-socket "$socket_path" "$url" curl -sS --fail-with-body --unix-socket "$socket_path" "$url"
if [[ "$action" == "run" ]]; then if [[ "$action" == "run" ]]; then
scripts/test_dependency_invalidation.sh scripts/test_dependency_invalidation.sh
scripts/test_cold_component_deadline.sh
fi fi

View File

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
test_name="component-deadline-test-$$"
site_directory="${UCE_TEST_SITE_DIRECTORY:-site}"
source_dir="$site_directory/$test_name"
bin_directory="${BIN_DIRECTORY:-}"
if [[ -z "$bin_directory" && -r /etc/uce/settings.cfg ]]; then
bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' /etc/uce/settings.cfg)
fi
bin_directory="${bin_directory:-/tmp/uce/work}"
cache_dir=""
cleanup() {
rm -rf "$source_dir"
if [[ -n "$cache_dir" ]]; then
rm -rf "$cache_dir"
fi
}
trap cleanup EXIT
mkdir -p "$source_dir"
cache_dir="$bin_directory$(realpath "$source_dir")"
printf '%s\n' \
'CLI(Request& context) { DValue props; print(component("child", props, context)); }' >"$source_dir/parent.uce"
for i in $(seq 1 800); do
printf 'String cold_component_pad_%s() { return("%s"); }\n' "$i" "$i"
done >"$source_dir/child.uce"
printf '%s\n' \
'COMPONENT(Request& context) { <><strong>cold-component-deadline-ok</strong></> }' >>"$source_dir/child.uce"
rm -rf "$cache_dir"
output=$(scripts/uce-cli "/$test_name/parent.uce")
if [[ "$output" != *cold-component-deadline-ok* ]]; then
echo "cold component compilation consumed the guest epoch budget: $output" >&2
exit 1
fi
echo "cold component deadline passed"

View File

@ -1190,6 +1190,10 @@ private:
auto mod = worker.unit_module(source_path, error); auto mod = worker.unit_module(source_path, error);
if(!mod) if(!mod)
return(error); return(error);
// Compiling/deserializing a cold module is host work. Refresh the guest
// watchdog before the first core call so that wall time cannot make the
// otherwise harmless malloc/reloc sequence trap immediately.
ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
if(mod->abi.version != abi_version) if(mod->abi.version != abi_version)
return(mod->wasm_path + ": uce.abi version " + std::to_string(mod->abi.version) return(mod->wasm_path + ": uce.abi version " + std::to_string(mod->abi.version)
+ " does not match core ABI " + std::to_string(abi_version)); + " does not match core ABI " + std::to_string(abi_version));
@ -1347,12 +1351,8 @@ private:
if(worker.cfg.verbose) if(worker.cfg.verbose)
fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n", fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n",
source_path.c_str(), memory_base, table_base); source_path.c_str(), memory_base, table_base);
// The epoch budget is a guest-CPU watchdog, but the ticker is wall-clock // Exclude the rest of host-side loading as well. A genuine runaway loop
// and host-side module compilation here (lazy, mid-render, possibly many // makes no loads, so it still trips the deadline.
// units) burns it without the guest running. Reset the deadline after a
// load so the budget measures guest execution between membrane crossings,
// not our compile time. A genuine runaway loop makes no loads, so it
// still trips the deadline.
ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks); ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
return(""); return("");
} }