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.
Whole-second mtime alone is insufficient because a dependency-triggered rebuild
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
`#load`, then replaces a warmed worker artifact while preserving its
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
`:HTTP_PORT` with path `/site/tests/websockets.ws.uce` (self-resolving
`SCRIPT_FILENAME`) and asserts the `hello-ack` frame — exercising the full
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"
if [[ "$action" == "run" ]]; then
scripts/test_dependency_invalidation.sh
scripts/test_cold_component_deadline.sh
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);
if(!mod)
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)
return(mod->wasm_path + ": uce.abi version " + std::to_string(mod->abi.version)
+ " does not match core ABI " + std::to_string(abi_version));
@ -1347,12 +1351,8 @@ private:
if(worker.cfg.verbose)
fprintf(stderr, "[wasm] loaded %s (mem_base=%u table_base=%u)\n",
source_path.c_str(), memory_base, table_base);
// The epoch budget is a guest-CPU watchdog, but the ticker is wall-clock
// and host-side module compilation here (lazy, mid-render, possibly many
// 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.
// Exclude the rest of host-side loading as well. A genuine runaway loop
// makes no loads, so it still trips the deadline.
ctx().set_epoch_deadline(worker.cfg.epoch_deadline_ticks);
return("");
}