fix-meeting-audio-pull-timeout-cleanup (#114526)

Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com>
This commit is contained in:
WhatsSkiLL
2026-07-27 18:43:08 +02:00
committed by GitHub
parent 9588a10f12
commit 1a5d71ee6b
2 changed files with 64 additions and 14 deletions

View File

@@ -1,5 +1,3 @@
import { setTimeout as sleep } from "node:timers/promises";
/** Internal pull-wait ownership used by the node-host long poll. */
export class MeetingNodeAudioPullWaiters {
readonly #waiters = new Set<() => void>();
@@ -14,12 +12,19 @@ export class MeetingNodeAudioPullWaiters {
wake = resolve;
this.#waiters.add(wake);
});
let timer: ReturnType<typeof setTimeout> | undefined;
const timeout = new Promise<void>((resolve) => {
timer = setTimeout(resolve, timeoutMs);
});
try {
await Promise.race([sleep(timeoutMs), ready]);
await Promise.race([timeout, ready]);
} finally {
// A stalled bridge can be polled indefinitely. Timeout must release its
// resolver instead of retaining one waiter per empty pull.
this.#waiters.delete(wake);
if (timer !== undefined) {
clearTimeout(timer);
}
}
}

View File

@@ -1,20 +1,65 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import { MeetingNodeAudioPullWaiters } from "./node-audio-pull-waiters.js";
describe("MeetingNodeAudioPullWaiters", () => {
afterEach(() => {
vi.useRealTimers();
});
it("removes a pull waiter when its timeout wins", async () => {
vi.useFakeTimers();
try {
const waiters = new MeetingNodeAudioPullWaiters();
const waiting = waiters.wait(250);
const waiters = new MeetingNodeAudioPullWaiters();
const waiting = waiters.wait(250);
expect(waiters.size).toBe(1);
await vi.advanceTimersByTimeAsync(250);
await waiting;
expect(waiters.size).toBe(1);
await vi.advanceTimersByTimeAsync(250);
await waiting;
expect(waiters.size).toBe(0);
} finally {
vi.useRealTimers();
}
expect(waiters.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
});
it("cancels a pull timeout when audio wakes the waiter early", async () => {
vi.useFakeTimers();
const waiters = new MeetingNodeAudioPullWaiters();
const waiting = waiters.wait(2_000);
expect(waiters.size).toBe(1);
expect(vi.getTimerCount()).toBe(1);
waiters.wake();
await waiting;
expect(waiters.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
});
it("releases its real timeout resource when audio wakes the waiter early", async () => {
const activeTimeouts = () =>
process.getActiveResourcesInfo().filter((resource) => resource === "Timeout").length;
const baselineTimeouts = activeTimeouts();
const waiters = new MeetingNodeAudioPullWaiters();
const waiting = waiters.wait(2_000);
expect(waiters.size).toBe(1);
expect(activeTimeouts()).toBe(baselineTimeouts + 1);
waiters.wake();
await waiting;
expect(waiters.size).toBe(0);
expect(activeTimeouts()).toBe(baselineTimeouts);
});
it("cancels every pull timeout when a wake releases concurrent waiters", async () => {
vi.useFakeTimers();
const waiters = new MeetingNodeAudioPullWaiters();
const waiting = [waiters.wait(2_000), waiters.wait(2_000)];
expect(waiters.size).toBe(2);
expect(vi.getTimerCount()).toBe(2);
waiters.wake();
await Promise.all(waiting);
expect(waiters.size).toBe(0);
expect(vi.getTimerCount()).toBe(0);
});
});