Skip to content

Commit b77aec5

Browse files
committed
fix: keep recovery restart cancellable
1 parent d55639a commit b77aec5

2 files changed

Lines changed: 56 additions & 2 deletions

File tree

src/gateway/server-channels.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,47 @@ describe("server-channels auto restart", () => {
367367
expect(startAccount).toHaveBeenCalledTimes(2);
368368
});
369369

370+
it("lets manual stops cancel recovery backoff after recovery stop times out", async () => {
371+
const releaseFirstTask = createDeferred();
372+
const startAccount = vi.fn(
373+
async ({ abortSignal }: { abortSignal: AbortSignal }) =>
374+
await new Promise<void>((resolve) => {
375+
abortSignal.addEventListener("abort", () => {}, { once: true });
376+
void releaseFirstTask.promise.then(resolve);
377+
}),
378+
);
379+
installTestRegistry(
380+
createTestPlugin({
381+
startAccount,
382+
}),
383+
);
384+
const manager = createManager();
385+
386+
await manager.startChannels();
387+
const recoveryStopTask = manager.stopChannel("discord", DEFAULT_ACCOUNT_ID, {
388+
manual: false,
389+
});
390+
await vi.advanceTimersByTimeAsync(5_000);
391+
await recoveryStopTask;
392+
393+
releaseFirstTask.resolve();
394+
await waitForMicrotaskCondition(
395+
() => hoisted.sleepWithAbort.mock.calls.length > 0,
396+
"expected recovery restart backoff to be scheduled",
397+
);
398+
expect(hoisted.sleepWithAbort).toHaveBeenCalledWith(10, expect.any(AbortSignal));
399+
400+
await manager.stopChannel("discord", DEFAULT_ACCOUNT_ID);
401+
await vi.advanceTimersByTimeAsync(10);
402+
await flushMicrotasks();
403+
404+
const account = manager.getRuntimeSnapshot().channelAccounts.discord?.[DEFAULT_ACCOUNT_ID];
405+
expect(startAccount).toHaveBeenCalledTimes(1);
406+
expect(account?.running).toBe(false);
407+
expect(account?.restartPending).toBe(false);
408+
expect(manager.isManuallyStopped("discord", DEFAULT_ACCOUNT_ID)).toBe(true);
409+
});
410+
370411
it("marks enabled/configured when account descriptors omit them", () => {
371412
installTestRegistry(
372413
createTestPlugin({

src/gateway/server-channels.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,14 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
573573
restartPending: true,
574574
reconnectAttempts: attempt,
575575
});
576+
const recoveryRestartSleepAbort = recoveryStopTimedOut.has(rKey)
577+
? new AbortController()
578+
: undefined;
579+
if (recoveryRestartSleepAbort) {
580+
store.aborts.set(id, recoveryRestartSleepAbort);
581+
}
576582
try {
577-
const restartSleepAbort = recoveryStopTimedOut.has(rKey) ? undefined : abort.signal;
583+
const restartSleepAbort = recoveryRestartSleepAbort?.signal ?? abort.signal;
578584
await sleepWithAbort(delayMs, restartSleepAbort);
579585
if (manuallyStopped.has(rKey)) {
580586
recoveryStopTimedOut.delete(rKey);
@@ -584,7 +590,7 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
584590
if (store.tasks.get(id) === trackedPromise) {
585591
store.tasks.delete(id);
586592
}
587-
if (store.aborts.get(id) === abort) {
593+
if (store.aborts.get(id) === (recoveryRestartSleepAbort ?? abort)) {
588594
store.aborts.delete(id);
589595
}
590596
await startChannelInternal(channelId, id, {
@@ -593,6 +599,13 @@ export function createChannelManager(opts: ChannelManagerOptions): ChannelManage
593599
});
594600
} catch {
595601
// abort or startup failure — next crash will retry
602+
} finally {
603+
if (recoveryRestartSleepAbort) {
604+
recoveryStopTimedOut.delete(rKey);
605+
if (store.aborts.get(id) === recoveryRestartSleepAbort) {
606+
store.aborts.delete(id);
607+
}
608+
}
596609
}
597610
})
598611
.finally(() => {

0 commit comments

Comments
 (0)