trigger.dev 4.0.0-v4-beta.11 → 4.0.0-v4-beta.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/build/bundle.d.ts +4 -0
- package/dist/esm/build/bundle.js +8 -1
- package/dist/esm/build/bundle.js.map +1 -1
- package/dist/esm/cli/common.d.ts +1 -1
- package/dist/esm/cli/common.js +4 -0
- package/dist/esm/cli/common.js.map +1 -1
- package/dist/esm/commands/deploy.d.ts +1 -1
- package/dist/esm/commands/deploy.js +58 -11
- package/dist/esm/commands/deploy.js.map +1 -1
- package/dist/esm/commands/dev.js +1 -1
- package/dist/esm/commands/dev.js.map +1 -1
- package/dist/esm/commands/init.d.ts +1 -1
- package/dist/esm/commands/list-profiles.d.ts +1 -1
- package/dist/esm/commands/login.d.ts +1 -1
- package/dist/esm/commands/logout.d.ts +1 -1
- package/dist/esm/commands/promote.d.ts +1 -1
- package/dist/esm/commands/switch.d.ts +1 -1
- package/dist/esm/commands/trigger.d.ts +1 -1
- package/dist/esm/commands/whoami.d.ts +1 -1
- package/dist/esm/dev/devOutput.js +30 -1
- package/dist/esm/dev/devOutput.js.map +1 -1
- package/dist/esm/dev/devSession.js +27 -16
- package/dist/esm/dev/devSession.js.map +1 -1
- package/dist/esm/entryPoints/dev-run-worker.js +32 -21
- package/dist/esm/entryPoints/dev-run-worker.js.map +1 -1
- package/dist/esm/entryPoints/managed/controller.d.ts +5 -2
- package/dist/esm/entryPoints/managed/controller.js +43 -74
- package/dist/esm/entryPoints/managed/controller.js.map +1 -1
- package/dist/esm/entryPoints/managed/env.d.ts +10 -15
- package/dist/esm/entryPoints/managed/env.js +14 -12
- package/dist/esm/entryPoints/managed/env.js.map +1 -1
- package/dist/esm/entryPoints/managed/execution.d.ts +52 -16
- package/dist/esm/entryPoints/managed/execution.js +348 -223
- package/dist/esm/entryPoints/managed/execution.js.map +1 -1
- package/dist/esm/entryPoints/managed/logger.d.ts +15 -5
- package/dist/esm/entryPoints/managed/logger.js +19 -6
- package/dist/esm/entryPoints/managed/logger.js.map +1 -1
- package/dist/esm/entryPoints/managed/notifier.d.ts +30 -0
- package/dist/esm/entryPoints/managed/notifier.js +63 -0
- package/dist/esm/entryPoints/managed/notifier.js.map +1 -0
- package/dist/esm/entryPoints/managed/overrides.d.ts +3 -1
- package/dist/esm/entryPoints/managed/overrides.js +5 -3
- package/dist/esm/entryPoints/managed/overrides.js.map +1 -1
- package/dist/esm/entryPoints/managed/poller.d.ts +12 -8
- package/dist/esm/entryPoints/managed/poller.js +34 -29
- package/dist/esm/entryPoints/managed/poller.js.map +1 -1
- package/dist/esm/entryPoints/managed/snapshot.d.ts +54 -0
- package/dist/esm/entryPoints/managed/snapshot.js +293 -0
- package/dist/esm/entryPoints/managed/snapshot.js.map +1 -0
- package/dist/esm/entryPoints/managed-run-worker.js +31 -20
- package/dist/esm/entryPoints/managed-run-worker.js.map +1 -1
- package/dist/esm/executions/taskRunProcess.d.ts +9 -13
- package/dist/esm/executions/taskRunProcess.js +47 -68
- package/dist/esm/executions/taskRunProcess.js.map +1 -1
- package/dist/esm/utilities/cliOutput.d.ts +6 -1
- package/dist/esm/utilities/cliOutput.js +10 -1
- package/dist/esm/utilities/cliOutput.js.map +1 -1
- package/dist/esm/utilities/eventBus.d.ts +1 -0
- package/dist/esm/utilities/eventBus.js.map +1 -1
- package/dist/esm/utilities/supportsHyperlinks.d.ts +15 -0
- package/dist/esm/utilities/supportsHyperlinks.js +122 -0
- package/dist/esm/utilities/supportsHyperlinks.js.map +1 -0
- package/dist/esm/utilities/terminalLink.d.ts +56 -0
- package/dist/esm/utilities/terminalLink.js +76 -0
- package/dist/esm/utilities/terminalLink.js.map +1 -0
- package/dist/esm/version.js +1 -1
- package/package.json +10 -4
|
@@ -5,6 +5,8 @@ import { RunExecutionSnapshotPoller } from "./poller.js";
|
|
|
5
5
|
import { assertExhaustive, tryCatch } from "@trigger.dev/core/utils";
|
|
6
6
|
import { MetadataClient } from "./overrides.js";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
|
+
import { SnapshotManager } from "./snapshot.js";
|
|
9
|
+
import { RunNotifier } from "./notifier.js";
|
|
8
10
|
class ExecutionAbortError extends Error {
|
|
9
11
|
constructor(message) {
|
|
10
12
|
super(message);
|
|
@@ -15,9 +17,9 @@ export class RunExecution {
|
|
|
15
17
|
id;
|
|
16
18
|
executionAbortController;
|
|
17
19
|
_runFriendlyId;
|
|
18
|
-
currentSnapshotId;
|
|
19
20
|
currentAttemptNumber;
|
|
20
21
|
currentTaskRunEnv;
|
|
22
|
+
snapshotManager;
|
|
21
23
|
dequeuedAt;
|
|
22
24
|
podScheduledAt;
|
|
23
25
|
workerManifest;
|
|
@@ -29,20 +31,50 @@ export class RunExecution {
|
|
|
29
31
|
snapshotPoller;
|
|
30
32
|
lastHeartbeat;
|
|
31
33
|
isShuttingDown = false;
|
|
34
|
+
shutdownReason;
|
|
35
|
+
supervisorSocket;
|
|
36
|
+
notifier;
|
|
37
|
+
metadataClient;
|
|
32
38
|
constructor(opts) {
|
|
33
39
|
this.id = randomBytes(4).toString("hex");
|
|
34
40
|
this.workerManifest = opts.workerManifest;
|
|
35
41
|
this.env = opts.env;
|
|
36
42
|
this.httpClient = opts.httpClient;
|
|
37
43
|
this.logger = opts.logger;
|
|
44
|
+
this.supervisorSocket = opts.supervisorSocket;
|
|
38
45
|
this.restoreCount = 0;
|
|
39
46
|
this.executionAbortController = new AbortController();
|
|
47
|
+
if (this.env.TRIGGER_METADATA_URL) {
|
|
48
|
+
this.metadataClient = new MetadataClient(this.env.TRIGGER_METADATA_URL);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Cancels the current execution.
|
|
53
|
+
*/
|
|
54
|
+
async cancel() {
|
|
55
|
+
if (this.isShuttingDown) {
|
|
56
|
+
throw new Error("cancel called after execution shut down");
|
|
57
|
+
}
|
|
58
|
+
this.sendDebugLog("cancelling attempt", { runId: this.runFriendlyId });
|
|
59
|
+
await this.taskRunProcess?.cancel();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Kills the current execution.
|
|
63
|
+
*/
|
|
64
|
+
async kill({ exitExecution = true } = {}) {
|
|
65
|
+
await this.taskRunProcess?.kill("SIGKILL");
|
|
66
|
+
if (exitExecution) {
|
|
67
|
+
this.shutdown("kill");
|
|
68
|
+
}
|
|
40
69
|
}
|
|
41
70
|
/**
|
|
42
71
|
* Prepares the execution with task run environment variables.
|
|
43
72
|
* This should be called before executing, typically after a successful run to prepare for the next one.
|
|
44
73
|
*/
|
|
45
74
|
prepareForExecution(opts) {
|
|
75
|
+
if (this.isShuttingDown) {
|
|
76
|
+
throw new Error("prepareForExecution called after execution shut down");
|
|
77
|
+
}
|
|
46
78
|
if (this.taskRunProcess) {
|
|
47
79
|
throw new Error("prepareForExecution called after process was already created");
|
|
48
80
|
}
|
|
@@ -89,6 +121,12 @@ export class RunExecution {
|
|
|
89
121
|
this.sendDebugLog("onTaskRunHeartbeat: failed", { error: error.message });
|
|
90
122
|
}
|
|
91
123
|
});
|
|
124
|
+
taskRunProcess.onSendDebugLog.attach(async (debugLog) => {
|
|
125
|
+
this.sendRuntimeDebugLog(debugLog.message, debugLog.properties);
|
|
126
|
+
});
|
|
127
|
+
taskRunProcess.onSetSuspendable.attach(async ({ suspendable }) => {
|
|
128
|
+
this.suspendable = suspendable;
|
|
129
|
+
});
|
|
92
130
|
return taskRunProcess;
|
|
93
131
|
}
|
|
94
132
|
/**
|
|
@@ -103,81 +141,50 @@ export class RunExecution {
|
|
|
103
141
|
}
|
|
104
142
|
/**
|
|
105
143
|
* Called by the RunController when it receives a websocket notification
|
|
106
|
-
* or when the snapshot poller detects a change
|
|
144
|
+
* or when the snapshot poller detects a change.
|
|
145
|
+
*
|
|
146
|
+
* This is the main entry point for snapshot changes, but processing is deferred to the snapshot manager.
|
|
107
147
|
*/
|
|
108
|
-
async
|
|
148
|
+
async enqueueSnapshotChangesAndWait(snapshots) {
|
|
109
149
|
if (this.isShuttingDown) {
|
|
110
|
-
this.sendDebugLog("
|
|
111
|
-
return;
|
|
112
|
-
}
|
|
113
|
-
const { run, snapshot, completedWaitpoints } = runData;
|
|
114
|
-
const snapshotMetadata = {
|
|
115
|
-
incomingRunId: run.friendlyId,
|
|
116
|
-
incomingSnapshotId: snapshot.friendlyId,
|
|
117
|
-
completedWaitpoints: completedWaitpoints.length,
|
|
118
|
-
};
|
|
119
|
-
// Ensure we have run details
|
|
120
|
-
if (!this.runFriendlyId || !this.currentSnapshotId) {
|
|
121
|
-
this.sendDebugLog("handleSnapshotChange: missing run or snapshot ID", snapshotMetadata, run.friendlyId);
|
|
150
|
+
this.sendDebugLog("enqueueSnapshotChangeAndWait: shutting down, skipping");
|
|
122
151
|
return;
|
|
123
152
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// Send debug log to both runs
|
|
127
|
-
this.sendDebugLog("handleSnapshotChange: mismatched run IDs", snapshotMetadata);
|
|
128
|
-
this.sendDebugLog("handleSnapshotChange: mismatched run IDs", snapshotMetadata, run.friendlyId);
|
|
153
|
+
if (!this.snapshotManager) {
|
|
154
|
+
this.sendDebugLog("enqueueSnapshotChangeAndWait: missing snapshot manager");
|
|
129
155
|
return;
|
|
130
156
|
}
|
|
131
|
-
this.
|
|
132
|
-
await this.processSnapshotChangeQueue();
|
|
157
|
+
await this.snapshotManager.handleSnapshotChanges(snapshots);
|
|
133
158
|
}
|
|
134
|
-
|
|
135
|
-
snapshotChangeQueueLock = false;
|
|
136
|
-
async processSnapshotChangeQueue() {
|
|
137
|
-
if (this.snapshotChangeQueueLock) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
this.snapshotChangeQueueLock = true;
|
|
141
|
-
while (this.snapshotChangeQueue.length > 0) {
|
|
142
|
-
const runData = this.snapshotChangeQueue.shift();
|
|
143
|
-
if (!runData) {
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
const [error] = await tryCatch(this.processSnapshotChange(runData));
|
|
147
|
-
if (error) {
|
|
148
|
-
this.sendDebugLog("Failed to process snapshot change", { error: error.message });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
this.snapshotChangeQueueLock = false;
|
|
152
|
-
}
|
|
153
|
-
async processSnapshotChange(runData) {
|
|
159
|
+
async processSnapshotChange(runData, deprecated) {
|
|
154
160
|
const { run, snapshot, completedWaitpoints } = runData;
|
|
155
161
|
const snapshotMetadata = {
|
|
156
162
|
incomingSnapshotId: snapshot.friendlyId,
|
|
157
163
|
completedWaitpoints: completedWaitpoints.length,
|
|
158
164
|
};
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
this.sendDebugLog("handleSnapshotChange: received older snapshot, skipping", snapshotMetadata);
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
if (snapshot.friendlyId === this.currentSnapshotId) {
|
|
165
|
+
if (!this.snapshotManager) {
|
|
166
|
+
this.sendDebugLog("handleSnapshotChange: missing snapshot manager", snapshotMetadata);
|
|
165
167
|
return;
|
|
166
168
|
}
|
|
167
169
|
if (this.currentAttemptNumber && this.currentAttemptNumber !== run.attemptNumber) {
|
|
168
|
-
this.sendDebugLog("
|
|
169
|
-
|
|
170
|
+
this.sendDebugLog("error: attempt number mismatch", snapshotMetadata);
|
|
171
|
+
// This is a rogue execution, a new one will already have been created elsewhere
|
|
172
|
+
await this.exitTaskRunProcessWithoutFailingRun({ flush: false });
|
|
170
173
|
return;
|
|
171
174
|
}
|
|
172
|
-
|
|
175
|
+
// DO NOT REMOVE (very noisy, but helpful for debugging)
|
|
176
|
+
// this.sendDebugLog(`processing snapshot change: ${snapshot.executionStatus}`, snapshotMetadata);
|
|
173
177
|
// Reset the snapshot poll interval so we don't do unnecessary work
|
|
174
|
-
this.snapshotPoller?.resetCurrentInterval();
|
|
175
|
-
// Update internal state
|
|
176
|
-
this.currentSnapshotId = snapshot.friendlyId;
|
|
177
|
-
// Update services
|
|
178
178
|
this.snapshotPoller?.updateSnapshotId(snapshot.friendlyId);
|
|
179
|
+
this.snapshotPoller?.resetCurrentInterval();
|
|
180
|
+
if (deprecated) {
|
|
181
|
+
this.sendDebugLog("run execution is deprecated", { incomingSnapshot: snapshot });
|
|
182
|
+
await this.exitTaskRunProcessWithoutFailingRun({ flush: false });
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
179
185
|
switch (snapshot.executionStatus) {
|
|
180
186
|
case "PENDING_CANCEL": {
|
|
187
|
+
this.sendDebugLog("run was cancelled", snapshotMetadata);
|
|
181
188
|
const [error] = await tryCatch(this.cancel());
|
|
182
189
|
if (error) {
|
|
183
190
|
this.sendDebugLog("snapshot change: failed to cancel attempt", {
|
|
@@ -189,83 +196,37 @@ export class RunExecution {
|
|
|
189
196
|
return;
|
|
190
197
|
}
|
|
191
198
|
case "QUEUED": {
|
|
192
|
-
this.sendDebugLog("
|
|
193
|
-
|
|
194
|
-
await this.taskRunProcess?.suspend();
|
|
199
|
+
this.sendDebugLog("run was re-queued", snapshotMetadata);
|
|
200
|
+
await this.exitTaskRunProcessWithoutFailingRun({ flush: true });
|
|
195
201
|
return;
|
|
196
202
|
}
|
|
197
203
|
case "FINISHED": {
|
|
198
|
-
this.sendDebugLog("
|
|
199
|
-
|
|
200
|
-
await this.taskRunProcess?.suspend();
|
|
204
|
+
this.sendDebugLog("run is finished", snapshotMetadata);
|
|
205
|
+
await this.exitTaskRunProcessWithoutFailingRun({ flush: true });
|
|
201
206
|
return;
|
|
202
207
|
}
|
|
203
208
|
case "QUEUED_EXECUTING":
|
|
204
209
|
case "EXECUTING_WITH_WAITPOINTS": {
|
|
205
|
-
this.sendDebugLog("
|
|
206
|
-
|
|
207
|
-
if (error) {
|
|
208
|
-
this.sendDebugLog("Failed to cleanup task run process, carrying on", {
|
|
209
|
-
...snapshotMetadata,
|
|
210
|
-
error: error.message,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
if (snapshot.friendlyId !== this.currentSnapshotId) {
|
|
214
|
-
this.sendDebugLog("Snapshot changed after cleanup, abort", snapshotMetadata);
|
|
215
|
-
this.abortExecution();
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
await sleep(this.env.TRIGGER_PRE_SUSPEND_WAIT_MS);
|
|
219
|
-
if (snapshot.friendlyId !== this.currentSnapshotId) {
|
|
220
|
-
this.sendDebugLog("Snapshot changed after suspend threshold, abort", snapshotMetadata);
|
|
221
|
-
this.abortExecution();
|
|
222
|
-
return;
|
|
223
|
-
}
|
|
224
|
-
if (!this.runFriendlyId || !this.currentSnapshotId) {
|
|
225
|
-
this.sendDebugLog("handleSnapshotChange: Missing run ID or snapshot ID after suspension, abort", snapshotMetadata);
|
|
226
|
-
this.abortExecution();
|
|
227
|
-
return;
|
|
228
|
-
}
|
|
229
|
-
const suspendResult = await this.httpClient.suspendRun(this.runFriendlyId, this.currentSnapshotId);
|
|
230
|
-
if (!suspendResult.success) {
|
|
231
|
-
this.sendDebugLog("Failed to suspend run, staying alive 🎶", {
|
|
232
|
-
...snapshotMetadata,
|
|
233
|
-
error: suspendResult.error,
|
|
234
|
-
});
|
|
235
|
-
this.sendDebugLog("checkpoint: suspend request failed", {
|
|
236
|
-
...snapshotMetadata,
|
|
237
|
-
error: suspendResult.error,
|
|
238
|
-
});
|
|
239
|
-
// This is fine, we'll wait for the next status change
|
|
240
|
-
return;
|
|
241
|
-
}
|
|
242
|
-
if (!suspendResult.data.ok) {
|
|
243
|
-
this.sendDebugLog("checkpoint: failed to suspend run", {
|
|
244
|
-
snapshotId: this.currentSnapshotId,
|
|
245
|
-
error: suspendResult.data.error,
|
|
246
|
-
});
|
|
247
|
-
// This is fine, we'll wait for the next status change
|
|
248
|
-
return;
|
|
249
|
-
}
|
|
250
|
-
this.sendDebugLog("Suspending, any day now 🚬", snapshotMetadata);
|
|
251
|
-
// Wait for next status change
|
|
210
|
+
this.sendDebugLog("run is executing with waitpoints", snapshotMetadata);
|
|
211
|
+
// Wait for next status change - suspension is handled by the snapshot manager
|
|
252
212
|
return;
|
|
253
213
|
}
|
|
254
214
|
case "SUSPENDED": {
|
|
255
|
-
this.sendDebugLog("
|
|
215
|
+
this.sendDebugLog("run was suspended", snapshotMetadata);
|
|
256
216
|
// This will kill the process and fail the execution with a SuspendedProcessError
|
|
257
|
-
|
|
217
|
+
// We don't flush because we already did before suspending
|
|
218
|
+
await this.exitTaskRunProcessWithoutFailingRun({ flush: false });
|
|
258
219
|
return;
|
|
259
220
|
}
|
|
260
221
|
case "PENDING_EXECUTING": {
|
|
261
|
-
this.sendDebugLog("
|
|
222
|
+
this.sendDebugLog("run is pending execution", snapshotMetadata);
|
|
262
223
|
if (completedWaitpoints.length === 0) {
|
|
263
|
-
this.sendDebugLog("
|
|
224
|
+
this.sendDebugLog("no waitpoints to complete, nothing to do", snapshotMetadata);
|
|
264
225
|
return;
|
|
265
226
|
}
|
|
266
227
|
const [error] = await tryCatch(this.restore());
|
|
267
228
|
if (error) {
|
|
268
|
-
this.sendDebugLog("
|
|
229
|
+
this.sendDebugLog("failed to restore execution", {
|
|
269
230
|
...snapshotMetadata,
|
|
270
231
|
error: error.message,
|
|
271
232
|
});
|
|
@@ -275,13 +236,13 @@ export class RunExecution {
|
|
|
275
236
|
return;
|
|
276
237
|
}
|
|
277
238
|
case "EXECUTING": {
|
|
278
|
-
this.sendDebugLog("Run is now executing", snapshotMetadata);
|
|
279
239
|
if (completedWaitpoints.length === 0) {
|
|
240
|
+
this.sendDebugLog("run is executing without completed waitpoints", snapshotMetadata);
|
|
280
241
|
return;
|
|
281
242
|
}
|
|
282
|
-
this.sendDebugLog("
|
|
243
|
+
this.sendDebugLog("run is executing with completed waitpoints", snapshotMetadata);
|
|
283
244
|
if (!this.taskRunProcess) {
|
|
284
|
-
this.sendDebugLog("
|
|
245
|
+
this.sendDebugLog("no task run process, ignoring completed waitpoints", snapshotMetadata);
|
|
285
246
|
this.abortExecution();
|
|
286
247
|
return;
|
|
287
248
|
}
|
|
@@ -291,7 +252,7 @@ export class RunExecution {
|
|
|
291
252
|
return;
|
|
292
253
|
}
|
|
293
254
|
case "RUN_CREATED": {
|
|
294
|
-
this.sendDebugLog("
|
|
255
|
+
this.sendDebugLog("aborting execution: invalid status change: RUN_CREATED", snapshotMetadata);
|
|
295
256
|
this.abortExecution();
|
|
296
257
|
return;
|
|
297
258
|
}
|
|
@@ -301,16 +262,16 @@ export class RunExecution {
|
|
|
301
262
|
}
|
|
302
263
|
}
|
|
303
264
|
async startAttempt({ isWarmStart, }) {
|
|
304
|
-
if (!this.runFriendlyId || !this.
|
|
305
|
-
throw new Error("Cannot start attempt: missing run or snapshot
|
|
265
|
+
if (!this.runFriendlyId || !this.snapshotManager) {
|
|
266
|
+
throw new Error("Cannot start attempt: missing run or snapshot manager");
|
|
306
267
|
}
|
|
307
|
-
this.sendDebugLog("
|
|
268
|
+
this.sendDebugLog("starting attempt");
|
|
308
269
|
const attemptStartedAt = Date.now();
|
|
309
270
|
// Check for abort before each major async operation
|
|
310
271
|
if (this.executionAbortController.signal.aborted) {
|
|
311
272
|
throw new ExecutionAbortError("Execution aborted before start");
|
|
312
273
|
}
|
|
313
|
-
const start = await this.httpClient.startRunAttempt(this.runFriendlyId, this.
|
|
274
|
+
const start = await this.httpClient.startRunAttempt(this.runFriendlyId, this.snapshotManager.snapshotId, { isWarmStart });
|
|
314
275
|
if (this.executionAbortController.signal.aborted) {
|
|
315
276
|
throw new ExecutionAbortError("Execution aborted after start");
|
|
316
277
|
}
|
|
@@ -318,14 +279,14 @@ export class RunExecution {
|
|
|
318
279
|
throw new Error(`Start API call failed: ${start.error}`);
|
|
319
280
|
}
|
|
320
281
|
// A snapshot was just created, so update the snapshot ID
|
|
321
|
-
this.
|
|
282
|
+
this.snapshotManager.updateSnapshot(start.data.snapshot.friendlyId, start.data.snapshot.executionStatus);
|
|
322
283
|
// Also set or update the attempt number - we do this to detect illegal attempt number changes, e.g. from stalled runners coming back online
|
|
323
284
|
const attemptNumber = start.data.run.attemptNumber;
|
|
324
285
|
if (attemptNumber && attemptNumber > 0) {
|
|
325
286
|
this.currentAttemptNumber = attemptNumber;
|
|
326
287
|
}
|
|
327
288
|
else {
|
|
328
|
-
this.sendDebugLog("
|
|
289
|
+
this.sendDebugLog("error: invalid attempt number returned from start attempt", {
|
|
329
290
|
attemptNumber: String(attemptNumber),
|
|
330
291
|
});
|
|
331
292
|
}
|
|
@@ -334,7 +295,7 @@ export class RunExecution {
|
|
|
334
295
|
dequeuedAt: this.dequeuedAt?.getTime(),
|
|
335
296
|
podScheduledAt: this.podScheduledAt?.getTime(),
|
|
336
297
|
});
|
|
337
|
-
this.sendDebugLog("
|
|
298
|
+
this.sendDebugLog("started attempt");
|
|
338
299
|
return { ...start.data, metrics };
|
|
339
300
|
}
|
|
340
301
|
/**
|
|
@@ -342,34 +303,53 @@ export class RunExecution {
|
|
|
342
303
|
* When this returns, the child process will have been cleaned up.
|
|
343
304
|
*/
|
|
344
305
|
async execute(runOpts) {
|
|
306
|
+
if (this.isShuttingDown) {
|
|
307
|
+
throw new Error("execute called after execution shut down");
|
|
308
|
+
}
|
|
345
309
|
// Setup initial state
|
|
346
310
|
this.runFriendlyId = runOpts.runFriendlyId;
|
|
347
|
-
|
|
311
|
+
// Create snapshot manager
|
|
312
|
+
this.snapshotManager = new SnapshotManager({
|
|
313
|
+
runFriendlyId: runOpts.runFriendlyId,
|
|
314
|
+
runnerId: this.env.TRIGGER_RUNNER_ID,
|
|
315
|
+
initialSnapshotId: runOpts.snapshotFriendlyId,
|
|
316
|
+
// We're just guessing here, but "PENDING_EXECUTING" is probably fine
|
|
317
|
+
initialStatus: "PENDING_EXECUTING",
|
|
318
|
+
logger: this.logger,
|
|
319
|
+
metadataClient: this.metadataClient,
|
|
320
|
+
onSnapshotChange: this.processSnapshotChange.bind(this),
|
|
321
|
+
onSuspendable: this.handleSuspendable.bind(this),
|
|
322
|
+
});
|
|
348
323
|
this.dequeuedAt = runOpts.dequeuedAt;
|
|
349
324
|
this.podScheduledAt = runOpts.podScheduledAt;
|
|
350
325
|
// Create and start services
|
|
351
326
|
this.snapshotPoller = new RunExecutionSnapshotPoller({
|
|
352
327
|
runFriendlyId: this.runFriendlyId,
|
|
353
|
-
snapshotFriendlyId: this.
|
|
354
|
-
httpClient: this.httpClient,
|
|
328
|
+
snapshotFriendlyId: this.snapshotManager.snapshotId,
|
|
355
329
|
logger: this.logger,
|
|
356
330
|
snapshotPollIntervalSeconds: this.env.TRIGGER_SNAPSHOT_POLL_INTERVAL_SECONDS,
|
|
357
|
-
|
|
358
|
-
});
|
|
359
|
-
this.
|
|
331
|
+
onPoll: this.fetchAndProcessSnapshotChanges.bind(this),
|
|
332
|
+
}).start();
|
|
333
|
+
this.notifier = new RunNotifier({
|
|
334
|
+
runFriendlyId: this.runFriendlyId,
|
|
335
|
+
supervisorSocket: this.supervisorSocket,
|
|
336
|
+
onNotify: this.fetchAndProcessSnapshotChanges.bind(this),
|
|
337
|
+
logger: this.logger,
|
|
338
|
+
}).start();
|
|
360
339
|
const [startError, start] = await tryCatch(this.startAttempt({ isWarmStart: runOpts.isWarmStart }));
|
|
361
340
|
if (startError) {
|
|
362
|
-
this.sendDebugLog("
|
|
363
|
-
this.
|
|
341
|
+
this.sendDebugLog("failed to start attempt", { error: startError.message });
|
|
342
|
+
this.shutdown("failed to start attempt");
|
|
364
343
|
return;
|
|
365
344
|
}
|
|
366
345
|
const [executeError] = await tryCatch(this.executeRunWrapper(start));
|
|
367
346
|
if (executeError) {
|
|
368
|
-
this.sendDebugLog("
|
|
369
|
-
this.
|
|
347
|
+
this.sendDebugLog("failed to execute run", { error: executeError.message });
|
|
348
|
+
this.shutdown("failed to execute run");
|
|
370
349
|
return;
|
|
371
350
|
}
|
|
372
|
-
|
|
351
|
+
// This is here for safety, but it
|
|
352
|
+
this.shutdown("execute call finished");
|
|
373
353
|
}
|
|
374
354
|
async executeRunWrapper({ run, snapshot, envVars, execution, metrics, isWarmStart, }) {
|
|
375
355
|
this.currentTaskRunEnv = envVars;
|
|
@@ -381,13 +361,11 @@ export class RunExecution {
|
|
|
381
361
|
metrics,
|
|
382
362
|
isWarmStart,
|
|
383
363
|
}));
|
|
384
|
-
this.sendDebugLog("Run execution completed", { error: executeError?.message });
|
|
385
364
|
if (!executeError) {
|
|
386
|
-
this.stopServices();
|
|
387
365
|
return;
|
|
388
366
|
}
|
|
389
367
|
if (executeError instanceof SuspendedProcessError) {
|
|
390
|
-
this.sendDebugLog("
|
|
368
|
+
this.sendDebugLog("execution was suspended", {
|
|
391
369
|
run: run.friendlyId,
|
|
392
370
|
snapshot: snapshot.friendlyId,
|
|
393
371
|
error: executeError.message,
|
|
@@ -395,14 +373,14 @@ export class RunExecution {
|
|
|
395
373
|
return;
|
|
396
374
|
}
|
|
397
375
|
if (executeError instanceof ExecutionAbortError) {
|
|
398
|
-
this.sendDebugLog("
|
|
376
|
+
this.sendDebugLog("execution was aborted", {
|
|
399
377
|
run: run.friendlyId,
|
|
400
378
|
snapshot: snapshot.friendlyId,
|
|
401
379
|
error: executeError.message,
|
|
402
380
|
});
|
|
403
381
|
return;
|
|
404
382
|
}
|
|
405
|
-
this.sendDebugLog("
|
|
383
|
+
this.sendDebugLog("error while executing attempt", {
|
|
406
384
|
error: executeError.message,
|
|
407
385
|
runId: run.friendlyId,
|
|
408
386
|
snapshotId: snapshot.friendlyId,
|
|
@@ -415,9 +393,8 @@ export class RunExecution {
|
|
|
415
393
|
};
|
|
416
394
|
const [completeError] = await tryCatch(this.complete({ completion }));
|
|
417
395
|
if (completeError) {
|
|
418
|
-
this.sendDebugLog("
|
|
396
|
+
this.sendDebugLog("failed to complete run", { error: completeError.message });
|
|
419
397
|
}
|
|
420
|
-
this.stopServices();
|
|
421
398
|
}
|
|
422
399
|
async executeRun({ run, snapshot, envVars, execution, metrics, isWarmStart, }) {
|
|
423
400
|
// For immediate retries, we need to ensure the task run process is prepared for the next attempt
|
|
@@ -425,7 +402,7 @@ export class RunExecution {
|
|
|
425
402
|
this.taskRunProcess &&
|
|
426
403
|
!this.taskRunProcess.isPreparedForNextAttempt) {
|
|
427
404
|
this.sendDebugLog("killing existing task run process before executing next attempt");
|
|
428
|
-
await this.kill().catch(() => { });
|
|
405
|
+
await this.kill({ exitExecution: false }).catch(() => { });
|
|
429
406
|
}
|
|
430
407
|
// To skip this step and eagerly create the task run process, run prepareForExecution first
|
|
431
408
|
if (!this.taskRunProcess || !this.taskRunProcess.isPreparedForNextRun) {
|
|
@@ -434,7 +411,7 @@ export class RunExecution {
|
|
|
434
411
|
this.sendDebugLog("executing task run process", { runId: execution.run.id });
|
|
435
412
|
// Set up an abort handler that will cleanup the task run process
|
|
436
413
|
this.executionAbortController.signal.addEventListener("abort", async () => {
|
|
437
|
-
this.sendDebugLog("
|
|
414
|
+
this.sendDebugLog("execution aborted during task run, cleaning up process", {
|
|
438
415
|
runId: execution.run.id,
|
|
439
416
|
});
|
|
440
417
|
await this.taskRunProcess?.cleanup(true);
|
|
@@ -449,39 +426,24 @@ export class RunExecution {
|
|
|
449
426
|
env: envVars,
|
|
450
427
|
}, isWarmStart);
|
|
451
428
|
// If we get here, the task completed normally
|
|
452
|
-
this.sendDebugLog("
|
|
429
|
+
this.sendDebugLog("completed run attempt", { attemptSuccess: completion.ok });
|
|
453
430
|
// The execution has finished, so we can cleanup the task run process. Killing it should be safe.
|
|
454
431
|
const [error] = await tryCatch(this.taskRunProcess.cleanup(true));
|
|
455
432
|
if (error) {
|
|
456
|
-
this.sendDebugLog("
|
|
433
|
+
this.sendDebugLog("failed to cleanup task run process, submitting completion anyway", {
|
|
457
434
|
error: error.message,
|
|
458
435
|
});
|
|
459
436
|
}
|
|
460
437
|
const [completionError] = await tryCatch(this.complete({ completion }));
|
|
461
438
|
if (completionError) {
|
|
462
|
-
this.sendDebugLog("
|
|
463
|
-
}
|
|
464
|
-
}
|
|
465
|
-
/**
|
|
466
|
-
* Cancels the current execution.
|
|
467
|
-
*/
|
|
468
|
-
async cancel() {
|
|
469
|
-
this.sendDebugLog("cancelling attempt", { runId: this.runFriendlyId });
|
|
470
|
-
await this.taskRunProcess?.cancel();
|
|
471
|
-
}
|
|
472
|
-
exit() {
|
|
473
|
-
if (this.taskRunProcess?.isPreparedForNextRun) {
|
|
474
|
-
this.taskRunProcess?.forceExit();
|
|
439
|
+
this.sendDebugLog("failed to complete run", { error: completionError.message });
|
|
475
440
|
}
|
|
476
441
|
}
|
|
477
|
-
async kill() {
|
|
478
|
-
await this.taskRunProcess?.kill("SIGKILL");
|
|
479
|
-
}
|
|
480
442
|
async complete({ completion }) {
|
|
481
|
-
if (!this.runFriendlyId || !this.
|
|
482
|
-
throw new Error("
|
|
443
|
+
if (!this.runFriendlyId || !this.snapshotManager) {
|
|
444
|
+
throw new Error("cannot complete run: missing run or snapshot manager");
|
|
483
445
|
}
|
|
484
|
-
const completionResult = await this.httpClient.completeRunAttempt(this.runFriendlyId, this.
|
|
446
|
+
const completionResult = await this.httpClient.completeRunAttempt(this.runFriendlyId, this.snapshotManager.snapshotId, { completion });
|
|
485
447
|
if (!completionResult.success) {
|
|
486
448
|
throw new Error(`failed to submit completion: ${completionResult.error}`);
|
|
487
449
|
}
|
|
@@ -491,39 +453,57 @@ export class RunExecution {
|
|
|
491
453
|
});
|
|
492
454
|
}
|
|
493
455
|
async handleCompletionResult({ completion, result, }) {
|
|
494
|
-
this.sendDebugLog(
|
|
456
|
+
this.sendDebugLog(`completion result: ${result.attemptStatus}`, {
|
|
495
457
|
attemptSuccess: completion.ok,
|
|
496
458
|
attemptStatus: result.attemptStatus,
|
|
497
459
|
snapshotId: result.snapshot.friendlyId,
|
|
498
460
|
runId: result.run.friendlyId,
|
|
499
461
|
});
|
|
500
|
-
|
|
501
|
-
//
|
|
502
|
-
this.
|
|
462
|
+
const snapshotStatus = this.convertAttemptStatusToSnapshotStatus(result.attemptStatus);
|
|
463
|
+
// Update our snapshot ID to match the completion result to ensure any subsequent API calls use the correct snapshot
|
|
464
|
+
this.updateSnapshotAfterCompletion(result.snapshot.friendlyId, snapshotStatus);
|
|
503
465
|
const { attemptStatus } = result;
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
this.sendDebugLog("Run pending cancel");
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
if (attemptStatus === "RETRY_QUEUED") {
|
|
513
|
-
this.sendDebugLog("Retry queued");
|
|
514
|
-
return;
|
|
515
|
-
}
|
|
516
|
-
if (attemptStatus === "RETRY_IMMEDIATELY") {
|
|
517
|
-
if (completion.ok) {
|
|
518
|
-
throw new Error("Should retry but completion OK.");
|
|
466
|
+
switch (attemptStatus) {
|
|
467
|
+
case "RUN_FINISHED":
|
|
468
|
+
case "RUN_PENDING_CANCEL":
|
|
469
|
+
case "RETRY_QUEUED": {
|
|
470
|
+
return;
|
|
519
471
|
}
|
|
520
|
-
|
|
521
|
-
|
|
472
|
+
case "RETRY_IMMEDIATELY": {
|
|
473
|
+
if (attemptStatus !== "RETRY_IMMEDIATELY") {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (completion.ok) {
|
|
477
|
+
throw new Error("Should retry but completion OK.");
|
|
478
|
+
}
|
|
479
|
+
if (!completion.retry) {
|
|
480
|
+
throw new Error("Should retry but missing retry params.");
|
|
481
|
+
}
|
|
482
|
+
await this.retryImmediately({ retryOpts: completion.retry });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
default: {
|
|
486
|
+
assertExhaustive(attemptStatus);
|
|
522
487
|
}
|
|
523
|
-
await this.retryImmediately({ retryOpts: completion.retry });
|
|
524
|
-
return;
|
|
525
488
|
}
|
|
526
|
-
|
|
489
|
+
}
|
|
490
|
+
updateSnapshotAfterCompletion(snapshotId, status) {
|
|
491
|
+
this.snapshotManager?.updateSnapshot(snapshotId, status);
|
|
492
|
+
this.snapshotPoller?.updateSnapshotId(snapshotId);
|
|
493
|
+
}
|
|
494
|
+
convertAttemptStatusToSnapshotStatus(attemptStatus) {
|
|
495
|
+
switch (attemptStatus) {
|
|
496
|
+
case "RUN_FINISHED":
|
|
497
|
+
return "FINISHED";
|
|
498
|
+
case "RUN_PENDING_CANCEL":
|
|
499
|
+
return "PENDING_CANCEL";
|
|
500
|
+
case "RETRY_QUEUED":
|
|
501
|
+
return "QUEUED";
|
|
502
|
+
case "RETRY_IMMEDIATELY":
|
|
503
|
+
return "EXECUTING";
|
|
504
|
+
default:
|
|
505
|
+
assertExhaustive(attemptStatus);
|
|
506
|
+
}
|
|
527
507
|
}
|
|
528
508
|
measureExecutionMetrics({ attemptCreatedAt, dequeuedAt, podScheduledAt, }) {
|
|
529
509
|
const metrics = [
|
|
@@ -553,7 +533,7 @@ export class RunExecution {
|
|
|
553
533
|
return metrics;
|
|
554
534
|
}
|
|
555
535
|
async retryImmediately({ retryOpts }) {
|
|
556
|
-
this.sendDebugLog("
|
|
536
|
+
this.sendDebugLog("retrying run immediately", {
|
|
557
537
|
timestamp: retryOpts.timestamp,
|
|
558
538
|
delay: retryOpts.delay,
|
|
559
539
|
});
|
|
@@ -565,52 +545,68 @@ export class RunExecution {
|
|
|
565
545
|
// Start and execute next attempt
|
|
566
546
|
const [startError, start] = await tryCatch(this.startAttempt({ isWarmStart: true }));
|
|
567
547
|
if (startError) {
|
|
568
|
-
this.sendDebugLog("
|
|
569
|
-
this.
|
|
548
|
+
this.sendDebugLog("failed to start attempt for retry", { error: startError.message });
|
|
549
|
+
this.shutdown("retryImmediately: failed to start attempt");
|
|
570
550
|
return;
|
|
571
551
|
}
|
|
572
552
|
const [executeError] = await tryCatch(this.executeRunWrapper({ ...start, isWarmStart: true }));
|
|
573
553
|
if (executeError) {
|
|
574
|
-
this.sendDebugLog("
|
|
575
|
-
this.
|
|
554
|
+
this.sendDebugLog("failed to execute run for retry", { error: executeError.message });
|
|
555
|
+
this.shutdown("retryImmediately: failed to execute run");
|
|
576
556
|
return;
|
|
577
557
|
}
|
|
578
|
-
this.stopServices();
|
|
579
558
|
}
|
|
580
559
|
/**
|
|
581
560
|
* Restores a suspended execution from PENDING_EXECUTING
|
|
582
561
|
*/
|
|
583
562
|
async restore() {
|
|
584
|
-
this.sendDebugLog("
|
|
585
|
-
if (!this.runFriendlyId || !this.
|
|
586
|
-
throw new Error("Cannot restore: missing run or snapshot
|
|
563
|
+
this.sendDebugLog("restoring execution");
|
|
564
|
+
if (!this.runFriendlyId || !this.snapshotManager) {
|
|
565
|
+
throw new Error("Cannot restore: missing run or snapshot manager");
|
|
587
566
|
}
|
|
588
567
|
// Short delay to give websocket time to reconnect
|
|
589
568
|
await sleep(100);
|
|
590
569
|
// Process any env overrides
|
|
591
|
-
await this.processEnvOverrides();
|
|
592
|
-
const continuationResult = await this.httpClient.continueRunExecution(this.runFriendlyId, this.
|
|
570
|
+
await this.processEnvOverrides("restore");
|
|
571
|
+
const continuationResult = await this.httpClient.continueRunExecution(this.runFriendlyId, this.snapshotManager.snapshotId);
|
|
593
572
|
if (!continuationResult.success) {
|
|
594
573
|
throw new Error(continuationResult.error);
|
|
595
574
|
}
|
|
596
575
|
// Track restore count
|
|
597
576
|
this.restoreCount++;
|
|
598
577
|
}
|
|
578
|
+
async exitTaskRunProcessWithoutFailingRun({ flush }) {
|
|
579
|
+
await this.taskRunProcess?.suspend({ flush });
|
|
580
|
+
// No services should be left running after this line - let's make sure of it
|
|
581
|
+
this.shutdown("exitTaskRunProcessWithoutFailingRun");
|
|
582
|
+
}
|
|
599
583
|
/**
|
|
600
584
|
* Processes env overrides from the metadata service. Generally called when we're resuming from a suspended state.
|
|
601
585
|
*/
|
|
602
|
-
async processEnvOverrides() {
|
|
603
|
-
if (!this.
|
|
604
|
-
|
|
605
|
-
return;
|
|
586
|
+
async processEnvOverrides(reason) {
|
|
587
|
+
if (!this.metadataClient) {
|
|
588
|
+
return null;
|
|
606
589
|
}
|
|
607
|
-
const
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
590
|
+
const [error, overrides] = await this.metadataClient.getEnvOverrides();
|
|
591
|
+
if (error) {
|
|
592
|
+
this.sendDebugLog("[override] failed to fetch", {
|
|
593
|
+
reason,
|
|
594
|
+
error: error.message,
|
|
595
|
+
});
|
|
596
|
+
return null;
|
|
612
597
|
}
|
|
613
|
-
|
|
598
|
+
if (overrides.TRIGGER_RUN_ID && overrides.TRIGGER_RUN_ID !== this.runFriendlyId) {
|
|
599
|
+
this.sendDebugLog("[override] run ID mismatch, ignoring overrides", {
|
|
600
|
+
reason,
|
|
601
|
+
currentRunId: this.runFriendlyId,
|
|
602
|
+
incomingRunId: overrides.TRIGGER_RUN_ID,
|
|
603
|
+
});
|
|
604
|
+
return null;
|
|
605
|
+
}
|
|
606
|
+
this.sendDebugLog(`[override] processing: ${reason}`, {
|
|
607
|
+
overrides,
|
|
608
|
+
currentEnv: this.env.raw,
|
|
609
|
+
});
|
|
614
610
|
// Override the env with the new values
|
|
615
611
|
this.env.override(overrides);
|
|
616
612
|
// Update services with new values
|
|
@@ -625,20 +621,23 @@ export class RunExecution {
|
|
|
625
621
|
if (overrides.TRIGGER_RUNNER_ID) {
|
|
626
622
|
this.httpClient.updateRunnerId(this.env.TRIGGER_RUNNER_ID);
|
|
627
623
|
}
|
|
624
|
+
return {
|
|
625
|
+
overrides,
|
|
626
|
+
};
|
|
628
627
|
}
|
|
629
628
|
async onHeartbeat() {
|
|
630
629
|
if (!this.runFriendlyId) {
|
|
631
|
-
this.sendDebugLog("
|
|
630
|
+
this.sendDebugLog("heartbeat: missing run ID");
|
|
632
631
|
return;
|
|
633
632
|
}
|
|
634
|
-
if (!this.
|
|
635
|
-
this.sendDebugLog("
|
|
633
|
+
if (!this.snapshotManager) {
|
|
634
|
+
this.sendDebugLog("heartbeat: missing snapshot manager");
|
|
636
635
|
return;
|
|
637
636
|
}
|
|
638
|
-
this.sendDebugLog("
|
|
639
|
-
const response = await this.httpClient.heartbeatRun(this.runFriendlyId, this.
|
|
637
|
+
this.sendDebugLog("heartbeat");
|
|
638
|
+
const response = await this.httpClient.heartbeatRun(this.runFriendlyId, this.snapshotManager.snapshotId);
|
|
640
639
|
if (!response.success) {
|
|
641
|
-
this.sendDebugLog("
|
|
640
|
+
this.sendDebugLog("heartbeat: failed", { error: response.error });
|
|
642
641
|
}
|
|
643
642
|
this.lastHeartbeat = new Date();
|
|
644
643
|
}
|
|
@@ -649,13 +648,33 @@ export class RunExecution {
|
|
|
649
648
|
properties: {
|
|
650
649
|
...properties,
|
|
651
650
|
runId: this.runFriendlyId,
|
|
652
|
-
snapshotId: this.
|
|
651
|
+
snapshotId: this.currentSnapshotFriendlyId,
|
|
652
|
+
executionId: this.id,
|
|
653
|
+
executionRestoreCount: this.restoreCount,
|
|
654
|
+
lastHeartbeat: this.lastHeartbeat?.toISOString(),
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
}
|
|
658
|
+
sendRuntimeDebugLog(message, properties, runIdOverride) {
|
|
659
|
+
this.logger.sendDebugLog({
|
|
660
|
+
runId: runIdOverride ?? this.runFriendlyId,
|
|
661
|
+
message: `[runtime] ${message}`,
|
|
662
|
+
print: false,
|
|
663
|
+
properties: {
|
|
664
|
+
...properties,
|
|
665
|
+
runId: this.runFriendlyId,
|
|
666
|
+
snapshotId: this.currentSnapshotFriendlyId,
|
|
653
667
|
executionId: this.id,
|
|
654
668
|
executionRestoreCount: this.restoreCount,
|
|
655
669
|
lastHeartbeat: this.lastHeartbeat?.toISOString(),
|
|
656
670
|
},
|
|
657
671
|
});
|
|
658
672
|
}
|
|
673
|
+
set suspendable(suspendable) {
|
|
674
|
+
this.snapshotManager?.setSuspendable(suspendable).catch((error) => {
|
|
675
|
+
this.sendDebugLog("failed to set suspendable", { error: error.message });
|
|
676
|
+
});
|
|
677
|
+
}
|
|
659
678
|
// Ensure we can only set this once
|
|
660
679
|
set runFriendlyId(id) {
|
|
661
680
|
if (this._runFriendlyId) {
|
|
@@ -667,14 +686,19 @@ export class RunExecution {
|
|
|
667
686
|
return this._runFriendlyId;
|
|
668
687
|
}
|
|
669
688
|
get currentSnapshotFriendlyId() {
|
|
670
|
-
return this.
|
|
689
|
+
return this.snapshotManager?.snapshotId;
|
|
671
690
|
}
|
|
672
691
|
get taskRunEnv() {
|
|
673
692
|
return this.currentTaskRunEnv;
|
|
674
693
|
}
|
|
675
694
|
get metrics() {
|
|
676
695
|
return {
|
|
677
|
-
|
|
696
|
+
execution: {
|
|
697
|
+
restoreCount: this.restoreCount,
|
|
698
|
+
lastHeartbeat: this.lastHeartbeat,
|
|
699
|
+
},
|
|
700
|
+
poller: this.snapshotPoller?.metrics,
|
|
701
|
+
notifier: this.notifier?.metrics,
|
|
678
702
|
};
|
|
679
703
|
}
|
|
680
704
|
get isAborted() {
|
|
@@ -682,19 +706,120 @@ export class RunExecution {
|
|
|
682
706
|
}
|
|
683
707
|
abortExecution() {
|
|
684
708
|
if (this.isAborted) {
|
|
685
|
-
this.sendDebugLog("
|
|
709
|
+
this.sendDebugLog("execution already aborted");
|
|
686
710
|
return;
|
|
687
711
|
}
|
|
688
712
|
this.executionAbortController.abort();
|
|
689
|
-
this.
|
|
713
|
+
this.shutdown("abortExecution");
|
|
690
714
|
}
|
|
691
|
-
|
|
715
|
+
shutdown(reason) {
|
|
692
716
|
if (this.isShuttingDown) {
|
|
717
|
+
this.sendDebugLog(`[shutdown] ${reason} (already shutting down)`, {
|
|
718
|
+
firstShutdownReason: this.shutdownReason,
|
|
719
|
+
});
|
|
693
720
|
return;
|
|
694
721
|
}
|
|
722
|
+
this.sendDebugLog(`[shutdown] ${reason}`);
|
|
695
723
|
this.isShuttingDown = true;
|
|
724
|
+
this.shutdownReason = reason;
|
|
696
725
|
this.snapshotPoller?.stop();
|
|
697
|
-
this.
|
|
726
|
+
this.snapshotManager?.stop();
|
|
727
|
+
this.notifier?.stop();
|
|
728
|
+
this.taskRunProcess?.unsafeDetachEvtHandlers();
|
|
729
|
+
}
|
|
730
|
+
async handleSuspendable(suspendableSnapshot) {
|
|
731
|
+
this.sendDebugLog("handleSuspendable", { suspendableSnapshot });
|
|
732
|
+
if (!this.snapshotManager) {
|
|
733
|
+
this.sendDebugLog("handleSuspendable: missing snapshot manager", { suspendableSnapshot });
|
|
734
|
+
return;
|
|
735
|
+
}
|
|
736
|
+
// Ensure this is the current snapshot
|
|
737
|
+
if (suspendableSnapshot.id !== this.currentSnapshotFriendlyId) {
|
|
738
|
+
this.sendDebugLog("snapshot changed before cleanup, abort", {
|
|
739
|
+
suspendableSnapshot,
|
|
740
|
+
currentSnapshotId: this.currentSnapshotFriendlyId,
|
|
741
|
+
});
|
|
742
|
+
this.abortExecution();
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// First cleanup the task run process
|
|
746
|
+
const [error] = await tryCatch(this.taskRunProcess?.cleanup(false));
|
|
747
|
+
if (error) {
|
|
748
|
+
this.sendDebugLog("failed to cleanup task run process, carrying on", {
|
|
749
|
+
suspendableSnapshot,
|
|
750
|
+
error: error.message,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
// Double check snapshot hasn't changed after cleanup
|
|
754
|
+
if (suspendableSnapshot.id !== this.currentSnapshotFriendlyId) {
|
|
755
|
+
this.sendDebugLog("snapshot changed after cleanup, abort", {
|
|
756
|
+
suspendableSnapshot,
|
|
757
|
+
currentSnapshotId: this.currentSnapshotFriendlyId,
|
|
758
|
+
});
|
|
759
|
+
this.abortExecution();
|
|
760
|
+
return;
|
|
761
|
+
}
|
|
762
|
+
if (!this.runFriendlyId) {
|
|
763
|
+
this.sendDebugLog("missing run ID for suspension, abort", { suspendableSnapshot });
|
|
764
|
+
this.abortExecution();
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
// Call the suspend API with the current snapshot ID
|
|
768
|
+
const suspendResult = await this.httpClient.suspendRun(this.runFriendlyId, suspendableSnapshot.id);
|
|
769
|
+
if (!suspendResult.success) {
|
|
770
|
+
this.sendDebugLog("suspension request failed, staying alive 🎶", {
|
|
771
|
+
suspendableSnapshot,
|
|
772
|
+
error: suspendResult.error,
|
|
773
|
+
});
|
|
774
|
+
// This is fine, we'll wait for the next status change
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
if (!suspendResult.data.ok) {
|
|
778
|
+
this.sendDebugLog("suspension request returned error, staying alive 🎶", {
|
|
779
|
+
suspendableSnapshot,
|
|
780
|
+
error: suspendResult.data.error,
|
|
781
|
+
});
|
|
782
|
+
// This is fine, we'll wait for the next status change
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
this.sendDebugLog("suspending, any day now 🚬", { suspendableSnapshot });
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Fetches the latest execution data and enqueues snapshot changes. Used by both poller and notification handlers.
|
|
789
|
+
* @param source string - where this call originated (e.g. 'poller', 'notification')
|
|
790
|
+
*/
|
|
791
|
+
async fetchAndProcessSnapshotChanges(source) {
|
|
792
|
+
if (!this.runFriendlyId) {
|
|
793
|
+
this.sendDebugLog(`fetchAndProcessSnapshotChanges: missing runFriendlyId`, { source });
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
// Use the last processed snapshot as the since parameter
|
|
797
|
+
const sinceSnapshotId = this.currentSnapshotFriendlyId;
|
|
798
|
+
if (!sinceSnapshotId) {
|
|
799
|
+
this.sendDebugLog(`fetchAndProcessSnapshotChanges: missing sinceSnapshotId`, { source });
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const response = await this.httpClient.getSnapshotsSince(this.runFriendlyId, sinceSnapshotId);
|
|
803
|
+
if (!response.success) {
|
|
804
|
+
this.sendDebugLog(`fetchAndProcessSnapshotChanges: failed to get snapshots since`, {
|
|
805
|
+
source,
|
|
806
|
+
error: response.error,
|
|
807
|
+
});
|
|
808
|
+
await this.processEnvOverrides("snapshots since error");
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
const { snapshots } = response.data;
|
|
812
|
+
if (!snapshots.length) {
|
|
813
|
+
return;
|
|
814
|
+
}
|
|
815
|
+
const [error] = await tryCatch(this.enqueueSnapshotChangesAndWait(snapshots));
|
|
816
|
+
if (error) {
|
|
817
|
+
this.sendDebugLog(`fetchAndProcessSnapshotChanges: failed to enqueue and process snapshot change`, {
|
|
818
|
+
source,
|
|
819
|
+
error: error.message,
|
|
820
|
+
});
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
698
823
|
}
|
|
699
824
|
}
|
|
700
825
|
//# sourceMappingURL=execution.js.map
|