fix restart recovery active groups

This commit is contained in:
ejclaw
2026-05-21 04:51:42 +09:00
parent 384fce81a3
commit 5d2f7f8748
3 changed files with 75 additions and 21 deletions

View File

@@ -34,6 +34,7 @@ import {
buildRestartAnnouncement, buildRestartAnnouncement,
buildInterruptedRestartAnnouncement, buildInterruptedRestartAnnouncement,
consumeRestartContext, consumeRestartContext,
getRecoverableInterruptedGroups,
getInterruptedRecoveryCandidates, getInterruptedRecoveryCandidates,
inferRecentRestartContext, inferRecentRestartContext,
type RestartContext, type RestartContext,
@@ -196,7 +197,9 @@ async function announceRestartRecovery(
'Sent explicit restart recovery announcement', 'Sent explicit restart recovery announcement',
); );
for (const interrupted of explicitContext.interruptedGroups ?? []) { for (const interrupted of getRecoverableInterruptedGroups(
explicitContext,
)) {
if (interrupted.chatJid === explicitContext.chatJid) continue; if (interrupted.chatJid === explicitContext.chatJid) continue;
if (hasRecentRestartAnnouncement(interrupted.chatJid, dedupeSince)) { if (hasRecentRestartAnnouncement(interrupted.chatJid, dedupeSince)) {
continue; continue;
@@ -265,8 +268,8 @@ async function main(): Promise<void> {
( (
status, status,
): status is typeof status & { ): status is typeof status & {
status: 'processing' | 'waiting'; status: 'processing';
} => status.status !== 'inactive', } => status.status === 'processing',
) )
.map((status) => ({ .map((status) => ({
chatJid: status.jid, chatJid: status.jid,

View File

@@ -1,6 +1,8 @@
import { describe, expect, it } from 'vitest'; import { describe, expect, it } from 'vitest';
import { import {
buildRestartAnnouncement,
getRecoverableInterruptedGroups,
getInterruptedRecoveryCandidates, getInterruptedRecoveryCandidates,
type RestartContext, type RestartContext,
} from './restart-context.js'; } from './restart-context.js';
@@ -16,8 +18,8 @@ function makeGroup(folder: string): RegisteredGroup {
}; };
} }
describe('getInterruptedRecoveryCandidates', () => { describe('restart recovery context', () => {
it('returns only registered interrupted groups and deduplicates by chatJid', () => { it('returns only registered processing groups and deduplicates by chatJid', () => {
const roomBindings: Record<string, RegisteredGroup> = { const roomBindings: Record<string, RegisteredGroup> = {
'dc:1': makeGroup('group-one'), 'dc:1': makeGroup('group-one'),
'dc:2': makeGroup('group-two'), 'dc:2': makeGroup('group-two'),
@@ -38,8 +40,8 @@ describe('getInterruptedRecoveryCandidates', () => {
pendingTasks: 0, pendingTasks: 0,
}, },
{ {
chatJid: 'dc:1', chatJid: 'dc:2',
groupName: 'one-duplicate', groupName: 'two-waiting',
status: 'waiting', status: 'waiting',
elapsedMs: null, elapsedMs: null,
pendingMessages: false, pendingMessages: false,
@@ -47,7 +49,7 @@ describe('getInterruptedRecoveryCandidates', () => {
}, },
{ {
chatJid: 'dc:2', chatJid: 'dc:2',
groupName: 'two', groupName: 'two-idle',
status: 'idle', status: 'idle',
elapsedMs: null, elapsedMs: null,
pendingMessages: false, pendingMessages: false,
@@ -61,6 +63,14 @@ describe('getInterruptedRecoveryCandidates', () => {
pendingMessages: true, pendingMessages: true,
pendingTasks: 0, pendingTasks: 0,
}, },
{
chatJid: 'dc:1',
groupName: 'one-duplicate',
status: 'processing',
elapsedMs: 2000,
pendingMessages: false,
pendingTasks: 1,
},
], ],
}; };
@@ -72,17 +82,40 @@ describe('getInterruptedRecoveryCandidates', () => {
pendingMessages: true, pendingMessages: true,
pendingTasks: 0, pendingTasks: 0,
}, },
{
chatJid: 'dc:2',
groupFolder: 'group-two',
status: 'idle',
pendingMessages: false,
pendingTasks: 0,
},
]); ]);
}); });
it('returns empty when there is no explicit restart context', () => { it('returns empty when there is no explicit restart context', () => {
expect(getInterruptedRecoveryCandidates(null, {})).toEqual([]); expect(getInterruptedRecoveryCandidates(null, {})).toEqual([]);
}); });
it('uses only processing groups for restart announcements', () => {
const context: RestartContext = {
chatJid: 'dc:main',
summary: 'restart',
verify: [],
writtenAt: '2026-05-21T00:00:00.000Z',
interruptedGroups: [
{
chatJid: 'dc:processing',
groupName: 'processing',
status: 'processing',
elapsedMs: 1000,
pendingMessages: false,
pendingTasks: 0,
},
{
chatJid: 'dc:waiting',
groupName: 'waiting',
status: 'waiting',
elapsedMs: null,
pendingMessages: true,
pendingTasks: 1,
},
],
};
expect(getRecoverableInterruptedGroups(context)).toHaveLength(1);
expect(buildRestartAnnouncement(context)).toContain('중단 작업 감지: 1개');
});
}); });

View File

@@ -72,6 +72,20 @@ function readRestartContextFile(filePath: string): RestartContext | null {
return data; return data;
} }
function isRecoverableInterruptedGroup(
group: RestartInterruptedGroup,
): boolean {
return group.status === 'processing';
}
export function getRecoverableInterruptedGroups(
context: Pick<RestartContext, 'interruptedGroups'> | null,
): RestartInterruptedGroup[] {
return (context?.interruptedGroups ?? []).filter(
isRecoverableInterruptedGroup,
);
}
function getLatestDistBuildTime(): number | null { function getLatestDistBuildTime(): number | null {
const distDir = path.join(process.cwd(), 'dist'); const distDir = path.join(process.cwd(), 'dist');
if (!fs.existsSync(distDir)) return null; if (!fs.existsSync(distDir)) return null;
@@ -145,7 +159,10 @@ export function writeShutdownRestartContext(
signal: string, signal: string,
serviceIds: string[] = [SERVICE_ID], serviceIds: string[] = [SERVICE_ID],
): string[] { ): string[] {
if (interruptedGroups.length === 0) return []; const recoverableInterruptedGroups = interruptedGroups.filter(
isRecoverableInterruptedGroup,
);
if (recoverableInterruptedGroups.length === 0) return [];
fs.mkdirSync(DATA_DIR, { recursive: true }); fs.mkdirSync(DATA_DIR, { recursive: true });
const mainChatJid = const mainChatJid =
@@ -156,8 +173,8 @@ export function writeShutdownRestartContext(
const filePath = getRestartContextPath(serviceId); const filePath = getRestartContextPath(serviceId);
const existing = readRestartContextFile(filePath); const existing = readRestartContextFile(filePath);
const mergedInterrupted = [ const mergedInterrupted = [
...(existing?.interruptedGroups ?? []), ...getRecoverableInterruptedGroups(existing),
...interruptedGroups, ...recoverableInterruptedGroups,
].filter( ].filter(
(group, index, all) => (group, index, all) =>
all.findIndex((candidate) => candidate.chatJid === group.chatJid) === all.findIndex((candidate) => candidate.chatJid === group.chatJid) ===
@@ -221,8 +238,9 @@ export function consumeRestartContext(): RestartContext | null {
export function buildRestartAnnouncement(context: RestartContext): string { export function buildRestartAnnouncement(context: RestartContext): string {
const lines = ['재시작 완료.', `- 변경: ${context.summary}`]; const lines = ['재시작 완료.', `- 변경: ${context.summary}`];
if (context.interruptedGroups && context.interruptedGroups.length > 0) { const recoverableInterruptedGroups = getRecoverableInterruptedGroups(context);
lines.push(`- 중단 작업 감지: ${context.interruptedGroups.length}`); if (recoverableInterruptedGroups.length > 0) {
lines.push(`- 중단 작업 감지: ${recoverableInterruptedGroups.length}`);
} }
if (context.verify.length > 0) { if (context.verify.length > 0) {
lines.push(`- 검증: ${context.verify.join(', ')}`); lines.push(`- 검증: ${context.verify.join(', ')}`);
@@ -240,7 +258,7 @@ export function getInterruptedRecoveryCandidates(
const seen = new Set<string>(); const seen = new Set<string>();
const candidates: RestartRecoveryCandidate[] = []; const candidates: RestartRecoveryCandidate[] = [];
for (const interrupted of context.interruptedGroups) { for (const interrupted of getRecoverableInterruptedGroups(context)) {
if (seen.has(interrupted.chatJid)) continue; if (seen.has(interrupted.chatJid)) continue;
const group = roomBindings[interrupted.chatJid]; const group = roomBindings[interrupted.chatJid];
if (!group) continue; if (!group) continue;