From 43a8265670edd75a7482b45a1e9d94efbefa546b Mon Sep 17 00:00:00 2001 From: bigboateng Date: Sun, 26 Jul 2026 03:23:55 +0100 Subject: [PATCH] fix(boatstack): guard retries the helper on Linux ETXTBSY under concurrent first use MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Linux refuses to exec a file another process still holds open for writing (ETXTBSY, exit 126). Under concurrent first use a waiting guard could reach the exec step while a peer was still finishing hydration and deny the tool call by mistake — a flaky Linux-only CI failure (TestGuardAutoHydrationSerializesConcurrentFirstUse); macOS and Windows do not enforce the rule. The writer already replaces the binary atomically (temp → close → rename), so the race is purely read-side. Replace the guard's final 'exec $HELPER' with a bounded retry that re-runs only on exit 126 and then propagates the helper's real status. Running the helper as a child (not exec) makes a failed start observable; stdio and the exit code pass through and an ETXTBSY start never consumes stdin. Windows/macOS guards are unchanged (no ETXTBSY there). Disclosure-Reviewed: reviewed — public-safe only, private facet kept out of this commit --- .../2026-07-26-guard-etxtbsy-retry.md | 15 +++++++++++++++ .../product-engineering-loop/hooks.go | 19 ++++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-26-guard-etxtbsy-retry.md diff --git a/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-26-guard-etxtbsy-retry.md b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-26-guard-etxtbsy-retry.md new file mode 100644 index 000000000..239c8eb93 --- /dev/null +++ b/labs/12-product-engineering-loop/boatstack-distribution/release-notes/2026-07-26-guard-etxtbsy-retry.md @@ -0,0 +1,15 @@ +### Concurrent first use no longer fails with "Text file busy" on Linux + +When several tool calls hit an empty shared-runtime slot at the same time, one guard +hydrates the slot and the others wait. On Linux the kernel refuses to run a file while +another process still holds it open for writing. It reports this as "Text file busy". A +waiting guard could reach the run step in that brief window, fail to start the helper, and +deny the tool call by mistake. This showed up as a flaky Linux CI failure under contention. +macOS and Windows do not enforce this rule, so only Linux saw the denial. + +The guard now retries the helper a bounded number of times when the start fails with this +exact condition, then hands off as before. The retry is short and self-clearing: the peer +closes the file the moment its write finishes, so the next attempt starts the helper. A +helper that genuinely cannot run still returns the same status after the retries, so no real +failure is hidden. The runtime binary is still written atomically, so the fix only closes +the read-side race. diff --git a/labs/12-product-engineering-loop/product-engineering-loop/hooks.go b/labs/12-product-engineering-loop/product-engineering-loop/hooks.go index 68d107834..4725d5c4e 100644 --- a/labs/12-product-engineering-loop/product-engineering-loop/hooks.go +++ b/labs/12-product-engineering-loop/product-engineering-loop/hooks.go @@ -223,7 +223,24 @@ if [[ -z "$EXPECTED" || "$ACTUAL" != "$EXPECTED" ]]; then exit 2 fi -exec "$HELPER" bootstrap-safety-hook --host "$HOST" --repo "$ROOT" +# Linux refuses to exec a file another process still holds open for writing +# (ETXTBSY, surfaced as exit 126). Under concurrent first use a peer guard can be +# finishing hydration at this instant, even though the writer replaces the binary +# atomically. Retry briefly, then hand off. A genuinely non-executable helper keeps +# returning 126 and the final status still propagates unchanged. Running the helper +# as a child (not exec) is required so a failed start is observable; stdio and the +# exit code pass through, and an ETXTBSY start never consumes stdin. +ATTEMPT=0 +while :; do + "$HELPER" bootstrap-safety-hook --host "$HOST" --repo "$ROOT" + HELPER_STATUS=$? + if [[ $HELPER_STATUS -eq 126 && $ATTEMPT -lt 30 ]]; then + ATTEMPT=$((ATTEMPT + 1)) + sleep 0.1 + continue + fi + exit $HELPER_STATUS +done `, Version, SourceCommit, Version, SourceCommit, Version, Version, runtimeHydrateCommandBash(Version), runtimeHydrateCommandBash(Version))) }