Skip to content

Commit c01a4f1

Browse files
authored
feat(supervisor): cancel a resumed run's in-flight checkpoint (#4502)
A run controller must call the continue route to resume, so the supervisor already knows synchronously that any checkpoint still running for that run is pointless. It only acted on that for the compute path. The continue route now cancels it for the Kubernetes path too, matching what completion already does since #4493. Called after the reply so the runner is never delayed, and skipped when there is no checkpoint client or when the compute path owns the run. The request is bounded by a 5s timeout so a hung call cannot leave the handler pending. `checkpoint_cancel_requests_total{result}` records the outcome, using the same label names as the delete path where they overlap: `sent`, `no_client`, `not_applicable`, `http_error`. No changeset: `CheckpointClient` is a server-only internal API, same as #4493. refs TRI-12915
1 parent ca9a74e commit c01a4f1

2 files changed

Lines changed: 52 additions & 0 deletions

File tree

apps/supervisor/src/workloadServer/index.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,13 @@ const checkpointDeleteRequests = new Counter({
6060
registers: [register],
6161
});
6262

63+
const checkpointCancelRequests = new Counter({
64+
name: "checkpoint_cancel_requests_total",
65+
help: "Checkpoint cancel requests attempted when a run continues, by outcome",
66+
labelNames: ["result"],
67+
registers: [register],
68+
});
69+
6370
const WorkloadActionParams = z.object({
6471
runFriendlyId: z.string(),
6572
snapshotFriendlyId: z.string(),
@@ -283,6 +290,30 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
283290
checkpointDeleteRequests.inc({ result: "sent" });
284291
}
285292

293+
private async cancelCheckpointsAfterReply(runFriendlyId: string): Promise<void> {
294+
if (!this.checkpointClient) {
295+
checkpointCancelRequests.inc({ result: "no_client" });
296+
return;
297+
}
298+
299+
if (this.snapshotService) {
300+
checkpointCancelRequests.inc({ result: "not_applicable" });
301+
return;
302+
}
303+
304+
const [error, accepted] = await tryCatch(
305+
this.checkpointClient.cancelCheckpoints({ runFriendlyId })
306+
);
307+
308+
if (error || !accepted) {
309+
checkpointCancelRequests.inc({ result: "http_error" });
310+
this.logger.error("Failed to request checkpoint cancel", { runFriendlyId, error });
311+
return;
312+
}
313+
314+
checkpointCancelRequests.inc({ result: "sent" });
315+
}
316+
286317
/**
287318
* Sets common route meta on the wide-event state from URL params.
288319
*/
@@ -643,6 +674,8 @@ export class WorkloadServer extends EventEmitter<WorkloadServerEvents> {
643674
}
644675

645676
reply.json(continuationResult.data as WorkloadContinueRunExecutionResponseBody);
677+
678+
await this.cancelCheckpointsAfterReply(params.runFriendlyId);
646679
}
647680
),
648681
}

packages/core/src/v3/serverOnly/checkpointClient.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export type CheckpointClientOptions = {
1313
orchestrator: CheckpointType;
1414
};
1515

16+
const CANCEL_TIMEOUT_MS = 5_000;
17+
1618
export class CheckpointClient {
1719
private readonly logger = new SimpleStructuredLogger("checkpoint-client");
1820

@@ -157,4 +159,21 @@ export class CheckpointClient {
157159

158160
return true;
159161
}
162+
163+
async cancelCheckpoints({ runFriendlyId }: { runFriendlyId: string }): Promise<boolean> {
164+
const res = await fetch(
165+
new URL(`/api/v1/runs/${runFriendlyId}/checkpoints/cancel`, this.opts.apiUrl),
166+
{ method: "POST", signal: AbortSignal.timeout(CANCEL_TIMEOUT_MS) }
167+
);
168+
169+
if (!res.ok) {
170+
this.logger.error("[CheckpointClient] Cancel checkpoints request failed", {
171+
runFriendlyId,
172+
status: res.status,
173+
});
174+
return false;
175+
}
176+
177+
return true;
178+
}
160179
}

0 commit comments

Comments
 (0)