fix(presence-sync): tick mutex so setInterval overlap can't spawn parallel ticks
The presence-sync tick iterates accounts serially with await on each agent-login + PUT round-trip — a single tick can easily run 20+s when there are several accounts. setInterval(intervalMs) does NOT wait for the previous tick to finish, so on a busy gateway the next tick fires on top of a still-running one and two parallel iterations each PUT the same agentId within ~10 ms. That tipped the guild backend's first-time-insert race (separate fix in nav/Fabric.Backend.Guild) into 500s on prod (caught in t2 gateway 2026-05-25 23:23:35Z; 6 of 6 agents showed paired log lines 4-10 ms apart for the same agent → idle). Fix: a simple `inflight` boolean. tick() returns immediately if already running; the next interval beat catches up. lastStatus !== bridge.get gating already means status changes catch the next tick anyway, so skipping a beat costs nothing the next beat won't fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -50,6 +50,16 @@ export class PresenceSync {
|
||||
private readonly accounts = new Map<string, PresenceSyncAccount>();
|
||||
private readonly tokenCache = new Map<string, CachedToken>(); // by agentId
|
||||
|
||||
// Mutex flag: a tick iterates accounts serially with `await` on each
|
||||
// agent-login + PUT round-trip, so a single tick can easily run 20+s
|
||||
// when there are many accounts. setInterval(intervalMs) does NOT wait
|
||||
// for the previous tick to finish — without this guard the next tick
|
||||
// fires on top of a still-running one and two parallel iterations
|
||||
// PUT the same agentId within milliseconds. That tipped the backend's
|
||||
// first-time-insert race (separate fix in Fabric.Backend.Guild) into
|
||||
// 500s on prod. Guarded ticks just skip a beat instead.
|
||||
private inflight = false;
|
||||
|
||||
constructor(private readonly logger: Logger, private readonly client: FabricClient) {}
|
||||
|
||||
setAccounts(accounts: PresenceSyncAccount[]): void {
|
||||
@@ -103,6 +113,20 @@ export class PresenceSync {
|
||||
}
|
||||
|
||||
private async tick(): Promise<void> {
|
||||
// Mutex: see the `inflight` field declaration for the why. Drop
|
||||
// overlapping ticks rather than letting them run concurrently —
|
||||
// status is gated by `lastStatus !== bridge.get`, so skipping a
|
||||
// beat costs nothing the next beat won't catch.
|
||||
if (this.inflight) return;
|
||||
this.inflight = true;
|
||||
try {
|
||||
await this.tickInner();
|
||||
} finally {
|
||||
this.inflight = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async tickInner(): Promise<void> {
|
||||
const bridge = (globalThis as Record<string, unknown>)['__hfAgentStatus'] as Bridge | undefined;
|
||||
if (!bridge || typeof bridge.get !== 'function') return; // HF plugin not loaded — skip
|
||||
|
||||
|
||||
Reference in New Issue
Block a user