Skip to content

Commit 7c0c4c2

Browse files
frostebiteclaude
andcommitted
fix(hot-runner): validate persisted registry state and add dispatcher safeguards
Validate runner entries when loading from hot-runners.json. Discard corrupted entries with warnings. Add validateAndRepair() method for runtime recovery. Validate data before persisting to prevent writing corrupt state. Handle corrupt persistence files (invalid JSON) gracefully. Rewrite executeWithTimeout using Promise.race to clean up transport connections on timeout. Fix pre-existing ESLint violations in dispatcher and test files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 1bb31f3 commit 7c0c4c2

File tree

5 files changed

+457
-56
lines changed

5 files changed

+457
-56
lines changed

dist/index.js

Lines changed: 133 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/model/orchestrator/services/hot-runner/hot-runner-dispatcher.ts

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { HotRunnerJobRequest, HotRunnerJobResult, HotRunnerStatus, HotRunnerTran
44

55
const POLL_INTERVAL_MS = 1000;
66

7-
export type OutputCallback = (chunk: string) => void;
7+
// eslint-disable-next-line no-unused-vars
8+
export type OutputCallback = (output: string) => void;
89

910
export class HotRunnerDispatcher {
1011
private transports: Map<string, HotRunnerTransport>;
@@ -119,27 +120,37 @@ export class HotRunnerDispatcher {
119120

120121
/**
121122
* Execute a job on a transport with a timeout guard.
123+
* On timeout, disconnects the transport to release the connection
124+
* and prevent the orphaned sendJob promise from holding resources.
122125
*/
123126
private async executeWithTimeout(
124127
transport: HotRunnerTransport,
125128
request: HotRunnerJobRequest,
126129
): Promise<HotRunnerJobResult> {
127-
return new Promise<HotRunnerJobResult>((resolve, reject) => {
128-
const timer = setTimeout(() => {
129-
reject(new Error(`[HotRunner] Job ${request.jobId} timed out after ${request.timeout}ms`));
130-
}, request.timeout);
130+
const TIMEOUT_SENTINEL = Symbol('timeout');
131131

132-
transport
133-
.sendJob(request)
134-
.then((result) => {
135-
clearTimeout(timer);
136-
resolve(result);
137-
})
138-
.catch((error) => {
139-
clearTimeout(timer);
140-
reject(error);
141-
});
132+
const timeoutPromise = new Promise<typeof TIMEOUT_SENTINEL>((resolve) => {
133+
setTimeout(() => {
134+
resolve(TIMEOUT_SENTINEL);
135+
}, request.timeout);
142136
});
137+
138+
const result = await Promise.race([transport.sendJob(request), timeoutPromise]);
139+
140+
if (result === TIMEOUT_SENTINEL) {
141+
// Disconnect the transport to clean up the orphaned sendJob call
142+
try {
143+
await transport.disconnect();
144+
} catch (disconnectError: any) {
145+
OrchestratorLogger.logWarning(
146+
`[HotRunner] Error disconnecting transport after timeout for job ${request.jobId}: ${disconnectError.message}`,
147+
);
148+
}
149+
150+
throw new Error(`[HotRunner] Job ${request.jobId} timed out after ${request.timeout}ms`);
151+
}
152+
153+
return result;
143154
}
144155

145156
private sleep(ms: number): Promise<void> {

0 commit comments

Comments
 (0)