fix restart recovery active groups
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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개');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user