From 82aec814651704a8b26f62c9515e60dca85d2639 Mon Sep 17 00:00:00 2001 From: udo Date: Wed, 15 Jul 2026 09:18:35 +0000 Subject: [PATCH] Refresh guest deadline after MySQL hostcalls --- docs/wasm-runtime-architecture.md | 4 ++ scripts/run_cli_tests.sh | 1 + scripts/test_mysql_epoch_refresh.sh | 70 +++++++++++++++++++++++++++++ src/wasm/worker.cpp | 3 +- 4 files changed, 77 insertions(+), 1 deletion(-) create mode 100755 scripts/test_mysql_epoch_refresh.sh diff --git a/docs/wasm-runtime-architecture.md b/docs/wasm-runtime-architecture.md index 556098c..16a7d78 100644 --- a/docs/wasm-runtime-architecture.md +++ b/docs/wasm-runtime-architecture.md @@ -407,6 +407,10 @@ header free-functions are `inline`. The wasm backend exposes only declarations caller props are restored. Its warm-request ceiling guards the request-scope invariant that entering a nested component swaps the active prop trees in constant time instead of deep-copying the outer tree for every child. + `scripts/test_mysql_epoch_refresh.sh` spends more than half of the configured + guest epoch on each side of a real MySQL query. It proves the blocking + database hostcall refreshes the guest deadline so cumulative request work is + allowed while a CPU loop without intervening host work still traps. - **Log timeliness**: the base process line-buffers stdout before forking workers, the proactive compiler, and the WebSocket broker. This keeps each diff --git a/scripts/run_cli_tests.sh b/scripts/run_cli_tests.sh index 7407492..166ad56 100755 --- a/scripts/run_cli_tests.sh +++ b/scripts/run_cli_tests.sh @@ -70,5 +70,6 @@ if [[ "$action" == "run" ]]; then scripts/test_cold_component_deadline.sh scripts/test_nested_component_props.sh scripts/test_password_hashing.sh + scripts/test_mysql_epoch_refresh.sh scripts/test_log_timeliness.sh fi diff --git a/scripts/test_mysql_epoch_refresh.sh b/scripts/test_mysql_epoch_refresh.sh new file mode 100755 index 0000000..13ad07a --- /dev/null +++ b/scripts/test_mysql_epoch_refresh.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +set -euo pipefail +cd "$(dirname "$0")/.." + +test_name="mysql-epoch-refresh-test-$$" +site_directory="${UCE_TEST_SITE_DIRECTORY:-site}" +settings_file="${UCE_SETTINGS_FILE:-/etc/uce/settings.cfg}" +if [[ -z "${UCE_TEST_SITE_DIRECTORY:-}" && -r "$settings_file" ]]; then + configured_site_directory=$(awk -F= '/^[[:space:]]*SITE_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file") + if [[ -n "${configured_site_directory:-}" ]]; then + site_directory="$configured_site_directory" + fi +fi +source_dir="$site_directory/$test_name" +bin_directory="${BIN_DIRECTORY:-}" +if [[ -z "$bin_directory" && -r "$settings_file" ]]; then + bin_directory=$(awk -F= '/^[[:space:]]*BIN_DIRECTORY[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file") +fi +bin_directory="${bin_directory:-/tmp/uce/work}" +ticks=$(awk -F= '/^[[:space:]]*WASM_EPOCH_DEADLINE_TICKS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true) +period_ms=$(awk -F= '/^[[:space:]]*WASM_EPOCH_PERIOD_MS[[:space:]]*=/ {gsub(/^[[:space:]]+|[[:space:]]+$/, "", $2); print $2; exit}' "$settings_file" 2>/dev/null || true) +ticks="${ticks:-200}" +period_ms="${period_ms:-50}" +segment_seconds=$(awk -v ticks="$ticks" -v period="$period_ms" 'BEGIN { printf "%.6f", ticks * period * 0.00055 }') +cache_dir="" +test_user="uce_epoch_$$" +test_password=$(printf '%s' "$test_name-$(date +%s%N)" | sha256sum | cut -c1-32) +test_user_created=false + +cleanup() { + if [[ "$test_user_created" == true ]]; then + mariadb -e "DROP USER IF EXISTS '$test_user'@'127.0.0.1'" >/dev/null 2>&1 || true + fi + 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")" +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 + +printf '%s\n' \ + 'void mysql_epoch_burn(f64 seconds) {' \ + ' f64 deadline = time_precise() + seconds;' \ + ' u64 spins = 0; while(time_precise() < deadline) spins++;' \ + '}' \ + 'CLI(Request& context) {' \ + " MySQL* db = mysql_connect(\"127.0.0.1\", \"$test_user\", \"$test_password\");" \ + ' if(db == 0 || mysql_error(db) != "") { print("mysql-connect-failed:", db == 0 ? "null" : mysql_error(db)); return; }' \ + " mysql_epoch_burn($segment_seconds);" \ + ' mysql_query(db, "SELECT 1 AS value");' \ + " mysql_epoch_burn($segment_seconds);" \ + ' mysql_query(db, "SELECT 2 AS value");' \ + ' if(mysql_error(db) == "") print("mysql-epoch-refresh-ok"); else print("mysql-query-failed:", mysql_error(db));' \ + ' mysql_disconnect(db);' \ + '}' >"$source_dir/test.uce" + +output=$(scripts/uce-cli "/$test_name/test.uce" 2>&1) || { + echo "MySQL hostcall did not refresh the guest epoch deadline: $output" >&2 + exit 1 +} +if [[ "$output" != "mysql-epoch-refresh-ok" ]]; then + echo "MySQL epoch refresh failed: $output" >&2 + exit 1 +fi + +echo "MySQL epoch refresh passed with ${segment_seconds}s guest segments" diff --git a/src/wasm/worker.cpp b/src/wasm/worker.cpp index a7e40bb..205ce87 100644 --- a/src/wasm/worker.cpp +++ b/src/wasm/worker.cpp @@ -2416,7 +2416,7 @@ private: return(std::monostate()); })); if(mod == "env" && name == "uce_host_mysql") - return(add([self](Caller, Span args, Span results) -> Result { + return(add([self](Caller caller, Span args, Span results) -> Result { String encoded; self->hostcall_read(args[0].i32(), args[1].i32(), encoded); u32 cap = (u32)args[3].i32(); @@ -2499,6 +2499,7 @@ private: } if(buf != 0 && cap >= out.size()) self->hostcall_write(buf, out); + caller.context().set_epoch_deadline(self->worker.cfg.epoch_deadline_ticks); results[0] = Val((int32_t)out.size()); return(std::monostate()); }));