perf(team): skip redundant task interval resumes for stable alive members

readPersistedStatuses каждый раз делал полный sync-scan всех task
JSON под file lock и звал resumeActiveIntervalsForMember для каждого
member с runtimeAlive=true — на больших командах блокировал main
до 8s. Теперь маркируем member как 'resume applied' пока он остаётся
alive, сбрасываем маркер при переходе в not-alive (через
syncMemberTaskActivityForRuntimeTransition и в readPersistedStatuses
loop). Resume остаётся идемпотентным и материализует интервалы из
истории один раз за цикл alive.
This commit is contained in:
777genius 2026-05-29 00:14:40 +03:00
parent 068399543e
commit d9479b5c61
2 changed files with 141 additions and 6 deletions

View file

@ -3549,6 +3549,7 @@ export class TeamProvisioningService {
private readonly transcriptProjectResolver: TeamTranscriptProjectResolver;
private readonly taskActivityIntervalService = new TeamTaskActivityIntervalService();
private readonly leadTaskActivitySyncedRunKeys = new Set<string>();
private readonly memberTaskActivityResumeAppliedByTeam = new Map<string, Set<string>>();
private readonly crashRepairedActivityIntervalsByTeam = new Set<string>();
private readonly pendingCrashRepairSnapshotByTeam = new Map<
string,
@ -3782,6 +3783,49 @@ export class TeamProvisioningService {
this.runtimeProcessRowsForUsageSnapshotByTeam.delete(teamName);
}
private normalizeMemberKeyForTaskActivity(memberName: string): string {
return memberName.trim().toLowerCase();
}
private getMemberTaskActivityResumeAppliedSet(teamName: string): Set<string> {
let applied = this.memberTaskActivityResumeAppliedByTeam.get(teamName);
if (!applied) {
applied = new Set();
this.memberTaskActivityResumeAppliedByTeam.set(teamName, applied);
}
return applied;
}
private resumeTaskActivityIntervalsForAliveMember(
teamName: string,
memberName: string,
at: string
): void {
const memberKey = this.normalizeMemberKeyForTaskActivity(memberName);
if (!memberKey) return;
const applied = this.getMemberTaskActivityResumeAppliedSet(teamName);
if (applied.has(memberKey)) return;
const result = this.taskActivityIntervalService.resumeActiveIntervalsForMember(
teamName,
memberName,
at
);
if (!result.failed) {
applied.add(memberKey);
}
}
private dropTaskActivityResumeMarkerForMember(teamName: string, memberName: string): void {
const memberKey = this.normalizeMemberKeyForTaskActivity(memberName);
if (!memberKey) return;
const applied = this.memberTaskActivityResumeAppliedByTeam.get(teamName);
if (!applied) return;
applied.delete(memberKey);
if (applied.size === 0) {
this.memberTaskActivityResumeAppliedByTeam.delete(teamName);
}
}
private cloneMemberSpawnStatusesSnapshot(
snapshot: MemberSpawnStatusesSnapshot
): MemberSpawnStatusesSnapshot {
@ -13587,6 +13631,7 @@ export class TeamProvisioningService {
): void {
if (previous.runtimeAlive === true && next.runtimeAlive !== true) {
this.pauseMemberTaskActivityForRuntimeLoss(run, memberName, previous, observedAt);
this.dropTaskActivityResumeMarkerForMember(run.teamName, memberName);
} else if (previous.runtimeAlive !== true && next.runtimeAlive === true) {
const nextUpdatedMs = parseOptionalIsoMs(next.updatedAt);
const previousUpdatedMs = parseOptionalIsoMs(previous.updatedAt);
@ -13594,7 +13639,7 @@ export class TeamProvisioningService {
nextUpdatedMs > 0 && (previousUpdatedMs <= 0 || nextUpdatedMs > previousUpdatedMs)
? next.updatedAt
: nowIso();
this.taskActivityIntervalService.resumeActiveIntervalsForMember(
this.resumeTaskActivityIntervalsForAliveMember(
run.teamName,
memberName,
deriveTaskActivityResumeAt(previous, observedAt, resumeFallbackAt)
@ -13940,11 +13985,9 @@ export class TeamProvisioningService {
const runtimeObservedAt = nowIso();
for (const [memberName, entry] of Object.entries(nextStatuses)) {
if (entry.runtimeAlive === true) {
this.taskActivityIntervalService.resumeActiveIntervalsForMember(
teamName,
memberName,
runtimeObservedAt
);
this.resumeTaskActivityIntervalsForAliveMember(teamName, memberName, runtimeObservedAt);
} else {
this.dropTaskActivityResumeMarkerForMember(teamName, memberName);
}
}
const expectedMembers = snapshot ? this.getPersistedLaunchMemberNames(snapshot) : undefined;

View file

@ -2663,6 +2663,98 @@ describe('TeamProvisioningService', () => {
pauseSpy.mockRestore();
}
});
it('skips redundant resumeActiveIntervalsForMember calls while a member stays alive', () => {
const resumeSpy = vi
.spyOn(TeamTaskActivityIntervalService.prototype, 'resumeActiveIntervalsForMember')
.mockReturnValue({ changedTasks: 0 });
try {
const svc = new TeamProvisioningService();
const internals = svc as unknown as {
resumeTaskActivityIntervalsForAliveMember: (
teamName: string,
memberName: string,
at: string
) => void;
dropTaskActivityResumeMarkerForMember: (teamName: string, memberName: string) => void;
};
const teamName = 'member-task-activity-resume-dedup-team';
internals.resumeTaskActivityIntervalsForAliveMember(
teamName,
'alice',
'2026-05-29T00:00:00.000Z'
);
internals.resumeTaskActivityIntervalsForAliveMember(
teamName,
'alice',
'2026-05-29T00:00:05.000Z'
);
internals.resumeTaskActivityIntervalsForAliveMember(
teamName,
'ALICE',
'2026-05-29T00:00:10.000Z'
);
expect(resumeSpy).toHaveBeenCalledTimes(1);
expect(resumeSpy).toHaveBeenNthCalledWith(
1,
teamName,
'alice',
'2026-05-29T00:00:00.000Z'
);
internals.dropTaskActivityResumeMarkerForMember(teamName, 'alice');
internals.resumeTaskActivityIntervalsForAliveMember(
teamName,
'alice',
'2026-05-29T00:01:00.000Z'
);
expect(resumeSpy).toHaveBeenCalledTimes(2);
expect(resumeSpy).toHaveBeenNthCalledWith(
2,
teamName,
'alice',
'2026-05-29T00:01:00.000Z'
);
} finally {
resumeSpy.mockRestore();
}
});
it('retries resumeActiveIntervalsForMember when the previous call reported failure', () => {
const resumeSpy = vi
.spyOn(TeamTaskActivityIntervalService.prototype, 'resumeActiveIntervalsForMember')
.mockReturnValueOnce({ changedTasks: 0, failed: true })
.mockReturnValueOnce({ changedTasks: 1 });
try {
const svc = new TeamProvisioningService();
const internals = svc as unknown as {
resumeTaskActivityIntervalsForAliveMember: (
teamName: string,
memberName: string,
at: string
) => void;
};
const teamName = 'member-task-activity-resume-retry-team';
internals.resumeTaskActivityIntervalsForAliveMember(
teamName,
'bob',
'2026-05-29T00:00:00.000Z'
);
internals.resumeTaskActivityIntervalsForAliveMember(
teamName,
'bob',
'2026-05-29T00:00:05.000Z'
);
expect(resumeSpy).toHaveBeenCalledTimes(2);
} finally {
resumeSpy.mockRestore();
}
});
});
describe('member spawn status launch reads', () => {