fix(presence-sync): tick mutex (#8)
This commit was merged in pull request #8.
This commit is contained in:
24
dist/fabric/src/presence-sync.js
vendored
24
dist/fabric/src/presence-sync.js
vendored
@@ -8,6 +8,15 @@ export class PresenceSync {
|
|||||||
lastStatus = new Map(); // by agentId
|
lastStatus = new Map(); // by agentId
|
||||||
accounts = new Map();
|
accounts = new Map();
|
||||||
tokenCache = new Map(); // by agentId
|
tokenCache = new Map(); // 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.
|
||||||
|
inflight = false;
|
||||||
constructor(logger, client) {
|
constructor(logger, client) {
|
||||||
this.logger = logger;
|
this.logger = logger;
|
||||||
this.client = client;
|
this.client = client;
|
||||||
@@ -60,6 +69,21 @@ export class PresenceSync {
|
|||||||
return entry.token;
|
return entry.token;
|
||||||
}
|
}
|
||||||
async tick() {
|
async tick() {
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async tickInner() {
|
||||||
const bridge = globalThis['__hfAgentStatus'];
|
const bridge = globalThis['__hfAgentStatus'];
|
||||||
if (!bridge || typeof bridge.get !== 'function')
|
if (!bridge || typeof bridge.get !== 'function')
|
||||||
return; // HF plugin not loaded — skip
|
return; // HF plugin not loaded — skip
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ export class PresenceSync {
|
|||||||
private readonly accounts = new Map<string, PresenceSyncAccount>();
|
private readonly accounts = new Map<string, PresenceSyncAccount>();
|
||||||
private readonly tokenCache = new Map<string, CachedToken>(); // by agentId
|
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) {}
|
constructor(private readonly logger: Logger, private readonly client: FabricClient) {}
|
||||||
|
|
||||||
setAccounts(accounts: PresenceSyncAccount[]): void {
|
setAccounts(accounts: PresenceSyncAccount[]): void {
|
||||||
@@ -103,6 +113,20 @@ export class PresenceSync {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async tick(): Promise<void> {
|
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;
|
const bridge = (globalThis as Record<string, unknown>)['__hfAgentStatus'] as Bridge | undefined;
|
||||||
if (!bridge || typeof bridge.get !== 'function') return; // HF plugin not loaded — skip
|
if (!bridge || typeof bridge.get !== 'function') return; // HF plugin not loaded — skip
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user