Skip to content
File

Blob: src/worker/dispatch/shared/run-execution-context/control.ts

typescript122 lines
1import {
2 isTerminalStatus,
3 type ProjectRunStatus,
4 type RunMetaState,
5 type UpdateRunStateInput,
6} from "@/worker/contracts";
7import { RunStateTransitionError } from "@/worker/durable/run-do/state";
8 
9import { logger } from "./shared";
10import type {
11 RunControl,
12 RunExecutionContextState,
13 RunExecutionOutcome,
14 RunExecutionScope,
15 RunStateUpdateOverrides,
16 RunStore,
17} from "./types";
18 
19export const updateRunFromCurrent = async (
20 scope: RunExecutionScope,
21 state: RunExecutionContextState,
22 runStore: RunStore,
23 current: RunMetaState,
24 status: UpdateRunStateInput["status"],
25 overrides: RunStateUpdateOverrides = {},
26): Promise<RunMetaState> => {
27 const hasOverride = <TKey extends keyof typeof overrides>(key: TKey): boolean =>
28 Object.prototype.hasOwnProperty.call(overrides, key);
29 
30 const result = await runStore.tryUpdateState({
31 status,
32 currentStep: hasOverride("currentStep") ? overrides.currentStep : current.currentStep,
33 startedAt: hasOverride("startedAt") ? overrides.startedAt : current.startedAt,
34 finishedAt: hasOverride("finishedAt") ? overrides.finishedAt : current.finishedAt,
35 exitCode: hasOverride("exitCode") ? overrides.exitCode : current.exitCode,
36 errorMessage: hasOverride("errorMessage") ? overrides.errorMessage : current.errorMessage,
37 });
38 
39 if (result.kind === "conflict") {
40 throw new RunStateTransitionError(result.reason, result.current.status, status);
41 }
42 
43 return result.state;
44};
45 
46export const preserveTerminalOutcome = (
47 state: RunExecutionContextState,
48 outcome: Exclude<RunExecutionOutcome, { kind: "ownership_lost" | "canceled" }>,
49): void => {
50 if (outcome.kind === "passed" || outcome.kind === "failed") {
51 state.preservedTerminalStatus = outcome.kind;
52 }
53};
54 
55export const ensureRunCancelRequested = async (
56 scope: RunExecutionScope,
57 state: RunExecutionContextState,
58 runStore: RunStore,
59): Promise<RunMetaState | null> => {
60 const current = await runStore.getMeta();
61 if (isTerminalStatus(current.status)) {
62 return current;
63 }
64 
65 if (state.preservedTerminalStatus !== null) {
66 return current;
67 }
68 
69 if (current.status === "queued" || current.status === "cancel_requested" || current.status === "canceling") {
70 return current;
71 }
72 
73 return await updateRunFromCurrent(scope, state, runStore, current, "cancel_requested", {
74 startedAt: current.startedAt ?? scope.startedAt,
75 });
76};
77 
78export const ensureRunCanceling = async (
79 scope: RunExecutionScope,
80 state: RunExecutionContextState,
81 runStore: RunStore,
82): Promise<RunMetaState | null> => {
83 const current = await ensureRunCancelRequested(scope, state, runStore);
84 if (!current || isTerminalStatus(current.status)) {
85 return current;
86 }
87 
88 if (state.preservedTerminalStatus !== null) {
89 return current;
90 }
91 
92 if (current.status === "canceling" || current.status === "queued") {
93 return current;
94 }
95 
96 if (current.status === "cancel_requested") {
97 return await updateRunFromCurrent(scope, state, runStore, current, "canceling", {
98 startedAt: current.startedAt ?? scope.startedAt,
99 });
100 }
101 
102 return current;
103};
104 
105export const markOwnershipLost = (
106 scope: RunExecutionScope,
107 state: RunExecutionContextState,
108 status: ProjectRunStatus | null,
109): void => {
110 if (state.ownershipLost) {
111 return;
112 }
113 
114 state.ownershipLost = true;
115 state.ownershipLossStatus = status;
116 logger.warn("run_ownership_lost", {
117 ...scope.logContext,
118 status,
119 phase: state.phase,
120 });
121};