start-command 0.35.4 → 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 +6 -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 +16 -0
- package/src/lib/docker-cleanup.js +23 -2
- 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-recovery.js +145 -29
- package/src/lib/execution-resume-all.js +11 -0
- package/src/lib/execution-resume.js +142 -29
- package/src/lib/execution-store.js +25 -121
- package/src/lib/exit-evidence.js +123 -0
- package/src/lib/exit-reason.js +24 -6
- package/src/lib/isolation.js +28 -3
- 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 +28 -0
- 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 +9 -3
- package/test/exit-reason.js +6 -1
- package/test/isolation.js +5 -4
- package/test/regression-174.js +3 -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 +2 -1
- 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
|
@@ -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`);
|
|
@@ -337,18 +386,6 @@ function recoverKilledExecution(params = {}) {
|
|
|
337
386
|
'resume-started'
|
|
338
387
|
);
|
|
339
388
|
}
|
|
340
|
-
const started = runner(getDockerCommand(), ['start', containerName]);
|
|
341
|
-
if (!started.success) {
|
|
342
|
-
if (nextAttempt) {
|
|
343
|
-
appendLifecycle(
|
|
344
|
-
{ uuid: record.uuid, logPath: record.logPath, attempt: nextAttempt },
|
|
345
|
-
'launch-failed',
|
|
346
|
-
{ error: failureDetail(started) }
|
|
347
|
-
);
|
|
348
|
-
}
|
|
349
|
-
return giveUp(`docker start failed: ${failureDetail(started)}`);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
389
|
const described = describeExitCode(params.exitCode);
|
|
353
390
|
const delayed = delayRange ? `, delayMs=${delayMs}` : '';
|
|
354
391
|
// The killed run's own counters: the resumed run starts a fresh cgroup.
|
|
@@ -363,7 +400,7 @@ function recoverKilledExecution(params = {}) {
|
|
|
363
400
|
cgroupMemory: memory || undefined,
|
|
364
401
|
});
|
|
365
402
|
archiveAttempt(record);
|
|
366
|
-
|
|
403
|
+
|
|
367
404
|
record.attempt = nextAttempt;
|
|
368
405
|
}
|
|
369
406
|
const counted = memory
|
|
@@ -374,6 +411,18 @@ function recoverKilledExecution(params = {}) {
|
|
|
374
411
|
: '';
|
|
375
412
|
record.options = {
|
|
376
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
|
+
: {}),
|
|
377
426
|
recoveryAttempts: attempt,
|
|
378
427
|
recoveryHistory: [
|
|
379
428
|
...(Array.isArray(opts.recoveryHistory) ? opts.recoveryHistory : []),
|
|
@@ -383,6 +432,7 @@ function recoverKilledExecution(params = {}) {
|
|
|
383
432
|
...(delayRange ? { lastRecoveryDelayMs: delayMs } : {}),
|
|
384
433
|
...(resourceLimits && resourceLimits.length > 0 ? { resourceLimits } : {}),
|
|
385
434
|
};
|
|
435
|
+
delete record.options.exitEvidence;
|
|
386
436
|
record.status = 'executing';
|
|
387
437
|
record.exitCode = null;
|
|
388
438
|
record.endTime = null;
|
|
@@ -395,11 +445,56 @@ function recoverKilledExecution(params = {}) {
|
|
|
395
445
|
record.observedAt = undefined;
|
|
396
446
|
record.staleDetectedAt = undefined;
|
|
397
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;
|
|
398
494
|
try {
|
|
399
495
|
store.save(record);
|
|
400
|
-
} catch {
|
|
401
|
-
|
|
402
|
-
// it and finalizes the record when it ends.
|
|
496
|
+
} catch (error) {
|
|
497
|
+
persistenceError = error;
|
|
403
498
|
}
|
|
404
499
|
|
|
405
500
|
const startWatcher =
|
|
@@ -407,18 +502,29 @@ function recoverKilledExecution(params = {}) {
|
|
|
407
502
|
if (nextAttempt) {
|
|
408
503
|
appendLifecycle(record, 'launch-accepted');
|
|
409
504
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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) {
|
|
422
528
|
const current = patchAttempt(store, record, {
|
|
423
529
|
watcherAttachedAt: new Date().toISOString(),
|
|
424
530
|
});
|
|
@@ -426,6 +532,16 @@ function recoverKilledExecution(params = {}) {
|
|
|
426
532
|
appendLifecycle(current, 'watcher-attached');
|
|
427
533
|
}
|
|
428
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
|
+
}
|
|
429
545
|
return {
|
|
430
546
|
recovered: true,
|
|
431
547
|
reason: 'resumed',
|
|
@@ -125,6 +125,17 @@ 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(
|
|
@@ -28,7 +28,6 @@ const { escapeForLinksNotation } = require('./output-blocks');
|
|
|
28
28
|
const {
|
|
29
29
|
buildResourceLimitsStatusLine,
|
|
30
30
|
normalizeResourceLimits,
|
|
31
|
-
readDockerResourceLimits,
|
|
32
31
|
} = require('./docker-resource-limits');
|
|
33
32
|
const { appendLogFile } = require('./isolation-log-utils');
|
|
34
33
|
const {
|
|
@@ -38,6 +37,11 @@ const {
|
|
|
38
37
|
patchAttempt,
|
|
39
38
|
} = require('./execution-attempt');
|
|
40
39
|
const { runCommand } = require('./execution-control');
|
|
40
|
+
const {
|
|
41
|
+
reserveLaunch,
|
|
42
|
+
rollbackLaunch,
|
|
43
|
+
failLaunchedPersistence,
|
|
44
|
+
} = require('./launch-persistence');
|
|
41
45
|
const { SessionState, probeSession } = require('./session-probe');
|
|
42
46
|
const { getDockerNetworks } = require('./docker-network-lifecycle');
|
|
43
47
|
const {
|
|
@@ -89,6 +93,7 @@ function buildResumedSessionName(sessionName, attempt) {
|
|
|
89
93
|
function buildLaunchOptions(record) {
|
|
90
94
|
const opts = record.options || {};
|
|
91
95
|
return {
|
|
96
|
+
...opts,
|
|
92
97
|
image: opts.image || null,
|
|
93
98
|
session: opts.sessionName,
|
|
94
99
|
detached: true,
|
|
@@ -359,6 +364,7 @@ function applyResumeToRecord(
|
|
|
359
364
|
options.resumedAt = attempt.startedAt;
|
|
360
365
|
// A resume is a new deliberate start: launch-time recovery applies again.
|
|
361
366
|
delete options.stopRequestedAt;
|
|
367
|
+
delete options.exitEvidence;
|
|
362
368
|
if (plan.resourceLimits && plan.resourceLimits.length > 0) {
|
|
363
369
|
options.resourceLimits = plan.resourceLimits;
|
|
364
370
|
}
|
|
@@ -411,27 +417,61 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
411
417
|
};
|
|
412
418
|
}
|
|
413
419
|
|
|
420
|
+
if (require('./launch-owner').hasActiveLaunch(record)) {
|
|
421
|
+
return {
|
|
422
|
+
success: false,
|
|
423
|
+
error: 'An execution launch is already reserved.',
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
const previousData = globalThis.structuredClone(
|
|
427
|
+
record.toObject ? record.toObject() : record
|
|
428
|
+
);
|
|
429
|
+
const previous = record.constructor.fromObject
|
|
430
|
+
? record.constructor.fromObject(previousData)
|
|
431
|
+
: previousData;
|
|
414
432
|
const runner = deps.runner || runCommand;
|
|
415
433
|
const probeFn = deps.probe || ((r) => probeSession(r, runner));
|
|
416
434
|
const probe = probeFn(record);
|
|
417
435
|
const opts = record.options || {};
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
436
|
+
let resources;
|
|
437
|
+
try {
|
|
438
|
+
resources = require('./resume-resources').prepareResumeResources(
|
|
439
|
+
record,
|
|
440
|
+
probe,
|
|
441
|
+
deps.resourceOptions || {},
|
|
442
|
+
runner,
|
|
443
|
+
deps.random
|
|
444
|
+
);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
return { success: false, error: error.message };
|
|
447
|
+
}
|
|
448
|
+
const planningRecord = resources
|
|
449
|
+
? {
|
|
450
|
+
...record,
|
|
451
|
+
options: { ...opts, resourceLimits: resources.resourceLimits },
|
|
452
|
+
}
|
|
453
|
+
: record;
|
|
429
454
|
const plan = buildResumePlan(
|
|
430
|
-
|
|
455
|
+
planningRecord,
|
|
431
456
|
deps.command || null,
|
|
432
457
|
probe,
|
|
433
|
-
|
|
458
|
+
resources?.resourceLimits
|
|
434
459
|
);
|
|
460
|
+
if (resources && !plan.error) {
|
|
461
|
+
plan.resourceLimits = resources.resourceLimits;
|
|
462
|
+
if (plan.mode === ResumeMode.DOCKER_START && resources.update) {
|
|
463
|
+
plan.steps.unshift({
|
|
464
|
+
command: getDockerCommand(),
|
|
465
|
+
args: [
|
|
466
|
+
'update',
|
|
467
|
+
...resources.resourceLimits.filter((s) =>
|
|
468
|
+
/^--(?:memory|memory-swap|cpus|cpu-quota|cpu-period)=/.test(s)
|
|
469
|
+
),
|
|
470
|
+
plan.sessionName,
|
|
471
|
+
],
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
}
|
|
435
475
|
if (plan.error) {
|
|
436
476
|
return { success: false, error: plan.error };
|
|
437
477
|
}
|
|
@@ -444,15 +484,50 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
444
484
|
attempt,
|
|
445
485
|
};
|
|
446
486
|
appendLifecycle(lifecycleRecord, 'resume-started');
|
|
487
|
+
const updated = applyResumeToRecord(record, plan, null, attempt);
|
|
488
|
+
if (resources) {
|
|
489
|
+
updated.options.resolvedLimits = resources.resolvedLimits;
|
|
490
|
+
updated.options.resourceLimitSpecs = resources.resourceLimitSpecs;
|
|
491
|
+
if (
|
|
492
|
+
opts.cpuPenaltyConfig &&
|
|
493
|
+
deps.resourceOptions?.cpus !== null &&
|
|
494
|
+
deps.resourceOptions?.cpus !== undefined
|
|
495
|
+
) {
|
|
496
|
+
updated.options.baseResourceLimits = resources.resourceLimits;
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
try {
|
|
500
|
+
reserveLaunch(store, updated, previous);
|
|
501
|
+
} catch (error) {
|
|
502
|
+
appendLifecycle(lifecycleRecord, 'launch-failed', { error: error.message });
|
|
503
|
+
return {
|
|
504
|
+
success: false,
|
|
505
|
+
error: `Launch reservation failed: ${error.message}`,
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
if (resources && deps.resourceOptions?.memory && record.logPath) {
|
|
510
|
+
const oldMemory = resources.oldMemory;
|
|
511
|
+
appendLogFile(
|
|
512
|
+
record.logPath,
|
|
513
|
+
`Resume memory limit: ${oldMemory} -> ${resources.resolvedLimits.memory} (${deps.resourceOptions.memory})\n`
|
|
514
|
+
);
|
|
515
|
+
}
|
|
447
516
|
|
|
448
517
|
if (plan.mode === ResumeMode.RELAUNCH) {
|
|
449
518
|
const runIsolated = deps.runIsolated || require('./isolation').runIsolated;
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
519
|
+
let launchResult;
|
|
520
|
+
try {
|
|
521
|
+
launchResult = await runIsolated(
|
|
522
|
+
plan.backend,
|
|
523
|
+
plan.command,
|
|
524
|
+
plan.launchOptions
|
|
525
|
+
);
|
|
526
|
+
} catch (error) {
|
|
527
|
+
launchResult = { success: false, message: error.message };
|
|
528
|
+
}
|
|
455
529
|
if (!launchResult || !launchResult.success) {
|
|
530
|
+
rollbackLaunch(store, previous);
|
|
456
531
|
appendLifecycle(lifecycleRecord, 'launch-failed', {
|
|
457
532
|
error: launchResult?.message || 'unknown error',
|
|
458
533
|
});
|
|
@@ -466,8 +541,14 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
466
541
|
containerId = launchResult.containerId || null;
|
|
467
542
|
} else {
|
|
468
543
|
for (const step of plan.steps) {
|
|
469
|
-
|
|
544
|
+
let result;
|
|
545
|
+
try {
|
|
546
|
+
result = runner(step.command, step.args);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
result = { success: false, error: error.message };
|
|
549
|
+
}
|
|
470
550
|
if (!result.success) {
|
|
551
|
+
rollbackLaunch(store, previous);
|
|
471
552
|
const detail =
|
|
472
553
|
(result.stderr || '').trim() ||
|
|
473
554
|
result.error ||
|
|
@@ -489,8 +570,16 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
489
570
|
|
|
490
571
|
const previousSessionName = plan.newSessionName ? plan.sessionName : null;
|
|
491
572
|
attempt.launchAcceptedAt = new Date().toISOString();
|
|
492
|
-
|
|
493
|
-
|
|
573
|
+
updated.options.launchPending = false;
|
|
574
|
+
if (containerId) {
|
|
575
|
+
updated.options.containerId = containerId;
|
|
576
|
+
}
|
|
577
|
+
let persistenceError = null;
|
|
578
|
+
try {
|
|
579
|
+
store.save(updated);
|
|
580
|
+
} catch (error) {
|
|
581
|
+
persistenceError = error;
|
|
582
|
+
}
|
|
494
583
|
appendLifecycle(updated, 'launch-accepted');
|
|
495
584
|
|
|
496
585
|
if (plan.backend === 'docker') {
|
|
@@ -513,26 +602,50 @@ async function resumeExecution(store, identifier, deps = {}) {
|
|
|
513
602
|
if (watcher === false) {
|
|
514
603
|
throw new Error('Watcher process could not be started');
|
|
515
604
|
}
|
|
516
|
-
const current =
|
|
517
|
-
|
|
518
|
-
|
|
605
|
+
const current = persistenceError
|
|
606
|
+
? null
|
|
607
|
+
: patchAttempt(store, updated, {
|
|
608
|
+
watcherAttachedAt: new Date().toISOString(),
|
|
609
|
+
});
|
|
519
610
|
if (current) {
|
|
520
611
|
appendLifecycle(current, 'watcher-attached');
|
|
521
612
|
}
|
|
522
613
|
} catch (error) {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
614
|
+
if (persistenceError) {
|
|
615
|
+
return {
|
|
616
|
+
success: false,
|
|
617
|
+
error: failLaunchedPersistence(
|
|
618
|
+
store,
|
|
619
|
+
updated,
|
|
620
|
+
runner,
|
|
621
|
+
persistenceError
|
|
622
|
+
),
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
const current = persistenceError
|
|
626
|
+
? null
|
|
627
|
+
: patchAttempt(store, updated, {
|
|
628
|
+
watcherError: error.message,
|
|
629
|
+
});
|
|
526
630
|
appendLifecycle(current || updated, 'watcher-attachment-failed', {
|
|
527
631
|
error: error.message,
|
|
528
632
|
});
|
|
529
633
|
return {
|
|
530
634
|
success: false,
|
|
531
|
-
error: `Launch accepted, but completion watcher attachment failed: ${error.message}`,
|
|
635
|
+
error: `Launch accepted for container ${activeSessionName(plan)}, but completion watcher attachment failed: ${error.message}`,
|
|
532
636
|
};
|
|
533
637
|
}
|
|
534
638
|
}
|
|
535
639
|
|
|
640
|
+
if (persistenceError) {
|
|
641
|
+
// The reservation remains durable and the watcher is attached. Stop the
|
|
642
|
+
// new container rather than allow an untracked command to keep running.
|
|
643
|
+
return {
|
|
644
|
+
success: false,
|
|
645
|
+
error: failLaunchedPersistence(store, updated, runner, persistenceError),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
536
649
|
return {
|
|
537
650
|
success: true,
|
|
538
651
|
output: formatResumeResult(
|