Skip to content

Commit db2de19

Browse files
committed
fix: run boot recovery under the CLI entry point too
Boot recovery (trace-poll resume, orphan benchmark/evaluation-run finalization) lived only in server/index.ts, which the CLI never executes — 'agent-health serve' imports app.js and listens itself, so recovery was dead code on the primary distribution path. Extract a shared runBootRecoverySafely() and call it post-listen from both entries (after AH_PORT reflects the bound port, since the trace poller makes HTTP self-calls). Signed-off-by: ashwin pc <ashwinpc@amazon.com>
1 parent 828dc9b commit db2de19

4 files changed

Lines changed: 52 additions & 24 deletions

File tree

cli/utils/startServer.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function startServer(options: StartOptions): Promise<number> {
4949

5050
const packageRoot = findPackageRoot();
5151
const serverPath = join(packageRoot, 'server', 'dist', 'app.js');
52-
const { createApp } = await import(serverPath);
52+
const { createApp, runBootRecoverySafely } = await import(serverPath);
5353

5454
const app = await createApp();
5555

@@ -83,5 +83,11 @@ export async function startServer(options: StartOptions): Promise<number> {
8383
if (actualPort !== options.port) {
8484
process.env.VITE_BACKEND_PORT = String(actualPort);
8585
}
86+
87+
// Post-listen boot recovery (orphan run finalization, trace-poll resume).
88+
// Must run after AH_PORT reflects the bound port because the trace poller
89+
// makes HTTP self-calls. Guarded for older compiled bundles without it.
90+
runBootRecoverySafely?.();
91+
8692
return actualPort;
8793
}

server/app.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,7 @@ export async function createApp(): Promise<Express> {
128128
}
129129

130130
export default createApp;
131+
132+
// Re-exported so the CLI entry (cli/utils/startServer.ts), which imports the
133+
// compiled app bundle, can run the same post-listen recovery as server/index.ts.
134+
export { runBootRecoverySafely } from './services/bootRecovery.js';

server/index.ts

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,7 @@ import { createApp } from './app.js';
1515
import { getStorageConfigFromFile, getObservabilityConfigFromFile, getStorageConfigFromTs, getObservabilityConfigFromTs } from './services/configService.js';
1616
import { findObservioRoot, spawnObservioAgent, OBSERVIO_DEFAULT_PORT, resetObservioPort, isPortFree, setObservioPort, waitForObservioReady, killObservioAgent } from './services/observioAgent.js';
1717
import { validateAwsCredentials } from './services/tracesService.js';
18-
import { resumePendingTracePollsSafely } from './services/traceRecoveryOnBoot.js';
19-
import { recoverOrphanBenchmarkRunsSafely } from './services/benchmarkRunRecoveryOnBoot.js';
20-
import { recoverOrphanEvaluationRunsSafely } from './services/evaluationRunRecoveryOnBoot.js';
21-
import { getStorageModule } from './adapters/index.js';
18+
import { runBootRecoverySafely } from './services/bootRecovery.js';
2219

2320
// Register server-side connectors (subprocess, claude-code)
2421
// This import has side effects that register connectors with the registry
@@ -159,25 +156,11 @@ async function startServer() {
159156
}
160157
console.log('');
161158

162-
// Resume orphan trace-mode polling that was lost during a restart.
163-
// Fire-and-forget — must never block server startup or crash on failure.
164-
// Runs after listen() so the poller's HTTP self-calls (asyncRunStorage)
165-
// can reach the local API.
166-
try {
167-
const storage = getStorageModule();
168-
if (storage) {
169-
resumePendingTracePollsSafely(storage);
170-
// Also fail out orphan BenchmarkRuns (status: 'running' for too long
171-
// with the runner long dead). Different bug class — see
172-
// server/services/benchmarkRunRecoveryOnBoot.ts.
173-
recoverOrphanBenchmarkRunsSafely(storage);
174-
// Top-level EvaluationRuns use a separate process-local registry;
175-
// finalize any persisted `running` docs left by the prior process.
176-
recoverOrphanEvaluationRunsSafely(storage);
177-
}
178-
} catch (err: any) {
179-
console.warn(`[bootRecovery] Could not start: ${err?.message || err}`);
180-
}
159+
// Resume orphan trace polling / finalize orphan runs lost in a restart.
160+
// Shared with the CLI entry point (cli/utils/startServer.ts) — see
161+
// server/services/bootRecovery.ts. Runs after listen() so the trace
162+
// poller's HTTP self-calls (asyncRunStorage) can reach the local API.
163+
runBootRecoverySafely();
181164

182165
// Graceful shutdown — stop background timers, kill child processes, drain connections
183166
const shutdown = (signal: string) => {

server/services/bootRecovery.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright OpenSearch Contributors
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
/**
7+
* Boot Recovery — shared post-listen recovery hooks.
8+
*
9+
* Historically these hooks lived only in server/index.ts, so they ran under
10+
* `node server/dist/index.js` (dev) but NEVER under the CLI (`agent-health
11+
* serve`), which imports app.js and listens itself. Every entry point that
12+
* listens should call this exactly once, after listen(), because the trace
13+
* poller makes HTTP self-calls to the local API.
14+
*/
15+
16+
import { getStorageModule } from '../adapters/index.js';
17+
import { resumePendingTracePollsSafely } from './traceRecoveryOnBoot.js';
18+
import { recoverOrphanBenchmarkRunsSafely } from './benchmarkRunRecoveryOnBoot.js';
19+
import { recoverOrphanEvaluationRunsSafely } from './evaluationRunRecoveryOnBoot.js';
20+
21+
/**
22+
* Fire-and-forget: must never block startup or throw.
23+
* Call once per process, after the HTTP server is listening.
24+
*/
25+
export function runBootRecoverySafely(): void {
26+
try {
27+
const storage = getStorageModule();
28+
if (!storage) return;
29+
resumePendingTracePollsSafely(storage);
30+
recoverOrphanBenchmarkRunsSafely(storage);
31+
recoverOrphanEvaluationRunsSafely(storage);
32+
} catch (err: any) {
33+
console.warn(`[bootRecovery] Could not start: ${err?.message || err}`);
34+
}
35+
}

0 commit comments

Comments
 (0)