fix(client): preserve latency deadlines when sharing heartbeat pings

This commit is contained in:
Mohamed Boudra
2026-11-03 09:21:47 +01:00
parent 09ada1e14f
commit 89018fe790
2 changed files with 35 additions and 5 deletions
+23 -1
View File
@@ -2670,8 +2670,18 @@ test("chunks extend the heartbeat deadline until silence causes two misses", asy
daemon.openConnection();
await connection;
client.ensureConnected({ verify: true });
let measurementError: unknown;
void client.measureLatency({ timeoutMs: 5_000 }).catch((error: unknown) => {
measurementError = error;
});
await vi.advanceTimersByTimeAsync(5_000);
expect(measurementError).toEqual(
expect.objectContaining({
message: "Latency measurement timed out (5000ms)",
}),
);
for (let index = 0; index < 4; index++) {
await vi.advanceTimersByTimeAsync(10_000);
await vi.advanceTimersByTimeAsync(index === 0 ? 5_000 : 10_000);
daemon.receiveChunk(index === 0);
expect(daemon.closesFromClient()).toEqual([]);
}
@@ -2831,6 +2841,18 @@ test("sends only one ping while a slow pong is still outstanding", async () => {
expect(session.pingTimestamps()).toEqual(["10s"]);
});
test("a heartbeat sharing a latency measurement still saves the RTT", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
session.daemonAnswersPingsAfter("2s");
await session.connect();
await session.advance(9_000);
const measurement = session.measureLatency({ timeoutMs: 5_000 });
await session.advance(2_000);
await expect(measurement).resolves.toBe(2000);
expect(session.lastLivenessRttMs()).toBe(2000);
});
test("reports the round-trip time of the last successful heartbeat", async () => {
useHeartbeatClock();
const session = new DaemonClientSession();
+12 -4
View File
@@ -2536,9 +2536,18 @@ export class DaemonClient {
measureLatency(params?: { timeoutMs?: number }): Promise<number> {
const timeoutMs = Math.max(1, params?.timeoutMs ?? DEFAULT_LIVENESS_TIMEOUT_MS);
return this.sendPingAwaitRtt({ timeoutMs, drivesLivenessFailure: false }).catch((error) => {
throw toTimeoutError(error, "Latency measurement", timeoutMs);
let deadline: ReturnType<typeof setTimeout>;
const timeout = new Promise<never>((_resolve, reject) => {
deadline = setTimeout(() => reject(new PingTimeoutError(timeoutMs)), timeoutMs);
});
return Promise.race([
this.sendPingAwaitRtt({ timeoutMs, drivesLivenessFailure: false }),
timeout,
])
.catch((error) => {
throw toTimeoutError(error, "Latency measurement", timeoutMs);
})
.finally(() => clearTimeout(deadline));
}
private async livenessPing(params?: { timeoutMs?: number }): Promise<number> {
@@ -2639,8 +2648,7 @@ export class DaemonClient {
private tickLivenessHeartbeat(): void {
this.stopLivenessHeartbeat();
const transport = this.transport;
const tick =
this.pingProbe?.promise ?? this.livenessPing({ timeoutMs: LIVENESS_HEARTBEAT_TIMEOUT_MS });
const tick = this.livenessPing({ timeoutMs: LIVENESS_HEARTBEAT_TIMEOUT_MS });
void tick
.catch(() => {})
.finally(() => {