mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 23:23:21 +00:00
fix(client): keep heartbeat cadence during active downloads
This commit is contained in:
@@ -2654,7 +2654,7 @@ test("foreground verification preserves a healthy socket and deduplicates simult
|
||||
expect(daemon.closesFromClient()).toEqual([]);
|
||||
});
|
||||
|
||||
test("chunks extend the heartbeat deadline until silence causes two misses", async () => {
|
||||
test("chunks prevent heartbeat misses until silence causes two misses", async () => {
|
||||
useHeartbeatClock();
|
||||
const daemon = new FakeDaemon();
|
||||
daemon.daemonGoesSilent();
|
||||
@@ -2685,9 +2685,9 @@ test("chunks extend the heartbeat deadline until silence causes two misses", asy
|
||||
daemon.receiveChunk(index === 0);
|
||||
expect(daemon.closesFromClient()).toEqual([]);
|
||||
}
|
||||
// No complete message or pong has arrived, but chunks keep the ping alive.
|
||||
expect(daemon.pingTimestamps()).toEqual(["0s"]);
|
||||
await vi.advanceTimersByTimeAsync(15_000);
|
||||
// No complete message or pong has arrived, but progress prevents misses.
|
||||
expect(daemon.pingTimestamps()).toEqual(["0s", "25s"]);
|
||||
await vi.advanceTimersByTimeAsync(25_000);
|
||||
expect(daemon.closesFromClient()).toEqual([]);
|
||||
await vi.advanceTimersByTimeAsync(25_000);
|
||||
expect(daemon.closesFromClient()).toEqual([{ code: 1001, reason: "Liveness check timed out" }]);
|
||||
@@ -2716,8 +2716,41 @@ test("foreground preserves a multi-MB transfer with its pong queued behind chunk
|
||||
daemon.receiveChunk(index === 0);
|
||||
expect(daemon.closesFromClient()).toEqual([]);
|
||||
}
|
||||
// No complete message or pong has arrived, but chunks keep the ping alive.
|
||||
expect(daemon.pingTimestamps()).toEqual(["0s"]);
|
||||
// No complete message or pong has arrived, but progress prevents misses.
|
||||
expect(daemon.pingTimestamps()).toEqual(["0s", "25s"]);
|
||||
daemon.finishTransfer();
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
expect(client.getConnectionState()).toEqual({ status: "connected" });
|
||||
expect(daemon.closesFromClient()).toEqual([]);
|
||||
});
|
||||
|
||||
test("chunk progress beyond the daemon lease keeps sending heartbeat pings", async () => {
|
||||
useHeartbeatClock();
|
||||
const daemon = new FakeDaemon();
|
||||
daemon.daemonGoesSilent();
|
||||
const client = new DaemonClient({
|
||||
url: "ws://test",
|
||||
clientId: "long-download",
|
||||
logger: noopLogger,
|
||||
reconnect: { enabled: false },
|
||||
transportFactory: () => daemon.transport,
|
||||
});
|
||||
clients.push(client);
|
||||
const connection = client.connect();
|
||||
daemon.openConnection();
|
||||
await connection;
|
||||
client.ensureConnected({ verify: true });
|
||||
for (let index = 0; index < 12; index++) {
|
||||
await vi.advanceTimersByTimeAsync(5_000);
|
||||
daemon.receiveChunk(index === 0);
|
||||
// Each completed window starts the next ping ten seconds later,
|
||||
// even though the response and all queued pongs are still in flight.
|
||||
const expected = ["0s"];
|
||||
if (index >= 4) expected.push("25s");
|
||||
if (index >= 9) expected.push("50s");
|
||||
expect(daemon.pingTimestamps()).toEqual(expected);
|
||||
expect(daemon.closesFromClient()).toEqual([]);
|
||||
}
|
||||
daemon.finishTransfer();
|
||||
await vi.advanceTimersByTimeAsync(30_000);
|
||||
expect(client.getConnectionState()).toEqual({ status: "connected" });
|
||||
|
||||
@@ -1295,7 +1295,7 @@ interface PingProbe {
|
||||
reject: (error: Error) => void;
|
||||
timeoutHandle: ReturnType<typeof setTimeout>;
|
||||
startedAt: number;
|
||||
extendDeadline: () => void;
|
||||
receivedActivity: boolean;
|
||||
// Whether a timeout on this ping should be recorded as a liveness failure. Only the
|
||||
// heartbeat sets this; a latency measurement never drives teardown, even when a
|
||||
// heartbeat tick shares (dedupes onto) an in-flight measurement ping.
|
||||
@@ -1800,7 +1800,7 @@ export class DaemonClient {
|
||||
|
||||
private recordTransportActivity(): void {
|
||||
this.consecutiveLivenessFailures = 0;
|
||||
if (this.pingProbe?.drivesLivenessFailure) this.pingProbe.extendDeadline();
|
||||
if (this.pingProbe) this.pingProbe.receivedActivity = true;
|
||||
}
|
||||
|
||||
requireConnectedHosts(): void {
|
||||
@@ -2588,7 +2588,9 @@ export class DaemonClient {
|
||||
this.pingProbe = null;
|
||||
const error = new PingTimeoutError(timeoutMs);
|
||||
probe.reject(error);
|
||||
if (probe.drivesLivenessFailure) {
|
||||
// Keep the deadline fixed so subsequent pings renew the daemon socket lease
|
||||
// during a long download. Progress proves life even when its pong is queued.
|
||||
if (probe.drivesLivenessFailure && !probe.receivedActivity) {
|
||||
this.recordLivenessFailure(toTimeoutError(error, "Liveness check", timeoutMs));
|
||||
}
|
||||
};
|
||||
@@ -2597,10 +2599,7 @@ export class DaemonClient {
|
||||
resolve: (value) => resolveProbe?.(value),
|
||||
reject: (error) => rejectProbe?.(error),
|
||||
timeoutHandle: setTimeout(expire, timeoutMs),
|
||||
extendDeadline: () => {
|
||||
clearTimeout(probe.timeoutHandle);
|
||||
probe.timeoutHandle = setTimeout(expire, timeoutMs);
|
||||
},
|
||||
receivedActivity: false,
|
||||
startedAt,
|
||||
drivesLivenessFailure: params.drivesLivenessFailure,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user