fix: Gateway 후속 프로필 큐 정체를 자동 복구
PM2 Node client 세션을 직렬화하고 callback timeout 뒤 다음 orchestrator poll이 계속 진행되게 한다. 운영 판별과 제한 복구 경계 및 회귀 테스트를 함께 기록한다.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import { buildPm2StartOptions } from '../src/orchestrator/pm2ProcessManager.js';
|
||||
import {
|
||||
buildPm2StartOptions,
|
||||
Pm2ProcessManager,
|
||||
type Pm2Client,
|
||||
} from '../src/orchestrator/pm2ProcessManager.js';
|
||||
|
||||
describe('buildPm2StartOptions', () => {
|
||||
it('enforces bounded restart policy and strips inherited PM2 identity at the PM2 boundary', () => {
|
||||
@@ -56,3 +60,118 @@ describe('buildPm2StartOptions', () => {
|
||||
expect(options.env).not.toHaveProperty('args');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Pm2ProcessManager session recovery', () => {
|
||||
it('serializes concurrent PM2 sessions so one disconnect cannot interrupt another request', async () => {
|
||||
const events: string[] = [];
|
||||
let listCall = 0;
|
||||
let releaseFirstList: (() => void) | undefined;
|
||||
const pm2 = {
|
||||
connect(callback: Parameters<Pm2Client['connect']>[0]) {
|
||||
events.push('connect');
|
||||
callback();
|
||||
},
|
||||
disconnect() {
|
||||
events.push('disconnect');
|
||||
},
|
||||
list(callback: Parameters<Pm2Client['list']>[0]) {
|
||||
listCall += 1;
|
||||
const currentCall = listCall;
|
||||
events.push(`list:${currentCall}`);
|
||||
if (currentCall === 1) {
|
||||
releaseFirstList = () => callback(null, []);
|
||||
return;
|
||||
}
|
||||
callback(null, []);
|
||||
},
|
||||
start() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
stop() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
delete() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
} satisfies Pm2Client;
|
||||
const manager = new Pm2ProcessManager({ loadPm2: () => pm2 });
|
||||
|
||||
const first = manager.list();
|
||||
const second = manager.list();
|
||||
await vi.waitFor(() => expect(events).toEqual(['connect', 'list:1']));
|
||||
|
||||
releaseFirstList?.();
|
||||
await expect(Promise.all([first, second])).resolves.toEqual([[], []]);
|
||||
expect(events).toEqual(['connect', 'list:1', 'disconnect', 'connect', 'list:2', 'disconnect']);
|
||||
});
|
||||
|
||||
it('times out a lost PM2 callback and lets the next queued session proceed', async () => {
|
||||
let listCall = 0;
|
||||
const pm2 = {
|
||||
connect(callback: Parameters<Pm2Client['connect']>[0]) {
|
||||
callback();
|
||||
},
|
||||
disconnect() {},
|
||||
list(callback: Parameters<Pm2Client['list']>[0]) {
|
||||
listCall += 1;
|
||||
if (listCall === 1) {
|
||||
return;
|
||||
}
|
||||
callback(null, []);
|
||||
},
|
||||
start() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
stop() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
delete() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
} satisfies Pm2Client;
|
||||
const manager = new Pm2ProcessManager({
|
||||
loadPm2: () => pm2,
|
||||
listTimeoutMs: 10,
|
||||
});
|
||||
|
||||
await expect(manager.list()).rejects.toThrow('PM2 list timed out after 10ms.');
|
||||
await expect(manager.list()).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('disconnects a timed-out PM2 connection before releasing the serialized session', async () => {
|
||||
let connectCall = 0;
|
||||
let disconnectCall = 0;
|
||||
const pm2 = {
|
||||
connect(callback: Parameters<Pm2Client['connect']>[0]) {
|
||||
connectCall += 1;
|
||||
if (connectCall > 1) {
|
||||
callback();
|
||||
}
|
||||
},
|
||||
disconnect() {
|
||||
disconnectCall += 1;
|
||||
},
|
||||
list(callback: Parameters<Pm2Client['list']>[0]) {
|
||||
callback(null, []);
|
||||
},
|
||||
start() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
stop() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
delete() {
|
||||
throw new Error('unused');
|
||||
},
|
||||
} satisfies Pm2Client;
|
||||
const manager = new Pm2ProcessManager({
|
||||
loadPm2: () => pm2,
|
||||
connectTimeoutMs: 10,
|
||||
});
|
||||
|
||||
await expect(manager.list()).rejects.toThrow('PM2 connect timed out after 10ms.');
|
||||
expect(disconnectCall).toBe(1);
|
||||
await expect(manager.list()).resolves.toEqual([]);
|
||||
expect(disconnectCall).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user