fix(SDK-7061): resilient TRA build-stop — retry + success-gated flag + synchronous shutdown stop - #99
fix(SDK-7061): resilient TRA build-stop — retry + success-gated flag + synchronous shutdown stop#99pri-gadhiya wants to merge 7 commits into
Conversation
…lag + detached watchdog) The build-stop PUT was a single best-effort request with no retry and no HTTP-status check; a transient failure, or a launcher terminated (CI cancel / kill -9 / OOM) before it was sent, left the O11Y build hanging 'running' until the ~60-min inactivity timeout. - stopBuildUpstream: 3x retry + backoff + response.ok check - buildStopped gated on stop success (recoverable by exit cleanup) - detached shutdown-watchdog survives launcher kill -9 and sends the stop; JWT from inherited env (not disk), marker 0o600 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…atchdog Replace the detached-watchdog approach with the sibling-idiomatic in-process shutdown hook, made SYNCHRONOUS/blocking (spawnSync) so the stop PUT lands before WDIO force-exits (the async variant did not). Matches browserstack-javaagent (addShutdownHook) + browserstack-node-agent (process.on signal suite); no separate process, no JWT on disk. Net vs main: util.ts (stop retry + response.ok), launcher.ts (buildStopped gated on success), exitHandler.ts (sync shutdown stop on SIGTERM/SIGINT/SIGHUP/uncaughtException/exit). Removes buildWatchdog.ts + buildWatchdogMarker.ts. True kill -9/OOM remains a server-side concern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
RUN_TESTS |
🔴 SDK PR Review — SDK-7061 (automated
|
…op (review) stopBuildSyncBlocking is armed by setupExitHandlers() before BrowserStackConfig is initialized in the launcher constructor; an early uncaughtException could hit a bare getInstance() returning undefined and throw a TypeError from inside the uncaughtException listener (fatal, masks the original error). Add an if(!config) return guard + optional chaining so the handler never throws. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ SDK PR Review (re-review) — SDK-7061 ·
|
…topBuildSyncBlocking for tests The stopBuildUpstream retry (3x) broke tests/util.test.ts 'return error if failed' (stubbed only one rejection → attempt 2 hit the default 200 mock → returned success). Fix the existing tests to the new behavior and add coverage. - util.test.ts: single-call success (response.ok path); reject all 3 attempts → error; non-2xx (500/502/503) retried → error; transient 500-then-200 → success. Fake timers for the 500ms*attempt backoff (real-timers-first then re-fake with setTimeout), mockClear (not mockReset) to preserve the shared global fetch mock. - launcher.test.ts: onComplete direct-HTTP stop success → buildStopped=true; stop error → buildStopped=false. - exitHandler.test.ts (new): stopBuildSyncBlocking — config-undefined early-return (no throw/spawn), spawnSync invoked once + marks buildStopped on status 0, no mark on non-zero, skips when CLI running / UUID+JWT absent / already stopped. Hermetic (spawnSync mocked). - src/exitHandler.ts: add `export` to stopBuildSyncBlocking (testability only; zero runtime behavior change). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What is this about?
Fixes SDK-7061: WebdriverIO runs where the Test Observability / TRA build appears "stuck" and only closes via the ~60-min server-side inactivity timeout, even though the run finished.
Root cause: the build-stop (
PUT /api/v1/builds/<uuid>/stop) was sent once, best-effort — no retry, and it did not check the HTTP status. If that request failed/timed out, or the WDIO launcher was interrupted (CI cancel/timeout) before it was sent, the build was never marked complete.This PR makes the build-stop resilient (verified on WebdriverIO-cucumber, before/after):
util.tsstopBuildUpstream— retries the stop 3× with backoff and treats a non-2xx response as a failure (previously a non-2xx was logged as success).launcher.tsonComplete—buildStoppedis set only when the stop actually succeeds, so a failed stop stays recoverable.exitHandler.ts— synchronous/blocking shutdown stop. OnSIGTERM/SIGINT/SIGHUP/uncaughtException/exit, a blockingspawnSyncchild issues the stop PUT and the process waits for it before exiting — the Node analog of Java's blockingRuntime.getRuntime().addShutdownHook. This closes the build on a graceful CI cancel/timeout. (An earlier async handler was verified not to work here: WDIO's launcher callsprocess.exitbefore an async PUT can land; the synchronous approach fixes that. The JWT is read from the inherited environment, never written to disk.)Consistency: this mirrors how
browserstack-javaagent(JVM shutdown hook) andbrowserstack-node-agent(process.onsignal suite) handle build-stop on termination — an in-process shutdown hook, no separate watchdog process.Known boundary (out of scope here, same as java/node): a true uncatchable
kill -9/OOM of the launcher runs no handler, so that build still relies on a server-side inactivity/heartbeat auto-close — tracked separately (the sharedbrowserstack-binarystopBuildpath has the same retry gap and is the highest-leverage place to harden across all binary-flow SDKs).Related Jira task/s
Release (mandatory for every PR — required for the
ready-for-reviewlabel)Version bump: (required — tick exactly one)
Release notes type: (optional)
Release notes (customer-facing): (optional but encouraged)
Release notes (internal): (required — engineer-facing; what actually changed / why)
stopBuildUpstreamhardened: 3× retry + backoff +response.okcheck (was a single best-effort PUT; non-2xx was logged as success).buildStoppedgated on stop success (was set unconditionally), so the shutdown path can re-attempt a failed stop.exitHandler.ts(SIGTERM/SIGINT/SIGHUP/uncaughtException/exit viaspawnSync) — JavaaddShutdownHookanalog; closes the build on graceful interruption. JWT read from env, not persisted. Bounded: blocks exit ≤15s if the collector is unreachable (teardown-only).Checklist
PR Validations
Run Tests: Comment RUN_TESTS to trigger sanity tests.