fix(calls): one delayed-event restart in flight per id, with a timeout (#318)
CI / Build & Quality Checks (pull_request) Successful in 3m12s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 15m31s
CI / Build & Quality Checks (pull_request) Successful in 3m12s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 9s
CI / Playwright smoke (e2e) (pull_request) Successful in 15m31s
During a homeserver stall Element Call retries update_delayed_event every 2 s, and the host opened a new HTTP request each time: 12 pending in a 60 s stall, all landing together on recovery (404 x5, 429 x7). Share the in-flight restart per delay id and abort it after 5 s. Refs #318 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3878a85339
commit
4ddcda122a
@@ -28,7 +28,10 @@ import {
|
||||
} from 'matrix-js-sdk';
|
||||
import { getCallCapabilities } from './utils';
|
||||
import { downloadMedia, mxcUrlToHttp } from '../../utils/matrix';
|
||||
import { dedupeInFlight } from '../../utils/inFlight';
|
||||
|
||||
/** A little longer than Element Call's own 2 s timeout for a restart. */
|
||||
const RESTART_TIMEOUT_MS = 5000;
|
||||
export class CallWidgetDriver extends WidgetDriver {
|
||||
private allowedCapabilities: Set<Capability>;
|
||||
|
||||
@@ -129,8 +132,18 @@ export class CallWidgetDriver extends WidgetDriver {
|
||||
await this.mx._unstable_cancelScheduledDelayedEvent(delayId);
|
||||
}
|
||||
|
||||
// [Gitea #318] While the homeserver is stalled, Element Call times out each
|
||||
// restart locally after 2 s and asks again, so requests for the same delay
|
||||
// id piled up and hit the server together on recovery (404s, then 429s).
|
||||
// Share one in-flight restart per id, and give it a timeout of its own.
|
||||
private readonly restartOnce = dedupeInFlight<unknown>();
|
||||
|
||||
public async restartScheduledDelayedEvent(delayId: string): Promise<void> {
|
||||
await this.mx._unstable_restartScheduledDelayedEvent(delayId);
|
||||
await this.restartOnce(delayId, () =>
|
||||
this.mx._unstable_restartScheduledDelayedEvent(delayId, {
|
||||
localTimeoutMs: RESTART_TIMEOUT_MS,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async sendScheduledDelayedEvent(delayId: string): Promise<void> {
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { dedupeInFlight } from './inFlight';
|
||||
|
||||
test('dedupeInFlight: one request per key while pending, a new one after (#318)', async () => {
|
||||
const once = dedupeInFlight<number>();
|
||||
let calls = 0;
|
||||
let release!: () => void;
|
||||
const gate = new Promise<void>((r) => {
|
||||
release = r;
|
||||
});
|
||||
const run = async () => {
|
||||
calls += 1;
|
||||
await gate;
|
||||
return calls;
|
||||
};
|
||||
const a = once('d1', run);
|
||||
const b = once('d1', run);
|
||||
const c = once('d2', run);
|
||||
assert.equal(a, b);
|
||||
release();
|
||||
assert.deepEqual(await Promise.all([a, b, c]), [2, 2, 2]);
|
||||
assert.equal(calls, 2);
|
||||
await once('d1', run);
|
||||
assert.equal(calls, 3, 'settled requests are not reused');
|
||||
});
|
||||
|
||||
test('dedupeInFlight: a rejection clears the key', async () => {
|
||||
const once = dedupeInFlight<void>();
|
||||
await assert.rejects(once('k', () => Promise.reject(new Error('boom'))));
|
||||
await once('k', () => Promise.resolve());
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Share one in-flight promise per key: callers that ask again while a request
|
||||
* for the same key is still pending get that request instead of a new one.
|
||||
*/
|
||||
export function dedupeInFlight<T>(): (key: string, run: () => Promise<T>) => Promise<T> {
|
||||
const pending = new Map<string, Promise<T>>();
|
||||
return (key, run) => {
|
||||
const existing = pending.get(key);
|
||||
if (existing) return existing;
|
||||
const p = run().finally(() => pending.delete(key));
|
||||
pending.set(key, p);
|
||||
return p;
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user