Skip to content

Commit a1af976

Browse files
committed
fix(gateway): gate busy run-age ceiling on disconnected transport
1 parent d5f25fa commit a1af976

3 files changed

Lines changed: 71 additions & 7 deletions

File tree

src/gateway/channel-health-policy.test.ts

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,17 +86,42 @@ describe("evaluateChannelHealth", () => {
8686
expect(evaluation).toEqual({ healthy: false, reason: "stuck" });
8787
});
8888

89-
it("flags a hung run as stuck once its start ages past the threshold despite a fresh heartbeat", () => {
89+
it("flags a hung run masking a disconnected transport as stuck despite a fresh heartbeat", () => {
9090
const now = 30 * 60_000;
9191
const evaluation = evaluateHealth(
9292
activeRunAccount(now - 1_000, {
93+
connected: false,
9394
activeRunStartedAt: now - 26 * 60_000,
9495
}),
9596
{ now },
9697
);
9798
expect(evaluation).toEqual({ healthy: false, reason: "stuck" });
9899
});
99100

101+
it("keeps a connected run healthy past the threshold while its heartbeat stays fresh", () => {
102+
const now = 30 * 60_000;
103+
const evaluation = evaluateHealth(
104+
activeRunAccount(now - 1_000, {
105+
connected: true,
106+
activeRunStartedAt: now - 26 * 60_000,
107+
}),
108+
{ now },
109+
);
110+
expect(evaluation).toEqual({ healthy: true, reason: "busy" });
111+
});
112+
113+
it("keeps a run without transport reporting healthy past the threshold while its heartbeat stays fresh", () => {
114+
const now = 30 * 60_000;
115+
const evaluation = evaluateHealth(
116+
activeRunAccount(now - 1_000, {
117+
connected: undefined,
118+
activeRunStartedAt: now - 26 * 60_000,
119+
}),
120+
{ now },
121+
);
122+
expect(evaluation).toEqual({ healthy: true, reason: "busy" });
123+
});
124+
100125
it("keeps a short-lived run healthy when both start and activity are recent", () => {
101126
const now = 30 * 60_000;
102127
const evaluation = evaluateHealth(

src/gateway/channel-health-policy.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -101,14 +101,15 @@ export function evaluateChannelHealth(
101101
if (!busyStateInitializedForLifecycle) {
102102
// Fall through to normal startup/disconnect checks below.
103103
} else {
104-
const runStartAge =
105-
activeRunStartedAt == null ? null : Math.max(0, policy.now - activeRunStartedAt);
106104
const runActivityAge =
107-
lastRunActivityAt == null ? null : Math.max(0, policy.now - lastRunActivityAt);
108-
const busyAge =
109-
runStartAge == null && runActivityAge == null
105+
lastRunActivityAt == null
110106
? Number.POSITIVE_INFINITY
111-
: Math.max(runStartAge ?? 0, runActivityAge ?? 0);
107+
: Math.max(0, policy.now - lastRunActivityAt);
108+
const disconnectedRunStartAge =
109+
snapshot.connected === false && activeRunStartedAt != null
110+
? Math.max(0, policy.now - activeRunStartedAt)
111+
: 0;
112+
const busyAge = Math.max(runActivityAge, disconnectedRunStartAge);
112113
if (busyAge < BUSY_ACTIVITY_STALE_THRESHOLD_MS) {
113114
return { healthy: true, reason: "busy" };
114115
}

src/plugin-sdk/channel-lifecycle.queue.test.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,44 @@ describe("createChannelRunQueue", () => {
123123
}
124124
});
125125

126+
it("advances to the next-oldest run start when the oldest run ends", async () => {
127+
vi.useFakeTimers();
128+
try {
129+
const first = createDeferred();
130+
const second = createDeferred();
131+
const setStatus = vi.fn();
132+
const queue = createChannelRunQueue({ setStatus });
133+
134+
vi.setSystemTime(1_000);
135+
queue.enqueue("first", async () => {
136+
await first.promise;
137+
});
138+
await flushAsyncWork();
139+
140+
vi.setSystemTime(2_000);
141+
queue.enqueue("second", async () => {
142+
await second.promise;
143+
});
144+
await flushAsyncWork();
145+
146+
first.resolve?.();
147+
await first.promise;
148+
await flushAsyncWork();
149+
150+
expect(setStatus.mock.calls.at(-1)?.[0]).toMatchObject({
151+
activeRuns: 1,
152+
busy: true,
153+
activeRunStartedAt: 2_000,
154+
});
155+
156+
queue.deactivate();
157+
second.resolve?.();
158+
await second.promise;
159+
} finally {
160+
vi.useRealTimers();
161+
}
162+
});
163+
126164
it("contains reporting hook errors", async () => {
127165
const taskError = new Error("boom");
128166
const onError = vi.fn(() => {

0 commit comments

Comments
 (0)