refactor(team): extract spawn status backoff
This commit is contained in:
parent
e718ccf39a
commit
38b0a87d5d
3 changed files with 123 additions and 9 deletions
|
|
@ -51,6 +51,13 @@ import {
|
||||||
invalidateTeamLocalStateEpoch,
|
invalidateTeamLocalStateEpoch,
|
||||||
isTeamLocalStateEpochCurrent,
|
isTeamLocalStateEpochCurrent,
|
||||||
} from '../team/teamLocalStateEpoch';
|
} from '../team/teamLocalStateEpoch';
|
||||||
|
import {
|
||||||
|
clearAllMemberSpawnStatusesIpcBackoffs,
|
||||||
|
clearMemberSpawnStatusesIpcBackoff,
|
||||||
|
hasMemberSpawnStatusesIpcBackoff,
|
||||||
|
isMemberSpawnStatusesIpcBackoffActive,
|
||||||
|
recordMemberSpawnStatusesIpcRetryBackoff,
|
||||||
|
} from '../team/teamMemberSpawnStatusBackoff';
|
||||||
import {
|
import {
|
||||||
areInboxMessageArraysEquivalent,
|
areInboxMessageArraysEquivalent,
|
||||||
clearTeamMessageSelectorCaches,
|
clearTeamMessageSelectorCaches,
|
||||||
|
|
@ -173,7 +180,6 @@ const pendingFreshTeamMemberActivityMetaRefreshes = new Set<string>();
|
||||||
const pendingTeamPendingReplyRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
const pendingTeamPendingReplyRefreshTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
||||||
let inFlightGlobalTasksRefresh: Promise<void> | null = null;
|
let inFlightGlobalTasksRefresh: Promise<void> | null = null;
|
||||||
let pendingFreshGlobalTasksRefresh = false;
|
let pendingFreshGlobalTasksRefresh = false;
|
||||||
const memberSpawnStatusesIpcBackoffUntilByTeam = new Map<string, number>();
|
|
||||||
const teamRefreshBurstDiagnostics = new Map<
|
const teamRefreshBurstDiagnostics = new Map<
|
||||||
string,
|
string,
|
||||||
{ windowStartedAt: number; count: number; lastWarnAt: number }
|
{ windowStartedAt: number; count: number; lastWarnAt: number }
|
||||||
|
|
@ -241,7 +247,7 @@ export function __resetTeamSliceModuleStateForTests(): void {
|
||||||
clearAllPendingReplyRefreshWaits();
|
clearAllPendingReplyRefreshWaits();
|
||||||
clearAllLastResolvedTeamDataRefreshes();
|
clearAllLastResolvedTeamDataRefreshes();
|
||||||
clearAllTeamLocalStateEpochs();
|
clearAllTeamLocalStateEpochs();
|
||||||
memberSpawnStatusesIpcBackoffUntilByTeam.clear();
|
clearAllMemberSpawnStatusesIpcBackoffs();
|
||||||
teamRefreshBurstDiagnostics.clear();
|
teamRefreshBurstDiagnostics.clear();
|
||||||
memberSpawnUiEqualLastWarnAtByTeam.clear();
|
memberSpawnUiEqualLastWarnAtByTeam.clear();
|
||||||
resolvedMembersSelectorCache.clear();
|
resolvedMembersSelectorCache.clear();
|
||||||
|
|
@ -274,7 +280,7 @@ function clearTeamScopedTransientState(teamName: string): void {
|
||||||
inFlightTeamMemberActivityMetaRequests.delete(teamName);
|
inFlightTeamMemberActivityMetaRequests.delete(teamName);
|
||||||
pendingFreshTeamMemberActivityMetaRefreshes.delete(teamName);
|
pendingFreshTeamMemberActivityMetaRefreshes.delete(teamName);
|
||||||
clearLastResolvedTeamDataRefreshAt(teamName);
|
clearLastResolvedTeamDataRefreshAt(teamName);
|
||||||
memberSpawnStatusesIpcBackoffUntilByTeam.delete(teamName);
|
clearMemberSpawnStatusesIpcBackoff(teamName);
|
||||||
teamRefreshBurstDiagnostics.delete(teamName);
|
teamRefreshBurstDiagnostics.delete(teamName);
|
||||||
memberSpawnUiEqualLastWarnAtByTeam.delete(teamName);
|
memberSpawnUiEqualLastWarnAtByTeam.delete(teamName);
|
||||||
clearTeamScopedSelectorCaches(teamName);
|
clearTeamScopedSelectorCaches(teamName);
|
||||||
|
|
@ -660,7 +666,7 @@ export function __getTeamScopedTransientStateForTests(teamName: string): {
|
||||||
pendingFreshTeamMemberActivityMetaRefreshes.has(teamName),
|
pendingFreshTeamMemberActivityMetaRefreshes.has(teamName),
|
||||||
hasLastResolvedTeamDataRefresh: hasLastResolvedTeamDataRefreshAt(teamName),
|
hasLastResolvedTeamDataRefresh: hasLastResolvedTeamDataRefreshAt(teamName),
|
||||||
hasCurrentLocalStateEpoch: hasTeamLocalStateEpoch(teamName),
|
hasCurrentLocalStateEpoch: hasTeamLocalStateEpoch(teamName),
|
||||||
hasMemberSpawnStatusesIpcBackoff: memberSpawnStatusesIpcBackoffUntilByTeam.has(teamName),
|
hasMemberSpawnStatusesIpcBackoff: hasMemberSpawnStatusesIpcBackoff(teamName),
|
||||||
hasTeamRefreshBurstDiagnostics: teamRefreshBurstDiagnostics.has(teamName),
|
hasTeamRefreshBurstDiagnostics: teamRefreshBurstDiagnostics.has(teamName),
|
||||||
hasMemberSpawnUiEqualLastWarn: memberSpawnUiEqualLastWarnAtByTeam.has(teamName),
|
hasMemberSpawnUiEqualLastWarn: memberSpawnUiEqualLastWarnAtByTeam.has(teamName),
|
||||||
};
|
};
|
||||||
|
|
@ -2987,13 +2993,12 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
launchParamsByTeam: loadAllLaunchParams(),
|
launchParamsByTeam: loadAllLaunchParams(),
|
||||||
fetchMemberSpawnStatuses: async (teamName: string) => {
|
fetchMemberSpawnStatuses: async (teamName: string) => {
|
||||||
if (!api.teams?.getMemberSpawnStatuses) return;
|
if (!api.teams?.getMemberSpawnStatuses) return;
|
||||||
const backoffUntil = memberSpawnStatusesIpcBackoffUntilByTeam.get(teamName) ?? 0;
|
if (isMemberSpawnStatusesIpcBackoffActive(teamName)) {
|
||||||
if (backoffUntil > Date.now()) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const snapshot = await api.teams.getMemberSpawnStatuses(teamName);
|
const snapshot = await api.teams.getMemberSpawnStatuses(teamName);
|
||||||
memberSpawnStatusesIpcBackoffUntilByTeam.delete(teamName);
|
clearMemberSpawnStatusesIpcBackoff(teamName);
|
||||||
set((prev) => {
|
set((prev) => {
|
||||||
if (snapshot.runId != null && prev.ignoredRuntimeRunIds[snapshot.runId] === teamName) {
|
if (snapshot.runId != null && prev.ignoredRuntimeRunIds[snapshot.runId] === teamName) {
|
||||||
return {};
|
return {};
|
||||||
|
|
@ -3057,9 +3062,9 @@ export const createTeamSlice: StateCreator<AppState, [], [], TeamSlice> = (set,
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const message = error instanceof Error ? error.message : String(error);
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
if (message.includes("No handler registered for 'team:memberSpawnStatuses'")) {
|
if (message.includes("No handler registered for 'team:memberSpawnStatuses'")) {
|
||||||
memberSpawnStatusesIpcBackoffUntilByTeam.set(
|
recordMemberSpawnStatusesIpcRetryBackoff(
|
||||||
teamName,
|
teamName,
|
||||||
Date.now() + MEMBER_SPAWN_STATUSES_IPC_RETRY_BACKOFF_MS
|
MEMBER_SPAWN_STATUSES_IPC_RETRY_BACKOFF_MS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// ignore — spawn statuses are best-effort
|
// ignore — spawn statuses are best-effort
|
||||||
|
|
|
||||||
39
src/renderer/store/team/teamMemberSpawnStatusBackoff.ts
Normal file
39
src/renderer/store/team/teamMemberSpawnStatusBackoff.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
const memberSpawnStatusesIpcBackoffUntilByTeam = new Map<string, number>();
|
||||||
|
|
||||||
|
export function getMemberSpawnStatusesIpcBackoffUntil(teamName: string): number {
|
||||||
|
return memberSpawnStatusesIpcBackoffUntilByTeam.get(teamName) ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasMemberSpawnStatusesIpcBackoff(teamName: string): boolean {
|
||||||
|
return memberSpawnStatusesIpcBackoffUntilByTeam.has(teamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isMemberSpawnStatusesIpcBackoffActive(
|
||||||
|
teamName: string,
|
||||||
|
now = Date.now()
|
||||||
|
): boolean {
|
||||||
|
return getMemberSpawnStatusesIpcBackoffUntil(teamName) > now;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordMemberSpawnStatusesIpcBackoffUntil(
|
||||||
|
teamName: string,
|
||||||
|
backoffUntil: number
|
||||||
|
): void {
|
||||||
|
memberSpawnStatusesIpcBackoffUntilByTeam.set(teamName, backoffUntil);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function recordMemberSpawnStatusesIpcRetryBackoff(
|
||||||
|
teamName: string,
|
||||||
|
retryBackoffMs: number,
|
||||||
|
now = Date.now()
|
||||||
|
): void {
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil(teamName, now + retryBackoffMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearMemberSpawnStatusesIpcBackoff(teamName: string): void {
|
||||||
|
memberSpawnStatusesIpcBackoffUntilByTeam.delete(teamName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAllMemberSpawnStatusesIpcBackoffs(): void {
|
||||||
|
memberSpawnStatusesIpcBackoffUntilByTeam.clear();
|
||||||
|
}
|
||||||
70
test/renderer/store/teamMemberSpawnStatusBackoff.test.ts
Normal file
70
test/renderer/store/teamMemberSpawnStatusBackoff.test.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import {
|
||||||
|
clearAllMemberSpawnStatusesIpcBackoffs,
|
||||||
|
clearMemberSpawnStatusesIpcBackoff,
|
||||||
|
getMemberSpawnStatusesIpcBackoffUntil,
|
||||||
|
hasMemberSpawnStatusesIpcBackoff,
|
||||||
|
isMemberSpawnStatusesIpcBackoffActive,
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil,
|
||||||
|
recordMemberSpawnStatusesIpcRetryBackoff,
|
||||||
|
} from '../../../src/renderer/store/team/teamMemberSpawnStatusBackoff';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
clearAllMemberSpawnStatusesIpcBackoffs();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('teamMemberSpawnStatusBackoff', () => {
|
||||||
|
it('defaults to no backoff for unknown teams', () => {
|
||||||
|
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(0);
|
||||||
|
expect(hasMemberSpawnStatusesIpcBackoff('my-team')).toBe(false);
|
||||||
|
expect(isMemberSpawnStatusesIpcBackoffActive('my-team', 100)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('tracks active backoff deadlines by team', () => {
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil('my-team', 150);
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil('other-team', 250);
|
||||||
|
|
||||||
|
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(150);
|
||||||
|
expect(isMemberSpawnStatusesIpcBackoffActive('my-team', 149)).toBe(true);
|
||||||
|
expect(isMemberSpawnStatusesIpcBackoffActive('my-team', 150)).toBe(false);
|
||||||
|
expect(isMemberSpawnStatusesIpcBackoffActive('other-team', 249)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records retry backoff from Date.now by default', () => {
|
||||||
|
vi.setSystemTime(new Date('2026-05-22T07:00:00.000Z'));
|
||||||
|
|
||||||
|
recordMemberSpawnStatusesIpcRetryBackoff('my-team', 5_000);
|
||||||
|
|
||||||
|
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(
|
||||||
|
new Date('2026-05-22T07:00:05.000Z').getTime()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('records retry backoff from an explicit clock for deterministic callers', () => {
|
||||||
|
recordMemberSpawnStatusesIpcRetryBackoff('my-team', 5_000, 100);
|
||||||
|
|
||||||
|
expect(getMemberSpawnStatusesIpcBackoffUntil('my-team')).toBe(5_100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears one team backoff without touching others', () => {
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil('my-team', 150);
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil('other-team', 250);
|
||||||
|
|
||||||
|
clearMemberSpawnStatusesIpcBackoff('my-team');
|
||||||
|
|
||||||
|
expect(hasMemberSpawnStatusesIpcBackoff('my-team')).toBe(false);
|
||||||
|
expect(getMemberSpawnStatusesIpcBackoffUntil('other-team')).toBe(250);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('clears all recorded backoffs', () => {
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil('my-team', 150);
|
||||||
|
recordMemberSpawnStatusesIpcBackoffUntil('other-team', 250);
|
||||||
|
|
||||||
|
clearAllMemberSpawnStatusesIpcBackoffs();
|
||||||
|
|
||||||
|
expect(hasMemberSpawnStatusesIpcBackoff('my-team')).toBe(false);
|
||||||
|
expect(hasMemberSpawnStatusesIpcBackoff('other-team')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
Loading…
Reference in a new issue