start-command 0.35.3 → 0.36.0
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/CHANGELOG.md +12 -0
- package/README.md +10 -0
- package/package.json +1 -1
- package/src/bin/cli.js +43 -7
- package/src/lib/args-parser.js +12 -0
- package/src/lib/atomic-write.js +34 -0
- package/src/lib/attached-diagnostics.js +90 -0
- package/src/lib/cgroup-memory.js +43 -11
- package/src/lib/command-builder.js +40 -0
- package/src/lib/cpu-penalty-monitor.js +226 -0
- package/src/lib/cpu-penalty.js +175 -0
- package/src/lib/detached-finalize.js +47 -7
- package/src/lib/detached-output.js +61 -0
- package/src/lib/docker-cleanup.js +43 -4
- package/src/lib/docker-resource-limits.js +28 -0
- package/src/lib/docker-resource-options.js +180 -0
- package/src/lib/docker-runtime-args.js +15 -0
- package/src/lib/execution-attempt.js +153 -0
- package/src/lib/execution-recovery.js +191 -15
- package/src/lib/execution-resume-all.js +23 -1
- package/src/lib/execution-resume.js +203 -40
- package/src/lib/execution-store.js +56 -121
- package/src/lib/exit-evidence.js +123 -0
- package/src/lib/exit-reason.js +24 -6
- package/src/lib/isolation-log-utils.js +3 -3
- package/src/lib/isolation.js +36 -9
- package/src/lib/launch-owner.js +25 -0
- package/src/lib/launch-persistence.js +54 -0
- package/src/lib/query-commands.js +14 -3
- package/src/lib/resume-resources.js +88 -0
- package/src/lib/status-formatter.js +66 -6
- package/src/lib/store-lock.js +133 -0
- package/src/lib/usage.js +11 -0
- package/test/docker-runtime-options.js +6 -0
- package/test/execution-resume.js +10 -3
- package/test/exit-reason.js +6 -1
- package/test/isolation.js +5 -4
- package/test/regression-174.js +3 -1
- package/test/regression-176.js +5 -1
- package/test/regression-180.js +17 -10
- package/test/regression-182.js +7 -3
- package/test/regression-185.js +4 -9
- package/test/regression-187.js +547 -0
- package/test/regression-189.js +224 -0
- package/test/regression-190.js +111 -0
- package/test/regression-191.js +152 -0
- package/test/regression-192.js +66 -0
- package/test/regression-193.js +241 -0
- package/test/regression-194.js +182 -0
|
@@ -97,6 +97,12 @@ function buildDockerRuntimeStatusLines(options = {}) {
|
|
|
97
97
|
if (limitsLine) {
|
|
98
98
|
lines.push(limitsLine);
|
|
99
99
|
}
|
|
100
|
+
const limitsSummary = require('./docker-resource-options').limitsLogLine(
|
|
101
|
+
options
|
|
102
|
+
);
|
|
103
|
+
if (limitsSummary) {
|
|
104
|
+
lines.push(limitsSummary);
|
|
105
|
+
}
|
|
100
106
|
const onKillResume = getOnKillResume(options);
|
|
101
107
|
if (onKillResume) {
|
|
102
108
|
const what = options.recoveryCommand || 'the original command';
|
|
@@ -131,6 +137,15 @@ function buildDockerRuntimeMetadata(options = {}) {
|
|
|
131
137
|
? options.networkAliases
|
|
132
138
|
: null,
|
|
133
139
|
resourceLimits: resourceLimits.length > 0 ? resourceLimits : null,
|
|
140
|
+
resolvedLimits: options.resolvedLimits || null,
|
|
141
|
+
resourceLimitSpecs: options.resourceLimitSpecs || null,
|
|
142
|
+
onKillResumeMemory: options.onKillResumeMemory || null,
|
|
143
|
+
...(options.cpuPenaltyConfig
|
|
144
|
+
? {
|
|
145
|
+
cpuPenaltyConfig: options.cpuPenaltyConfig,
|
|
146
|
+
baseResourceLimits: resourceLimits,
|
|
147
|
+
}
|
|
148
|
+
: {}),
|
|
134
149
|
onKillResume: getOnKillResume(options),
|
|
135
150
|
recoveryCommand: options.recoveryCommand || null,
|
|
136
151
|
onKillResumeDelay: getOnKillResume(options)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/** Attempt-scoped evidence for logical executions sharing a UUID and log. */
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const { appendLogFile, readLogTail } = require('./isolation-log-utils');
|
|
4
|
+
|
|
5
|
+
const TERMINAL_FIELDS = [
|
|
6
|
+
'endTimeSource',
|
|
7
|
+
'observedAt',
|
|
8
|
+
'staleDetectedAt',
|
|
9
|
+
'containerStartedAt',
|
|
10
|
+
'exitReason',
|
|
11
|
+
'oomKilled',
|
|
12
|
+
'memoryExhausted',
|
|
13
|
+
'memoryExhaustedReason',
|
|
14
|
+
'cgroupMemory',
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function activityPath(record) {
|
|
18
|
+
return `${record.logPath}.attempt-${record.attempt.number}.activity`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readAttemptActivity(record) {
|
|
22
|
+
if (!record.attempt || !record.logPath) {
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
let fd;
|
|
26
|
+
try {
|
|
27
|
+
fd = fs.openSync(activityPath(record), 'r');
|
|
28
|
+
const buffer = Buffer.alloc(128);
|
|
29
|
+
const length = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
30
|
+
const at = buffer.subarray(0, length).toString().trim();
|
|
31
|
+
if (
|
|
32
|
+
Number.isFinite(Date.parse(at)) &&
|
|
33
|
+
Date.parse(at) >= Date.parse(record.attempt.startedAt)
|
|
34
|
+
) {
|
|
35
|
+
record.attempt = { ...record.attempt, lastOutputAt: at };
|
|
36
|
+
}
|
|
37
|
+
} catch {
|
|
38
|
+
// Quiet, missing, or unreadable output is not evidence of task progress.
|
|
39
|
+
} finally {
|
|
40
|
+
if (fd !== undefined) {
|
|
41
|
+
fs.closeSync(fd);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return record;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createAttempt(record, plan) {
|
|
48
|
+
let logOffset = null;
|
|
49
|
+
if (record.logPath) {
|
|
50
|
+
try {
|
|
51
|
+
logOffset = fs.statSync(record.logPath).size;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error.code === 'ENOENT') {
|
|
54
|
+
logOffset = 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
number:
|
|
60
|
+
(record.attempt?.number ||
|
|
61
|
+
(Number(record.options?.resumeCount) || 0) +
|
|
62
|
+
(Number(record.options?.recoveryAttempts) || 0) +
|
|
63
|
+
1) + 1,
|
|
64
|
+
startedAt: new Date().toISOString(),
|
|
65
|
+
logOffset,
|
|
66
|
+
lastOutputAt: null,
|
|
67
|
+
launchAcceptedAt: null,
|
|
68
|
+
watcherAttachedAt: null,
|
|
69
|
+
mode: plan.mode,
|
|
70
|
+
previousSessionName: record.options?.sessionName || null,
|
|
71
|
+
sessionName: plan.newSessionName || plan.sessionName,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function archiveAttempt(record) {
|
|
76
|
+
readAttemptActivity(record);
|
|
77
|
+
const snapshot = {
|
|
78
|
+
...(record.attempt || {
|
|
79
|
+
number:
|
|
80
|
+
(Number(record.options?.resumeCount) || 0) +
|
|
81
|
+
(Number(record.options?.recoveryAttempts) || 0) +
|
|
82
|
+
1,
|
|
83
|
+
startedAt: record.options?.resumedAt || record.startTime,
|
|
84
|
+
logOffset: null,
|
|
85
|
+
}),
|
|
86
|
+
command: record.command,
|
|
87
|
+
sessionName: record.options?.sessionName || null,
|
|
88
|
+
containerId: record.options?.containerId || null,
|
|
89
|
+
containerError: record.options?.containerError || null,
|
|
90
|
+
status: record.status,
|
|
91
|
+
exitCode: record.exitCode,
|
|
92
|
+
endTime: record.endTime,
|
|
93
|
+
};
|
|
94
|
+
for (const field of TERMINAL_FIELDS) {
|
|
95
|
+
if (record[field] !== undefined) {
|
|
96
|
+
snapshot[field] = record[field];
|
|
97
|
+
}
|
|
98
|
+
record[field] = undefined;
|
|
99
|
+
}
|
|
100
|
+
record.attemptHistory = [...(record.attemptHistory || []), snapshot];
|
|
101
|
+
delete record.options.containerError;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function appendLifecycle(record, event, details = {}) {
|
|
105
|
+
if (!record.logPath || !record.attempt) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
appendLogFile(
|
|
109
|
+
record.logPath,
|
|
110
|
+
`\n[Start Command Lifecycle] ${JSON.stringify({
|
|
111
|
+
event,
|
|
112
|
+
at: new Date().toISOString(),
|
|
113
|
+
uuid: record.uuid,
|
|
114
|
+
attemptNumber: record.attempt.number,
|
|
115
|
+
...record.attempt,
|
|
116
|
+
...details,
|
|
117
|
+
})}\n`
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function patchAttempt(store, record, fields) {
|
|
122
|
+
if (typeof store.patchAttempt === 'function') {
|
|
123
|
+
return store.patchAttempt(record.uuid, record.attempt.number, fields);
|
|
124
|
+
}
|
|
125
|
+
// Compatibility for consumers injecting a minimal in-memory store.
|
|
126
|
+
const current = store.get(record.uuid) || record;
|
|
127
|
+
if (current.attempt?.number !== record.attempt.number) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
current.attempt = { ...current.attempt, ...fields };
|
|
131
|
+
store.save(current);
|
|
132
|
+
return current;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readAttemptLogTail(record, bytes) {
|
|
136
|
+
if (
|
|
137
|
+
!record.logPath ||
|
|
138
|
+
(record.attempt && record.attempt.logOffset === null)
|
|
139
|
+
) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
return readLogTail(record.logPath, bytes, record.attempt?.logOffset || 0);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
activityPath,
|
|
147
|
+
appendLifecycle,
|
|
148
|
+
archiveAttempt,
|
|
149
|
+
createAttempt,
|
|
150
|
+
patchAttempt,
|
|
151
|
+
readAttemptActivity,
|
|
152
|
+
readAttemptLogTail,
|
|
153
|
+
};
|
|
@@ -39,6 +39,11 @@ const {
|
|
|
39
39
|
readDockerResourceLimits,
|
|
40
40
|
} = require('./docker-resource-limits');
|
|
41
41
|
const { shellQuote } = require('./isolation-log-utils');
|
|
42
|
+
const {
|
|
43
|
+
reserveLaunch,
|
|
44
|
+
rollbackLaunch,
|
|
45
|
+
failLaunchedPersistence,
|
|
46
|
+
} = require('./launch-persistence');
|
|
42
47
|
const {
|
|
43
48
|
CGROUP_SHELL_VARS,
|
|
44
49
|
formatCgroupMemoryLogLine,
|
|
@@ -299,7 +304,51 @@ function recoverKilledExecution(params = {}) {
|
|
|
299
304
|
// Docker keeps `docker update` limits in the HostConfig across restarts;
|
|
300
305
|
// they are read back so the log and `--status` show what the resumed run
|
|
301
306
|
// is held to, and so a later relaunch can re-apply them.
|
|
302
|
-
|
|
307
|
+
let resourceLimits =
|
|
308
|
+
readDockerResourceLimits(containerName, runner) ||
|
|
309
|
+
opts.resourceLimits ||
|
|
310
|
+
[];
|
|
311
|
+
resourceLimits = require('./resume-resources').restoreBaseCpu(
|
|
312
|
+
resourceLimits,
|
|
313
|
+
opts
|
|
314
|
+
);
|
|
315
|
+
if (opts.cpuPenaltyConfig) {
|
|
316
|
+
try {
|
|
317
|
+
resourceLimits = require('./resume-resources').clampCpuToDaemon(
|
|
318
|
+
resourceLimits,
|
|
319
|
+
runner
|
|
320
|
+
);
|
|
321
|
+
} catch (error) {
|
|
322
|
+
return giveUp(error.message);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
const evidence = require('./exit-evidence');
|
|
326
|
+
const terminalOom =
|
|
327
|
+
(params.oomKilled === true || params.oomKilled === 'true') &&
|
|
328
|
+
evidence.recentOomDelta(params.cgroupMemory, params.finishedAt) &&
|
|
329
|
+
!evidence.fromLog(
|
|
330
|
+
record.logPath
|
|
331
|
+
? require('./execution-attempt').readAttemptLogTail(record, 65536)
|
|
332
|
+
: null
|
|
333
|
+
).daemonRestart;
|
|
334
|
+
let recoveryLimits = null;
|
|
335
|
+
if (opts.onKillResumeMemory && terminalOom) {
|
|
336
|
+
try {
|
|
337
|
+
recoveryLimits =
|
|
338
|
+
require('./docker-resource-options').resolveResourceOptions(
|
|
339
|
+
{ memory: opts.onKillResumeMemory, resourceLimits },
|
|
340
|
+
runner,
|
|
341
|
+
params.random
|
|
342
|
+
);
|
|
343
|
+
} catch (error) {
|
|
344
|
+
return giveUp(`recovery limit failed: ${error.message}`);
|
|
345
|
+
}
|
|
346
|
+
log(
|
|
347
|
+
`Recovery memory limit: ${resourceLimits.find((s) => s.startsWith('--memory=')) || 'unlimited'} -> ${recoveryLimits.resolvedLimits.memory} (${opts.onKillResumeMemory})\n`
|
|
348
|
+
);
|
|
349
|
+
resourceLimits = recoveryLimits.resourceLimits;
|
|
350
|
+
}
|
|
351
|
+
|
|
303
352
|
const limitsLine = buildResourceLimitsStatusLine(resourceLimits);
|
|
304
353
|
if (limitsLine) {
|
|
305
354
|
log(`${limitsLine}\n`);
|
|
@@ -318,15 +367,42 @@ function recoverKilledExecution(params = {}) {
|
|
|
318
367
|
}
|
|
319
368
|
|
|
320
369
|
const since = now().toISOString();
|
|
321
|
-
const
|
|
322
|
-
|
|
323
|
-
|
|
370
|
+
const {
|
|
371
|
+
appendLifecycle,
|
|
372
|
+
archiveAttempt,
|
|
373
|
+
createAttempt,
|
|
374
|
+
patchAttempt,
|
|
375
|
+
} = require('./execution-attempt');
|
|
376
|
+
const nextAttempt = record.attempt
|
|
377
|
+
? createAttempt(record, {
|
|
378
|
+
mode: 'automatic-recovery',
|
|
379
|
+
sessionName: containerName,
|
|
380
|
+
})
|
|
381
|
+
: null;
|
|
382
|
+
if (nextAttempt) {
|
|
383
|
+
nextAttempt.startedAt = since;
|
|
384
|
+
appendLifecycle(
|
|
385
|
+
{ uuid: record.uuid, logPath: record.logPath, attempt: nextAttempt },
|
|
386
|
+
'resume-started'
|
|
387
|
+
);
|
|
324
388
|
}
|
|
325
|
-
|
|
326
389
|
const described = describeExitCode(params.exitCode);
|
|
327
390
|
const delayed = delayRange ? `, delayMs=${delayMs}` : '';
|
|
328
391
|
// The killed run's own counters: the resumed run starts a fresh cgroup.
|
|
329
392
|
const memory = parseCgroupMemorySample(params.cgroupMemory);
|
|
393
|
+
if (nextAttempt) {
|
|
394
|
+
Object.assign(record, {
|
|
395
|
+
status: 'executed',
|
|
396
|
+
exitCode: described.code,
|
|
397
|
+
endTime: params.finishedAt || null,
|
|
398
|
+
containerStartedAt: params.startedAt || undefined,
|
|
399
|
+
oomKilled: params.oomKilled === true || params.oomKilled === 'true',
|
|
400
|
+
cgroupMemory: memory || undefined,
|
|
401
|
+
});
|
|
402
|
+
archiveAttempt(record);
|
|
403
|
+
|
|
404
|
+
record.attempt = nextAttempt;
|
|
405
|
+
}
|
|
330
406
|
const counted = memory
|
|
331
407
|
? ['oomEvents', 'oomKills']
|
|
332
408
|
.filter((field) => memory[field] !== null)
|
|
@@ -335,6 +411,18 @@ function recoverKilledExecution(params = {}) {
|
|
|
335
411
|
: '';
|
|
336
412
|
record.options = {
|
|
337
413
|
...opts,
|
|
414
|
+
...(recoveryLimits
|
|
415
|
+
? {
|
|
416
|
+
resolvedLimits: {
|
|
417
|
+
...opts.resolvedLimits,
|
|
418
|
+
...recoveryLimits.resolvedLimits,
|
|
419
|
+
},
|
|
420
|
+
resourceLimitSpecs: {
|
|
421
|
+
...opts.resourceLimitSpecs,
|
|
422
|
+
memory: opts.onKillResumeMemory,
|
|
423
|
+
},
|
|
424
|
+
}
|
|
425
|
+
: {}),
|
|
338
426
|
recoveryAttempts: attempt,
|
|
339
427
|
recoveryHistory: [
|
|
340
428
|
...(Array.isArray(opts.recoveryHistory) ? opts.recoveryHistory : []),
|
|
@@ -344,28 +432,116 @@ function recoverKilledExecution(params = {}) {
|
|
|
344
432
|
...(delayRange ? { lastRecoveryDelayMs: delayMs } : {}),
|
|
345
433
|
...(resourceLimits && resourceLimits.length > 0 ? { resourceLimits } : {}),
|
|
346
434
|
};
|
|
435
|
+
delete record.options.exitEvidence;
|
|
347
436
|
record.status = 'executing';
|
|
348
437
|
record.exitCode = null;
|
|
349
438
|
record.endTime = null;
|
|
350
439
|
record.exitReason = undefined;
|
|
351
440
|
record.oomKilled = undefined;
|
|
352
441
|
record.cgroupMemory = undefined;
|
|
442
|
+
record.memoryExhausted = undefined;
|
|
443
|
+
record.memoryExhaustedReason = undefined;
|
|
444
|
+
record.endTimeSource = undefined;
|
|
445
|
+
record.observedAt = undefined;
|
|
446
|
+
record.staleDetectedAt = undefined;
|
|
447
|
+
record.containerStartedAt = undefined;
|
|
448
|
+
const previousData = globalThis.structuredClone(store.get(executionId));
|
|
449
|
+
const previous = record.constructor.fromObject
|
|
450
|
+
? record.constructor.fromObject(previousData)
|
|
451
|
+
: previousData;
|
|
452
|
+
try {
|
|
453
|
+
reserveLaunch(store, record, previous);
|
|
454
|
+
} catch (error) {
|
|
455
|
+
return giveUp(`launch reservation failed: ${error.message}`);
|
|
456
|
+
}
|
|
457
|
+
if (recoveryLimits || opts.cpuPenaltyConfig) {
|
|
458
|
+
const updated = runner(getDockerCommand(), [
|
|
459
|
+
'update',
|
|
460
|
+
...resourceLimits.filter((s) =>
|
|
461
|
+
/^--(?:memory|memory-swap|cpus|cpu-quota|cpu-period)=/.test(s)
|
|
462
|
+
),
|
|
463
|
+
containerName,
|
|
464
|
+
]);
|
|
465
|
+
if (!updated.success) {
|
|
466
|
+
rollbackLaunch(store, previous);
|
|
467
|
+
return giveUp(`docker update failed: ${failureDetail(updated)}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const started = runner(getDockerCommand(), ['start', containerName]);
|
|
471
|
+
if (!started.success) {
|
|
472
|
+
if (nextAttempt) {
|
|
473
|
+
appendLifecycle(
|
|
474
|
+
{ uuid: record.uuid, logPath: record.logPath, attempt: nextAttempt },
|
|
475
|
+
'launch-failed',
|
|
476
|
+
{ error: failureDetail(started) }
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
rollbackLaunch(store, previous);
|
|
480
|
+
return giveUp(`docker start failed: ${failureDetail(started)}`);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (opts.cpuPenaltyConfig) {
|
|
484
|
+
record.options.resolvedLimits = {
|
|
485
|
+
...record.options.resolvedLimits,
|
|
486
|
+
cpus: require('./docker-resource-limits').cpuCount(resourceLimits),
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
record.options.launchPending = false;
|
|
490
|
+
if (nextAttempt) {
|
|
491
|
+
nextAttempt.launchAcceptedAt = new Date().toISOString();
|
|
492
|
+
}
|
|
493
|
+
let persistenceError = null;
|
|
353
494
|
try {
|
|
354
495
|
store.save(record);
|
|
355
|
-
} catch {
|
|
356
|
-
|
|
357
|
-
// it and finalizes the record when it ends.
|
|
496
|
+
} catch (error) {
|
|
497
|
+
persistenceError = error;
|
|
358
498
|
}
|
|
359
499
|
|
|
360
500
|
const startWatcher =
|
|
361
501
|
params.startWatcher || startDetachedDockerCompletionWatcher;
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
502
|
+
if (nextAttempt) {
|
|
503
|
+
appendLifecycle(record, 'launch-accepted');
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
startWatcher(
|
|
507
|
+
containerName,
|
|
508
|
+
getDockerContainerCleanupPolicy(opts),
|
|
509
|
+
record.logPath || null,
|
|
510
|
+
record.uuid,
|
|
511
|
+
{
|
|
512
|
+
since,
|
|
513
|
+
recoverOnKill: true,
|
|
514
|
+
...(nextAttempt ? { attemptNumber: nextAttempt.number } : {}),
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
} catch (error) {
|
|
518
|
+
const warning = failLaunchedPersistence(
|
|
519
|
+
store,
|
|
520
|
+
record,
|
|
521
|
+
runner,
|
|
522
|
+
persistenceError || error
|
|
523
|
+
);
|
|
524
|
+
log(`[Recovery] ${warning}\n`);
|
|
525
|
+
return { recovered: false, reason: warning, attempt };
|
|
526
|
+
}
|
|
527
|
+
if (nextAttempt && !persistenceError) {
|
|
528
|
+
const current = patchAttempt(store, record, {
|
|
529
|
+
watcherAttachedAt: new Date().toISOString(),
|
|
530
|
+
});
|
|
531
|
+
if (current) {
|
|
532
|
+
appendLifecycle(current, 'watcher-attached');
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (persistenceError) {
|
|
536
|
+
const warning = failLaunchedPersistence(
|
|
537
|
+
store,
|
|
538
|
+
record,
|
|
539
|
+
runner,
|
|
540
|
+
persistenceError
|
|
541
|
+
);
|
|
542
|
+
log(`[Recovery] ${warning}\n`);
|
|
543
|
+
return { recovered: false, reason: warning, attempt };
|
|
544
|
+
}
|
|
369
545
|
return {
|
|
370
546
|
recovered: true,
|
|
371
547
|
reason: 'resumed',
|
|
@@ -125,13 +125,35 @@ function resumeAllExecutions(store, deps = {}) {
|
|
|
125
125
|
for (const record of records) {
|
|
126
126
|
const probe = probeFn(record);
|
|
127
127
|
const sessionName = probe.sessionName;
|
|
128
|
+
if (require('./launch-owner').hasActiveLaunch(record)) {
|
|
129
|
+
executions.push(
|
|
130
|
+
describeRecord(
|
|
131
|
+
record,
|
|
132
|
+
probe,
|
|
133
|
+
ResumeAllAction.RUNNING,
|
|
134
|
+
'Launch is reserved; no watcher or duplicate launch was started.'
|
|
135
|
+
)
|
|
136
|
+
);
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
128
139
|
|
|
129
140
|
if (probe.alive && probe.backend === 'docker') {
|
|
130
141
|
startWatcher(
|
|
131
142
|
sessionName,
|
|
132
143
|
getDockerContainerCleanupPolicy(record.options || {}),
|
|
133
144
|
record.logPath || null,
|
|
134
|
-
record.uuid || null
|
|
145
|
+
record.uuid || null,
|
|
146
|
+
record.attempt
|
|
147
|
+
? {
|
|
148
|
+
since: record.attempt.startedAt,
|
|
149
|
+
attemptNumber: record.attempt.number,
|
|
150
|
+
recoverOnKill:
|
|
151
|
+
Boolean(record.options?.onKillResume) &&
|
|
152
|
+
['docker-start', 'automatic-recovery', 'relaunch'].includes(
|
|
153
|
+
record.attempt.mode
|
|
154
|
+
),
|
|
155
|
+
}
|
|
156
|
+
: {}
|
|
135
157
|
);
|
|
136
158
|
executions.push(
|
|
137
159
|
describeRecord(
|