Conversation
Size Report
Startup median (7 runs, lower is better):
|
There was a problem hiding this comment.
2 issues found across 2 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/daemon-client/daemon-client-transport.ts">
<violation number="1" location="src/daemon-client/daemon-client-transport.ts:293">
P2: For a remaining budget below 5 ms, this threshold is negative, so an immediately failed health probe is reported as `daemon_transport_timeout` instead of `Remote daemon is unavailable`. Do not apply the timer slop when the probe budget is at most the slop window.</violation>
</file>
<file name="src/daemon-client/__tests__/daemon-client-transport.test.ts">
<violation number="1" location="src/daemon-client/__tests__/daemon-client-transport.test.ts:240">
P3: This regression test only distinguishes fixed code from unfixed code while the health-probe timer fires within ~2 ms of its nominal deadline. Without the fix the misclassification depends on `remainingMs = deadline - performance.now()`, which at timer-fire time equals `2 - δ` where δ is the timer's real fire latency; any δ ≥ 2 ms (a loaded CI, exactly where this bug was observed) makes the old code throw `daemon_transport_timeout` and the test passes against the buggy implementation. Test-assert `probing === true` after the rejects so the skew demonstrably engaged, and widen the simulated clock skew to ~4 ms (still safely under the 5 ms slop, so the fix path keeps triggering via `elapsed_seen ≥ probeTimeoutMs - 5`) to give the discrimination real margin.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| if (probeTimeoutMs === undefined || probeTimeoutMs > REMOTE_DAEMON_HEALTHCHECK_TIMEOUT_MS) { | ||
| return false; | ||
| } | ||
| return performance.now() - probeStartedAt >= probeTimeoutMs - PROBE_TIMER_SLOP_MS; |
There was a problem hiding this comment.
P2: For a remaining budget below 5 ms, this threshold is negative, so an immediately failed health probe is reported as daemon_transport_timeout instead of Remote daemon is unavailable. Do not apply the timer slop when the probe budget is at most the slop window.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/daemon-client/daemon-client-transport.ts, line 293:
<comment>For a remaining budget below 5 ms, this threshold is negative, so an immediately failed health probe is reported as `daemon_transport_timeout` instead of `Remote daemon is unavailable`. Do not apply the timer slop when the probe budget is at most the slop window.</comment>
<file context>
@@ -272,6 +280,19 @@ async function retryAfterRemoteInstanceMismatch(
+ if (probeTimeoutMs === undefined || probeTimeoutMs > REMOTE_DAEMON_HEALTHCHECK_TIMEOUT_MS) {
+ return false;
+ }
+ return performance.now() - probeStartedAt >= probeTimeoutMs - PROBE_TIMER_SLOP_MS;
+}
+
</file context>
| return performance.now() - probeStartedAt >= probeTimeoutMs - PROBE_TIMER_SLOP_MS; | |
| return ( | |
| probeTimeoutMs > PROBE_TIMER_SLOP_MS && | |
| performance.now() - probeStartedAt >= probeTimeoutMs - PROBE_TIMER_SLOP_MS | |
| ); |
| }); | ||
| const clock = vi | ||
| .spyOn(performance, 'now') | ||
| .mockImplementation(() => realNow() - (probing ? 2 : 0)); |
There was a problem hiding this comment.
P3: This regression test only distinguishes fixed code from unfixed code while the health-probe timer fires within ~2 ms of its nominal deadline. Without the fix the misclassification depends on remainingMs = deadline - performance.now(), which at timer-fire time equals 2 - δ where δ is the timer's real fire latency; any δ ≥ 2 ms (a loaded CI, exactly where this bug was observed) makes the old code throw daemon_transport_timeout and the test passes against the buggy implementation. Test-assert probing === true after the rejects so the skew demonstrably engaged, and widen the simulated clock skew to ~4 ms (still safely under the 5 ms slop, so the fix path keeps triggering via elapsed_seen ≥ probeTimeoutMs - 5) to give the discrimination real margin.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/daemon-client/__tests__/daemon-client-transport.test.ts, line 240:
<comment>This regression test only distinguishes fixed code from unfixed code while the health-probe timer fires within ~2 ms of its nominal deadline. Without the fix the misclassification depends on `remainingMs = deadline - performance.now()`, which at timer-fire time equals `2 - δ` where δ is the timer's real fire latency; any δ ≥ 2 ms (a loaded CI, exactly where this bug was observed) makes the old code throw `daemon_transport_timeout` and the test passes against the buggy implementation. Test-assert `probing === true` after the rejects so the skew demonstrably engaged, and widen the simulated clock skew to ~4 ms (still safely under the 5 ms slop, so the fix path keeps triggering via `elapsed_seen ≥ probeTimeoutMs - 5`) to give the discrimination real margin.</comment>
<file context>
@@ -222,6 +222,35 @@ test('a delayed restart health probe stops at the RPC deadline without retrying'
+ });
+ const clock = vi
+ .spyOn(performance, 'now')
+ .mockImplementation(() => realNow() - (probing ? 2 : 0));
+ try {
+ const port = await listenOnLoopback(server);
</file context>
|
Thanks for the fix. At 7566714 the restart probe still guesses "timeout" from elapsed time, so the flake is narrowed but not removed, and a real outage can now be reported as a timeout. There are no conflicts. The check at https://github.com/callstack/agent-device/blob/7566714/src/daemon-client/daemon-client-transport.ts#L293 never asks which event ended the probe. The new test at https://github.com/callstack/agent-device/blob/7566714/src/daemon-client/__tests__/daemon-client-transport.test.ts#L240 does not reliably separate old code from new code. The 2 ms skew only helps when the timer fires less than 2 ms late; at 2 ms or more, the old code already throws Not blocking: The failing Smoke Tests job looks unrelated to this diff. It is the iOS simulator run against a local daemon with no base URL, so it never reaches the changed branch, and it failed with |
…h probe runs to it
7566714 to
3c5d77b
Compare
|
Thanks for the review. Pushed 3c5d77b.
|
There was a problem hiding this comment.
1 issue found across 3 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/daemon-client/daemon-client-transport.ts">
<violation number="1" location="src/daemon-client/daemon-client-transport.ts:141">
P2: This refactor moves the wire-relevant health request tokens out of the ledger-covered `readDaemonHttpHealth` declaration. Previously `buildDaemonHttpUrl`, `transport.request({method: 'GET', ...})`, the `statusCode < 500` threshold, and body-read/abort handling all lived inside `readDaemonHttpHealth`'s digested body; now they live in `probeDaemonHttpHealth`/`collectHealthResponse`, neither of which is listed in `test/wire-compat/surface.ts`'s /health consumer group. The re-pinned digest for `readDaemonHttpHealth` now covers only a delegation, so a future wire-breaking change to the health request shape inside these helpers will no longer move the ledger digest — coverage the /health boundary previously had is silently lost. Add the new helpers to the surface (with a `wire-mutations` proof if they introduce a new break class) or note the moved tokens as reviewer-owned `uncovered`, per the wire-compat README's 'never claim coverage you don't provide'.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| return (await probeDaemonHttpHealth(info, probeTimeoutMs)).health; | ||
| } | ||
|
|
||
| async function probeDaemonHttpHealth( |
There was a problem hiding this comment.
P2: This refactor moves the wire-relevant health request tokens out of the ledger-covered readDaemonHttpHealth declaration. Previously buildDaemonHttpUrl, transport.request({method: 'GET', ...}), the statusCode < 500 threshold, and body-read/abort handling all lived inside readDaemonHttpHealth's digested body; now they live in probeDaemonHttpHealth/collectHealthResponse, neither of which is listed in test/wire-compat/surface.ts's /health consumer group. The re-pinned digest for readDaemonHttpHealth now covers only a delegation, so a future wire-breaking change to the health request shape inside these helpers will no longer move the ledger digest — coverage the /health boundary previously had is silently lost. Add the new helpers to the surface (with a wire-mutations proof if they introduce a new break class) or note the moved tokens as reviewer-owned uncovered, per the wire-compat README's 'never claim coverage you don't provide'.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/daemon-client/daemon-client-transport.ts, line 141:
<comment>This refactor moves the wire-relevant health request tokens out of the ledger-covered `readDaemonHttpHealth` declaration. Previously `buildDaemonHttpUrl`, `transport.request({method: 'GET', ...})`, the `statusCode < 500` threshold, and body-read/abort handling all lived inside `readDaemonHttpHealth`'s digested body; now they live in `probeDaemonHttpHealth`/`collectHealthResponse`, neither of which is listed in `test/wire-compat/surface.ts`'s /health consumer group. The re-pinned digest for `readDaemonHttpHealth` now covers only a delegation, so a future wire-breaking change to the health request shape inside these helpers will no longer move the ledger digest — coverage the /health boundary previously had is silently lost. Add the new helpers to the surface (with a `wire-mutations` proof if they introduce a new break class) or note the moved tokens as reviewer-owned `uncovered`, per the wire-compat README's 'never claim coverage you don't provide'.</comment>
<file context>
@@ -125,21 +135,30 @@ async function readDaemonHttpHealth(
+ return (await probeDaemonHttpHealth(info, probeTimeoutMs)).health;
+}
+
+async function probeDaemonHttpHealth(
+ info: DaemonInfo,
+ probeTimeoutMs?: number,
</file context>
|
Thanks, this resolves the earlier finding at 3c5d77b. |
|
Small correction to my previous comment on 3c5d77b, with the full notes. The verdict does not change. This PR is ready at 3c5d77b. The fixes from the earlier review at 7566714 are in, and I found no remaining problems in the code. Not blocking, and you can take or leave these: isCallerDeadlineProbeBudget re-derives which bound won min(REMOTE_DAEMON_HEALTHCHECK_TIMEOUT_MS, probeTimeoutMs), a choice probeDaemonHttpHealth already makes at line 154, and the result leaves readRemoteDaemonHealth through the onProbeTimedOut callback. The probe could report which budget ended it, for example timedOut: 'caller' | 'cap' | false, from an internal variant that returns it. Also, both amended rationales in ledger.json now say the health request and payload are unchanged twice, so keeping only the appended clause about the internal timer outcome would read better. I did not run the tests or the wire-compat gate. The digests and the loopback test behavior come from reading the code only. I also did not check that Node emits 'error' or 'aborted' with signal.aborted set on every platform when a body read stalls. The req 'timeout' path covers the idle case on its own. Smoke Tests is still in progress with no failure so far. The change reaches the remote-daemon instance-mismatch retry, which needs a baseUrl. canConnectHttp only goes through the refactored probe and its result does not change. Smoke runs a local daemon, so it does not reach the retry path. There are no conflicts. Smoke Tests must finish green before merge. |
|
Closing: #3056 landed the same fix on main (typed timeout on the restart health probe, with the ledger re-pin), so this PR is superseded. |
Summary
Root cause: after an instance mismatch,
retryAfterRemoteInstanceMismatchprobes/healthwith the remaining RPC budget. The probe's timer can fire a few ms beforeperformance.now()reaches the deadline. The remaining-time check then saw a sliver of budget, so the failed probe was classified asRemote daemon is unavailableinstead ofdaemon_transport_timeout. A loaded CI runner hits this and a fast local machine does not.Fix: the health probe now reports which event ended it. Its own budget timer sets a typed
timedOutoutcome (internal to daemon-client). The retry throws the request timeout only when the probe timed out and its budget was the caller's remaining time. Refusals, resets, aborts and 5xx still reportRemote daemon is unavailable. The public return shapes ofreadRemoteDaemonHealthandRemoteDaemonHealthare unchanged; two wire-ledger digests were re-pinned (health request and payload are unchanged).Found on unrelated PR CI (#3014, #3025, #3029, #3033, #3036).
Validation
daemon-client-transport.test.tspassed 20/20 in a loop.pnpm check:affected --run: all runnable checks passed (including daemon-wire-compat).performance.nowby 50 ms and asserts/healthwas reached; it fails on the previous code. A new test has/healthreturn 503 near the deadline and expectsRemote daemon is unavailable; it fails if any failed probe near the deadline is treated as a timeout.