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
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/** Docker CPU hysteresis with timestamp-weighted, fully covered windows. */
|
|
2
|
+
const DEFAULTS = {
|
|
3
|
+
cpus: 2,
|
|
4
|
+
trigger: 95,
|
|
5
|
+
triggerWindowMs: 900000,
|
|
6
|
+
release: 65,
|
|
7
|
+
releaseWindowMs: 900000,
|
|
8
|
+
};
|
|
9
|
+
const FIELDS = {
|
|
10
|
+
'--cpu-penalty-cpus': 'cpus',
|
|
11
|
+
'--cpu-penalty-trigger': 'trigger',
|
|
12
|
+
'--cpu-penalty-trigger-window': 'triggerWindowMs',
|
|
13
|
+
'--cpu-penalty-release': 'release',
|
|
14
|
+
'--cpu-penalty-release-window': 'releaseWindowMs',
|
|
15
|
+
};
|
|
16
|
+
function duration(value) {
|
|
17
|
+
const m = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(value);
|
|
18
|
+
const n = m
|
|
19
|
+
? Number(m[1]) * { ms: 1, s: 1000, m: 60000, h: 3600000 }[m[2]]
|
|
20
|
+
: NaN;
|
|
21
|
+
if (!Number.isSafeInteger(n) || n <= 0) {
|
|
22
|
+
throw Error(`Invalid CPU penalty duration: ${value}`);
|
|
23
|
+
}
|
|
24
|
+
return n;
|
|
25
|
+
}
|
|
26
|
+
function parse(args, index, options) {
|
|
27
|
+
const [flag, inline] = args[index].split('=', 2);
|
|
28
|
+
if (flag === '--cpu-penalty') {
|
|
29
|
+
if (inline !== undefined) {
|
|
30
|
+
throw Error('--cpu-penalty does not take a value');
|
|
31
|
+
}
|
|
32
|
+
options.cpuPenalty = true;
|
|
33
|
+
return 1;
|
|
34
|
+
}
|
|
35
|
+
const field = FIELDS[flag];
|
|
36
|
+
if (!field) {
|
|
37
|
+
return 0;
|
|
38
|
+
}
|
|
39
|
+
const value = inline ?? args[index + 1];
|
|
40
|
+
if (!value) {
|
|
41
|
+
throw Error(`${flag} requires a value`);
|
|
42
|
+
}
|
|
43
|
+
const number = field.endsWith('Ms')
|
|
44
|
+
? duration(value)
|
|
45
|
+
: Number(field === 'cpus' ? value : value.replace(/%$/, ''));
|
|
46
|
+
if (
|
|
47
|
+
!(number > 0) ||
|
|
48
|
+
!Number.isFinite(number) ||
|
|
49
|
+
(field !== 'cpus' && !field.endsWith('Ms') && number > 100)
|
|
50
|
+
) {
|
|
51
|
+
throw Error(`Invalid ${flag}: ${value}`);
|
|
52
|
+
}
|
|
53
|
+
options.cpuPenaltyConfig = {
|
|
54
|
+
...DEFAULTS,
|
|
55
|
+
...options.cpuPenaltyConfig,
|
|
56
|
+
[field]: number,
|
|
57
|
+
};
|
|
58
|
+
return inline === undefined ? 2 : 1;
|
|
59
|
+
}
|
|
60
|
+
function validate(options) {
|
|
61
|
+
if (!options.cpuPenalty && !options.cpuPenaltyConfig) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!options.cpuPenalty) {
|
|
65
|
+
throw Error('CPU penalty settings require --cpu-penalty');
|
|
66
|
+
}
|
|
67
|
+
if (!(options.isolatedStack || [options.isolated]).includes('docker')) {
|
|
68
|
+
throw Error('--cpu-penalty requires Docker isolation');
|
|
69
|
+
}
|
|
70
|
+
options.cpuPenaltyConfig = { ...DEFAULTS, ...options.cpuPenaltyConfig };
|
|
71
|
+
}
|
|
72
|
+
function initialState(baseCpus = null, now = Date.now(), saved = null) {
|
|
73
|
+
return {
|
|
74
|
+
phase: saved?.phase === 'penalized' ? 'penalized' : 'observing',
|
|
75
|
+
since: saved?.phase === 'penalized' ? saved.since : now,
|
|
76
|
+
limitCpus: saved?.phase === 'penalized' ? saved.limitCpus : baseCpus,
|
|
77
|
+
baseCpus,
|
|
78
|
+
penaltyCount: saved?.penaltyCount || 0,
|
|
79
|
+
penalizedMs: saved?.penalizedMs || 0,
|
|
80
|
+
samples: [],
|
|
81
|
+
lastAt: now,
|
|
82
|
+
capacity: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function average(samples, now, window) {
|
|
86
|
+
const start = now - window;
|
|
87
|
+
if (
|
|
88
|
+
samples.length < 2 ||
|
|
89
|
+
samples[0].at > start ||
|
|
90
|
+
samples.at(-1).at !== now
|
|
91
|
+
) {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
let weighted = 0;
|
|
95
|
+
for (let i = 1; i < samples.length; i++) {
|
|
96
|
+
const left = Math.max(start, samples[i - 1].at),
|
|
97
|
+
right = samples[i].at;
|
|
98
|
+
if (right > left) {
|
|
99
|
+
weighted += (right - left) * samples[i - 1].cores;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return weighted / window;
|
|
103
|
+
}
|
|
104
|
+
function evaluate(
|
|
105
|
+
state,
|
|
106
|
+
{ now, cores, daemonCpus, maxGapMs },
|
|
107
|
+
config = DEFAULTS
|
|
108
|
+
) {
|
|
109
|
+
const s = { ...state, samples: [...state.samples] };
|
|
110
|
+
if (s.phase === 'penalized') {
|
|
111
|
+
s.penalizedMs += Math.max(0, now - s.lastAt);
|
|
112
|
+
}
|
|
113
|
+
s.lastAt = now;
|
|
114
|
+
const capacity = Number.isFinite(daemonCpus)
|
|
115
|
+
? Math.min(s.baseCpus || Infinity, daemonCpus)
|
|
116
|
+
: NaN;
|
|
117
|
+
if (
|
|
118
|
+
!(cores >= 0) ||
|
|
119
|
+
!Number.isFinite(cores) ||
|
|
120
|
+
!(capacity > 0) ||
|
|
121
|
+
now <= (s.samples.at(-1)?.at ?? -Infinity) ||
|
|
122
|
+
(s.samples.length && now - s.samples.at(-1).at > maxGapMs) ||
|
|
123
|
+
s.capacity !== capacity
|
|
124
|
+
) {
|
|
125
|
+
s.samples = [];
|
|
126
|
+
}
|
|
127
|
+
s.capacity = capacity;
|
|
128
|
+
if (!(cores >= 0) || !Number.isFinite(cores) || !(capacity > 0)) {
|
|
129
|
+
return { state: s, action: null };
|
|
130
|
+
}
|
|
131
|
+
s.samples.push({ at: now, cores });
|
|
132
|
+
const window =
|
|
133
|
+
s.phase === 'penalized' ? config.releaseWindowMs : config.triggerWindowMs;
|
|
134
|
+
while (s.samples.length > 2 && s.samples[1].at <= now - window) {
|
|
135
|
+
s.samples.shift();
|
|
136
|
+
}
|
|
137
|
+
const avg = average(s.samples, now, window);
|
|
138
|
+
let action = null;
|
|
139
|
+
if (
|
|
140
|
+
s.phase === 'observing' &&
|
|
141
|
+
config.cpus < capacity &&
|
|
142
|
+
avg !== null &&
|
|
143
|
+
avg >= (config.trigger / 100) * capacity
|
|
144
|
+
) {
|
|
145
|
+
action = { kind: 'apply', cpus: config.cpus, average: avg, capacity };
|
|
146
|
+
s.phase = 'penalized';
|
|
147
|
+
s.since = now;
|
|
148
|
+
s.limitCpus = config.cpus;
|
|
149
|
+
s.penaltyCount++;
|
|
150
|
+
} else if (
|
|
151
|
+
s.phase === 'penalized' &&
|
|
152
|
+
(config.cpus >= capacity ||
|
|
153
|
+
(now - s.since >= config.releaseWindowMs &&
|
|
154
|
+
avg !== null &&
|
|
155
|
+
avg < (config.release / 100) * config.cpus))
|
|
156
|
+
) {
|
|
157
|
+
action = { kind: 'lift', cpus: capacity, average: avg, capacity };
|
|
158
|
+
s.phase = 'observing';
|
|
159
|
+
s.since = now;
|
|
160
|
+
s.limitCpus = capacity;
|
|
161
|
+
}
|
|
162
|
+
if (action) {
|
|
163
|
+
s.samples = [];
|
|
164
|
+
}
|
|
165
|
+
return { state: s, action };
|
|
166
|
+
}
|
|
167
|
+
module.exports = {
|
|
168
|
+
DEFAULTS,
|
|
169
|
+
parse,
|
|
170
|
+
validate,
|
|
171
|
+
duration,
|
|
172
|
+
initialState,
|
|
173
|
+
average,
|
|
174
|
+
evaluate,
|
|
175
|
+
};
|
|
@@ -146,6 +146,21 @@ function finalizeDetachedExecution(options = {}) {
|
|
|
146
146
|
record.cgroupMemory = cgroupMemory;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
const evidence = require('./exit-evidence');
|
|
150
|
+
const logged = evidence.fromLog(
|
|
151
|
+
record.logPath
|
|
152
|
+
? require('./execution-attempt').readAttemptLogTail(record, 65536)
|
|
153
|
+
: null
|
|
154
|
+
);
|
|
155
|
+
record.options.exitEvidence = {
|
|
156
|
+
daemonRestart: logged.daemonRestart,
|
|
157
|
+
mainOom:
|
|
158
|
+
!logged.daemonRestart &&
|
|
159
|
+
(logged.mainOom ||
|
|
160
|
+
(evidence.recentOomDelta(options.cgroupMemory, options.finishedAt) &&
|
|
161
|
+
record.oomKilled === true &&
|
|
162
|
+
(record.exitCode === 137 || record.exitCode < 0))),
|
|
163
|
+
};
|
|
149
164
|
const exitReason =
|
|
150
165
|
resolveReason(record) || (finishedAt ? null : WATCHER_LOST_CONTAINER);
|
|
151
166
|
if (exitReason) {
|
|
@@ -197,6 +212,7 @@ function resolveReason(record) {
|
|
|
197
212
|
logTail,
|
|
198
213
|
oomKilled: record.oomKilled,
|
|
199
214
|
cgroupMemory: record.cgroupMemory,
|
|
215
|
+
exitEvidence: record.options?.exitEvidence,
|
|
200
216
|
});
|
|
201
217
|
} catch {
|
|
202
218
|
return null;
|
|
@@ -275,10 +275,13 @@ function buildAttachedDockerKeptMessage({
|
|
|
275
275
|
oomKilled,
|
|
276
276
|
logPath,
|
|
277
277
|
}) {
|
|
278
|
+
const logTail = logPath
|
|
279
|
+
? readLogTail(logPath, FATAL_MARKER_TAIL_BYTES)
|
|
280
|
+
: null;
|
|
278
281
|
let message;
|
|
279
282
|
if (oomKilled !== true) {
|
|
280
283
|
message = `\nContainer kept because the command failed.`;
|
|
281
|
-
} else if (isOomKillOfCommand({ exitCode, oomKilled })) {
|
|
284
|
+
} else if (isOomKillOfCommand({ exitCode, oomKilled, logTail })) {
|
|
282
285
|
message = `\nContainer kept because Docker reports it was OOM-killed.`;
|
|
283
286
|
} else {
|
|
284
287
|
// The flag is container-wide (#180): a child was OOM-killed, the command
|
|
@@ -287,7 +290,7 @@ function buildAttachedDockerKeptMessage({
|
|
|
287
290
|
}
|
|
288
291
|
const memory = resolveMemoryExhaustion({
|
|
289
292
|
exitCode,
|
|
290
|
-
logTail
|
|
293
|
+
logTail,
|
|
291
294
|
oomKilled,
|
|
292
295
|
});
|
|
293
296
|
if (memory) {
|
|
@@ -360,7 +363,17 @@ function buildDetachedDockerCompletionScript(
|
|
|
360
363
|
const quotedName = shellQuote(containerName);
|
|
361
364
|
// The container's cgroup disappears with it, so its memory counters are
|
|
362
365
|
// sampled while it runs (issue #182).
|
|
366
|
+
const cpuMonitor = require('./cpu-penalty-monitor');
|
|
363
367
|
const parts = [buildCgroupSamplerStartSnippet(containerName)];
|
|
368
|
+
const cpuStart = cpuMonitor.startSnippet(
|
|
369
|
+
executionId,
|
|
370
|
+
containerName,
|
|
371
|
+
watcherOptions.attemptNumber || 1,
|
|
372
|
+
watcherOptions.cpuOptions
|
|
373
|
+
);
|
|
374
|
+
if (cpuStart) {
|
|
375
|
+
parts.push(cpuStart);
|
|
376
|
+
}
|
|
364
377
|
// Everything that assumes the container has exited: cleanup, footer and
|
|
365
378
|
// finalization. Guarded as a whole by `.State.Running` below.
|
|
366
379
|
const exited = [];
|
|
@@ -393,8 +406,15 @@ function buildDetachedDockerCompletionScript(
|
|
|
393
406
|
);
|
|
394
407
|
}
|
|
395
408
|
parts.push(buildDockerWaitForExitSnippet(containerName, quotedLogPath));
|
|
409
|
+
parts.push(cpuMonitor.stopSnippet());
|
|
396
410
|
parts.push(buildCgroupSamplerStopSnippet());
|
|
397
411
|
parts.push(buildDockerStateSnippet(containerName));
|
|
412
|
+
parts.push(
|
|
413
|
+
require('./exit-evidence').buildExitEvidenceSnippet(
|
|
414
|
+
containerName,
|
|
415
|
+
quotedLogPath
|
|
416
|
+
)
|
|
417
|
+
);
|
|
398
418
|
if (policy === DOCKER_CONTAINER_CLEANUP_POLICY.ALWAYS) {
|
|
399
419
|
exited.push(remove);
|
|
400
420
|
} else if (
|
|
@@ -414,6 +434,7 @@ function buildDetachedDockerCompletionScript(
|
|
|
414
434
|
} else {
|
|
415
435
|
parts.push(`docker wait ${quotedName} >/dev/null 2>&1`);
|
|
416
436
|
parts.push(buildDockerWaitForExitSnippet(containerName));
|
|
437
|
+
parts.push(cpuMonitor.stopSnippet());
|
|
417
438
|
parts.push(buildCgroupSamplerStopSnippet());
|
|
418
439
|
parts.push(buildDockerStateSnippet(containerName));
|
|
419
440
|
if (policy === DOCKER_CONTAINER_CLEANUP_POLICY.ALWAYS) {
|
|
@@ -200,8 +200,36 @@ function buildResourceLimitsStatusLine(value) {
|
|
|
200
200
|
: null;
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/** Preserve Docker's CPU representation: an existing quota cannot become NanoCPUs in place. */
|
|
204
|
+
function cpuCount(limits) {
|
|
205
|
+
const get = (prefix) =>
|
|
206
|
+
positive(limits.find((s) => s.startsWith(prefix))?.slice(prefix.length));
|
|
207
|
+
return (
|
|
208
|
+
get('--cpus=') ||
|
|
209
|
+
(get('--cpu-quota=')
|
|
210
|
+
? get('--cpu-quota=') / (get('--cpu-period=') || 100000)
|
|
211
|
+
: null)
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
function cpuUpdateArgs(limits, cpus) {
|
|
215
|
+
return !limits.some((s) => s.startsWith('--cpus=')) &&
|
|
216
|
+
limits.some((s) => /^--cpu-(quota|period)=/.test(s))
|
|
217
|
+
? ['--cpu-period=100000', `--cpu-quota=${Math.ceil(cpus * 100000)}`]
|
|
218
|
+
: ['--cpus', String(cpus)];
|
|
219
|
+
}
|
|
220
|
+
function withCpuLimit(limits, cpus) {
|
|
221
|
+
const args = cpuUpdateArgs(limits, cpus);
|
|
222
|
+
return [
|
|
223
|
+
...limits.filter((s) => !/^--(?:cpus|cpu-quota|cpu-period)=/.test(s)),
|
|
224
|
+
...(args[0] === '--cpus' ? [`--cpus=${args[1]}`] : args),
|
|
225
|
+
];
|
|
226
|
+
}
|
|
227
|
+
|
|
203
228
|
module.exports = {
|
|
204
229
|
DEFAULT_SHM_SIZE,
|
|
230
|
+
cpuCount,
|
|
231
|
+
cpuUpdateArgs,
|
|
232
|
+
withCpuLimit,
|
|
205
233
|
buildResourceLimitsStatusLine,
|
|
206
234
|
formatDockerBytes,
|
|
207
235
|
formatDockerCpus,
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/** Launch and recovery resource specifications, resolved against the daemon. */
|
|
2
|
+
const { getDockerCommand } = require('./docker-cleanup');
|
|
3
|
+
const { normalizeResourceLimits } = require('./docker-resource-limits');
|
|
4
|
+
const FIELDS = {
|
|
5
|
+
'--memory': 'memory',
|
|
6
|
+
'--memory-swap': 'memorySwap',
|
|
7
|
+
'--cpus': 'cpus',
|
|
8
|
+
'--on-kill-resume-memory': 'onKillResumeMemory',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function parseResourceSpec(value, cpu = false) {
|
|
12
|
+
const text = String(value).trim().toLowerCase();
|
|
13
|
+
const percent = /^(\d+(?:\.\d+)?)%(?:-(\d+(?:\.\d+)?)%)?$/.exec(text);
|
|
14
|
+
if (percent) {
|
|
15
|
+
const min = Number(percent[1]),
|
|
16
|
+
max = Number(percent[2] || percent[1]);
|
|
17
|
+
if (min > 0 && max <= 100 && min <= max) {
|
|
18
|
+
return { min, max, percent: true };
|
|
19
|
+
}
|
|
20
|
+
} else {
|
|
21
|
+
const absolute = /^(\d+(?:\.\d+)?)(b|k|m|g|t|kb|mb|gb|tb)?$/.exec(text);
|
|
22
|
+
if (absolute && (!cpu || !absolute[2])) {
|
|
23
|
+
const power = ['', 'k', 'm', 'g', 't'].indexOf(
|
|
24
|
+
(absolute[2] || '').replace('b', '')
|
|
25
|
+
);
|
|
26
|
+
const amount =
|
|
27
|
+
Number(absolute[1]) * (cpu ? 1 : 1024 ** Math.max(0, power));
|
|
28
|
+
if (
|
|
29
|
+
Number.isFinite(amount) &&
|
|
30
|
+
amount > 0 &&
|
|
31
|
+
amount <= Number.MAX_SAFE_INTEGER
|
|
32
|
+
) {
|
|
33
|
+
return { amount };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
throw new Error(
|
|
38
|
+
`Invalid resource limit: "${value}". Expected a positive ${cpu ? 'CPU count' : 'Docker size'}, N% or MIN%-MAX% within (0, 100].`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parse(args, index, options) {
|
|
43
|
+
const [flag, inline] = args[index].split('=', 2);
|
|
44
|
+
if (!FIELDS[flag]) {
|
|
45
|
+
return 0;
|
|
46
|
+
}
|
|
47
|
+
const value = inline ?? args[index + 1];
|
|
48
|
+
if (!value || value.startsWith('-')) {
|
|
49
|
+
throw new Error(`Option ${flag} requires a resource limit`);
|
|
50
|
+
}
|
|
51
|
+
parseResourceSpec(value, flag === '--cpus');
|
|
52
|
+
options[FIELDS[flag]] = value;
|
|
53
|
+
return inline === undefined ? 2 : 1;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function validate(options) {
|
|
57
|
+
if (
|
|
58
|
+
!Object.values(FIELDS).some(
|
|
59
|
+
(field) => options[field] !== null && options[field] !== undefined
|
|
60
|
+
)
|
|
61
|
+
) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (
|
|
65
|
+
!options.resume &&
|
|
66
|
+
!(options.isolatedStack || [options.isolated]).includes('docker')
|
|
67
|
+
) {
|
|
68
|
+
throw new Error('Resource options require Docker isolation or --resume');
|
|
69
|
+
}
|
|
70
|
+
if (options.memorySwap && !options.memory && !options.resume) {
|
|
71
|
+
throw new Error('--memory-swap requires --memory');
|
|
72
|
+
}
|
|
73
|
+
if (
|
|
74
|
+
options.onKillResumeMemory &&
|
|
75
|
+
!options.onKillResume &&
|
|
76
|
+
!options.recoveryCommand
|
|
77
|
+
) {
|
|
78
|
+
throw new Error(
|
|
79
|
+
'--on-kill-resume-memory requires --on-kill-resume or --recovery-command'
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function readDaemonCapacity(runner) {
|
|
85
|
+
const result = runner(getDockerCommand(), ['info', '--format', '{{json .}}']);
|
|
86
|
+
if (!result?.success) {
|
|
87
|
+
throw new Error('Cannot read resource capacity from Docker daemon');
|
|
88
|
+
}
|
|
89
|
+
const info = JSON.parse(result.stdout);
|
|
90
|
+
if (!(info.MemTotal > 0) || !(info.NCPU > 0)) {
|
|
91
|
+
throw new Error('Docker daemon did not report MemTotal/NCPU');
|
|
92
|
+
}
|
|
93
|
+
return { memory: info.MemTotal, cpus: info.NCPU };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function resolveResourceOptions(options, runner, random = Math.random) {
|
|
97
|
+
const specs = {};
|
|
98
|
+
for (const field of ['memory', 'memorySwap', 'cpus']) {
|
|
99
|
+
if (options[field] !== null && options[field] !== undefined) {
|
|
100
|
+
specs[field] = parseResourceSpec(options[field], field === 'cpus');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const capacity = Object.values(specs).some((s) => s.percent)
|
|
104
|
+
? readDaemonCapacity(runner)
|
|
105
|
+
: null;
|
|
106
|
+
const resolved = {};
|
|
107
|
+
const limits = normalizeResourceLimits(options.resourceLimits);
|
|
108
|
+
for (const [field, spec] of Object.entries(specs)) {
|
|
109
|
+
const amount = spec.percent
|
|
110
|
+
? (capacity[field === 'cpus' ? 'cpus' : 'memory'] *
|
|
111
|
+
(spec.min + (spec.max - spec.min) * random())) /
|
|
112
|
+
100
|
|
113
|
+
: spec.amount;
|
|
114
|
+
resolved[field] =
|
|
115
|
+
field === 'cpus' ? Math.floor(amount * 1e9) / 1e9 : Math.floor(amount);
|
|
116
|
+
if (resolved[field] < (field === 'cpus' ? 0.01 : 6 * 1024 * 1024)) {
|
|
117
|
+
throw new Error(`Resolved ${field} is below Docker's minimum`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
resolved.memory !== null &&
|
|
122
|
+
resolved.memory !== undefined &&
|
|
123
|
+
(resolved.memorySwap === null || resolved.memorySwap === undefined)
|
|
124
|
+
) {
|
|
125
|
+
resolved.memorySwap = resolved.memory;
|
|
126
|
+
}
|
|
127
|
+
const existingMemory = limits
|
|
128
|
+
.find((flag) => flag.startsWith('--memory='))
|
|
129
|
+
?.slice(9);
|
|
130
|
+
const memory =
|
|
131
|
+
resolved.memory ??
|
|
132
|
+
(existingMemory ? parseResourceSpec(existingMemory).amount : null);
|
|
133
|
+
if (
|
|
134
|
+
resolved.memorySwap !== null &&
|
|
135
|
+
resolved.memorySwap !== undefined &&
|
|
136
|
+
(memory === null || memory === undefined || resolved.memorySwap < memory)
|
|
137
|
+
) {
|
|
138
|
+
throw new Error('memory-swap must be at least memory');
|
|
139
|
+
}
|
|
140
|
+
for (const [field, amount] of Object.entries(resolved)) {
|
|
141
|
+
const flag = field === 'memorySwap' ? '--memory-swap' : `--${field}`;
|
|
142
|
+
for (let i = limits.length - 1; i >= 0; i--) {
|
|
143
|
+
if (
|
|
144
|
+
limits[i].startsWith(`${flag}=`) ||
|
|
145
|
+
(field === 'cpus' && /^--cpu-(quota|period)=/.test(limits[i]))
|
|
146
|
+
) {
|
|
147
|
+
limits.splice(i, 1);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
limits.push(`${flag}=${amount}`);
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
resourceLimits: limits,
|
|
154
|
+
resolvedLimits: resolved,
|
|
155
|
+
resourceLimitSpecs: Object.fromEntries(
|
|
156
|
+
Object.keys(specs).map((field) => [field, options[field]])
|
|
157
|
+
),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function limitsLogLine(options) {
|
|
162
|
+
const values = options.resolvedLimits || {};
|
|
163
|
+
return Object.keys(values).length
|
|
164
|
+
? `Limits: ${Object.entries(values)
|
|
165
|
+
.map(
|
|
166
|
+
([field, value]) =>
|
|
167
|
+
`${field}=${value}${options.resourceLimitSpecs?.[field] ? ` (${options.resourceLimitSpecs[field]} -> ${value})` : ''}`
|
|
168
|
+
)
|
|
169
|
+
.join(' ')}`
|
|
170
|
+
: null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = {
|
|
174
|
+
parse,
|
|
175
|
+
validate,
|
|
176
|
+
parseResourceSpec,
|
|
177
|
+
readDaemonCapacity,
|
|
178
|
+
resolveResourceOptions,
|
|
179
|
+
limitsLogLine,
|
|
180
|
+
};
|
|
@@ -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)
|