feat: add real tmux session manager

This commit is contained in:
chungyeong
2026-05-13 21:44:58 +09:00
parent aa3033771a
commit ef4c56e6b0
14 changed files with 3499 additions and 76 deletions

View File

@@ -112,9 +112,12 @@ export async function startM4Api(options: StartM4ApiOptions = {}): Promise<Start
sessionRecovery,
sessionManager,
async stop() {
await sessionManager.shutdown();
if (ownedClient) {
await dbClient.close();
try {
await sessionManager.shutdown();
} finally {
if (ownedClient) {
await dbClient.close();
}
}
},
};

View File

@@ -220,6 +220,46 @@ describe("startWorker", () => {
});
await next.shutdown();
});
it("drains SessionManager resources when Temporal worker shutdown fails", async () => {
client = createDbClient(databaseUrl);
const workspaceRoot = realpathSync(mkdtempSync(join(tmpdir(), "devflow-worker-shutdown-")));
tempRoots.push(workspaceRoot);
const connection = countingConnection();
const worker = await startWorkerWhenLockFree({
config: {
DATABASE_URL: databaseUrl,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
WORKSPACE_ROOT: workspaceRoot,
MAX_CONCURRENT_RUNS: 4,
backends: [{ id: "fake", enabled: true }],
},
dbClient: client,
recoveryRunIds: [],
connectionFactory: async () => connection,
workerFactory: async () => failingShutdownWorker(),
});
await expect(worker.shutdown()).rejects.toThrow("worker shutdown failed");
expect(connection.closes).toBe(1);
const next = await startWorkerWhenLockFree({
config: {
DATABASE_URL: databaseUrl,
LOG_LEVEL: "info",
TEMPORAL_ADDRESS: "localhost:7233",
WORKSPACE_ROOT: workspaceRoot,
MAX_CONCURRENT_RUNS: 4,
backends: [{ id: "fake", enabled: true }],
},
dbClient: client,
recoveryRunIds: [],
connectionFactory: async () => fakeConnection(),
workerFactory: async () => fakeWorker(),
});
await next.shutdown();
});
});
function fakeConnection() {
@@ -257,6 +297,15 @@ function countingWorker() {
};
}
function failingShutdownWorker() {
return {
run: async () => undefined,
shutdown() {
throw new Error("worker shutdown failed");
},
};
}
async function startWorkerWhenLockFree(options: Parameters<typeof startWorker>[0]) {
const deadline = Date.now() + 6_000;
let lastError: unknown;

View File

@@ -71,11 +71,23 @@ export async function startWorker(options: StartWorkerOptions = {}) {
let shutdownPromise: Promise<void> | undefined;
const shutdown = () => {
shutdownPromise ??= (async () => {
await Promise.resolve(startedWorker.shutdown());
await sessionManager.shutdown();
await startedConnection.close();
if (ownedClient) {
await dbClient.close();
let workerShutdownError: unknown;
try {
await Promise.resolve(startedWorker.shutdown());
} catch (error) {
workerShutdownError = error;
} finally {
try {
await sessionManager.shutdown();
} finally {
await startedConnection.close();
if (ownedClient) {
await dbClient.close();
}
}
}
if (workerShutdownError !== undefined) {
throw workerShutdownError;
}
})();
return shutdownPromise;