start-command 0.39.0 → 0.39.1
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 +8 -0
- package/package.json +1 -1
- package/src/lib/cgroup-memory.js +35 -11
- package/src/lib/screen-isolation.js +72 -42
- package/test/isolation.js +32 -0
- package/test/regression-247.js +140 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# start-command
|
|
2
2
|
|
|
3
|
+
## 0.39.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 55c57b0: Stop the Docker cgroup sampler when the counters can never be read (#247). On hosts whose containers have no `memory.events` (the `memory` controller is not delegated, e.g. a root `cgroup.subtree_control` of `cpuset cpu pids`) and whose local cgroup is hidden, the sampler started one `docker exec` and one `docker inspect -f {{.State.Running}}` every interval for the whole life of the task without ever recording a sample. Exit 3 (unreadable counters) from the long-lived stream or from the per-round read now records the reason and stops sampling, as a missing `sh` (exit 126/127) already did; the post-mortem reports `Memory: unavailable (local cgroup unavailable; Docker exec exit=3: private cgroup v2 counters unreadable)` instead of the last round's error. Other failed rounds are still retried, but each one doubles the wait, up to 60 s (or the interval, if longer); a sample restores the interval. The completion watcher (`docker wait`) still finishes the run.
|
|
8
|
+
|
|
9
|
+
Attached `--isolated screen` runs no longer report a failed command as exit 0 when `screen -dmS` returns before `screen -ls` lists the new session. A session missing from `screen -ls` now counts as finished only once its exit-code file is written, or after 20 polls (about 2 s) without the session.
|
|
10
|
+
|
|
3
11
|
## 0.39.0
|
|
4
12
|
|
|
5
13
|
### Minor Changes
|
package/package.json
CHANGED
package/src/lib/cgroup-memory.js
CHANGED
|
@@ -30,6 +30,15 @@ const { shellQuote } = require('./isolation-log-utils');
|
|
|
30
30
|
/** Default seconds between samples; the watcher environment may override it. */
|
|
31
31
|
const CGROUP_SAMPLE_INTERVAL_SECONDS = 5;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Longest wait between rounds that keep failing (issue #247), unless the
|
|
35
|
+
* interval itself is longer. Each round without a sample doubles the wait.
|
|
36
|
+
*/
|
|
37
|
+
const CGROUP_SAMPLE_BACKOFF_MAX_SECONDS = 60;
|
|
38
|
+
|
|
39
|
+
/** Exit status of both in-container reads for unreadable counters. */
|
|
40
|
+
const CGROUP_UNREADABLE_EXIT = 3;
|
|
41
|
+
|
|
33
42
|
/** Shell variables used by the sampler snippets. */
|
|
34
43
|
const CGROUP_SHELL_VARS = {
|
|
35
44
|
/** Last sample: `<memory.max> <memory.peak> <oom> <oom_kill>`. */
|
|
@@ -80,17 +89,18 @@ function buildCgroupFunctionsSnippet() {
|
|
|
80
89
|
* (`docker exec -i`), so it runs only shell builtins: the task's own CPU
|
|
81
90
|
* limit pays for no `cat`, `sed` or `sleep` processes. Stdin reaches EOF when
|
|
82
91
|
* the exec's client is gone, so the loop never outlives the sampler; dockerd
|
|
83
|
-
* would accept its output even then. Exit 3: unreadable counters
|
|
92
|
+
* would accept its output even then. Exit 3: unreadable counters (no
|
|
93
|
+
* delegated `memory` controller), which no later round can change.
|
|
84
94
|
* @returns {string} Shell script for `sh -c`
|
|
85
95
|
*/
|
|
86
96
|
function buildCgroupStreamScript() {
|
|
87
97
|
return [
|
|
88
98
|
'while read -r __scx_tick; do __scx_oom=; __scx_kill=; __scx_max=-; __scx_peak=-',
|
|
89
|
-
|
|
99
|
+
`[ -r "$1/memory.events" ] || exit ${CGROUP_UNREADABLE_EXIT}`,
|
|
90
100
|
'while read -r __scx_key __scx_value; do case "$__scx_key" in ' +
|
|
91
101
|
'oom) __scx_oom=$__scx_value;; oom_kill) __scx_kill=$__scx_value;; esac; ' +
|
|
92
102
|
'done < "$1/memory.events"',
|
|
93
|
-
|
|
103
|
+
`case "$__scx_oom:$__scx_kill" in *[!0-9:]*|:*|*:) exit ${CGROUP_UNREADABLE_EXIT};; esac`,
|
|
94
104
|
'[ ! -r "$1/memory.max" ] || read -r __scx_max < "$1/memory.max"',
|
|
95
105
|
'[ ! -r "$1/memory.peak" ] || read -r __scx_peak < "$1/memory.peak"',
|
|
96
106
|
'printf \'%s %s %s %s %s\\n\' "${__scx_max:--}" "${__scx_peak:--}" ' +
|
|
@@ -116,15 +126,21 @@ const CGROUP_SAMPLING_OFF_REASON =
|
|
|
116
126
|
* successful read proves the container runs, so Docker is asked for liveness
|
|
117
127
|
* only after a failed round or when the stream ends. Without a usable stream it falls back
|
|
118
128
|
* to one `docker exec` per round; without `sh` in the image (exit 126/127) or
|
|
119
|
-
* without any readable cgroup
|
|
120
|
-
*
|
|
129
|
+
* without any readable cgroup (exit 3 from either exec, issue #247) it
|
|
130
|
+
* records the reason and stops. Other failed rounds are retried, each one
|
|
131
|
+
* doubling the wait up to CGROUP_SAMPLE_BACKOFF_MAX_SECONDS; a sample restores
|
|
132
|
+
* the interval. `START_COMMAND_CGROUP_SAMPLE_INTERVAL_SECONDS=off` disables
|
|
133
|
+
* sampling.
|
|
121
134
|
* @param {string} containerName - Container to sample
|
|
122
135
|
* @returns {string} Shell command
|
|
123
136
|
*/
|
|
124
137
|
function buildCgroupSamplerStartSnippet(containerName) {
|
|
125
138
|
const v = CGROUP_SHELL_VARS;
|
|
126
139
|
const name = shellQuote(containerName);
|
|
127
|
-
const remoteRead =
|
|
140
|
+
const remoteRead = `__start_command_cgroup_read /sys/fs/cgroup || exit ${CGROUP_UNREADABLE_EXIT}`;
|
|
141
|
+
// Exit statuses no later round can change: no `sh`, or no counters.
|
|
142
|
+
const permanent = `${CGROUP_UNREADABLE_EXIT}|126|127`;
|
|
143
|
+
const backoff = CGROUP_SAMPLE_BACKOFF_MAX_SECONDS;
|
|
128
144
|
const unavailable = (detail) =>
|
|
129
145
|
`printf '%s\\n' "local cgroup unavailable; ${detail}" > "$${v.file}.reason"`;
|
|
130
146
|
const execReason = unavailable(
|
|
@@ -155,7 +171,8 @@ function buildCgroupSamplerStartSnippet(containerName) {
|
|
|
155
171
|
`__scs_dir=$(__start_command_cgroup_dir ${name}); ` +
|
|
156
172
|
// The namespace is fixed at container creation, including when reads fail.
|
|
157
173
|
`__scs_ns=$(docker inspect -f '{{.HostConfig.CgroupnsMode}}' ${name} 2>/dev/null); __scs_stream=1; __scs_reader=''; ` +
|
|
158
|
-
`
|
|
174
|
+
`__scs_delay=$__scs_interval; ` +
|
|
175
|
+
`while :; do __scs_ok=''; __scs_sampled=''; if [ -n "$__scs_dir" ]; then ` +
|
|
159
176
|
'__scs_line=$(__start_command_cgroup_read "$__scs_dir") && __scs_record "$__scs_line" && __scs_ok=1; ' +
|
|
160
177
|
`elif [ "$__scs_ns" != private ]; then ${unavailable('container cgroup namespace=${__scs_ns:-unavailable}, private required')}; break; ` +
|
|
161
178
|
`elif [ "$__scs_stream" = 1 ]; then ` +
|
|
@@ -171,15 +188,20 @@ function buildCgroupSamplerStartSnippet(containerName) {
|
|
|
171
188
|
`[ -z "$__scs_got" ] || exit 0; ${execReason}; ` +
|
|
172
189
|
`case "$__scs_exit" in ''|0|*[!0-9]*) exit 1;; esac; exit "$__scs_exit"; } & ` +
|
|
173
190
|
`__scs_reader=$!; wait "$__scs_reader"; __scs_exit=$?; trap - TERM; __scs_reader=''; ` +
|
|
174
|
-
// No shell in the image: a per-round exec cannot
|
|
175
|
-
|
|
191
|
+
// No shell in the image or no counters: a per-round exec cannot
|
|
192
|
+
// succeed either.
|
|
193
|
+
`case "$__scs_exit" in 0) __scs_sampled=1;; ${permanent}) break;; *) __scs_stream=0;; esac; ` +
|
|
176
194
|
`else __scs_remote=$(docker exec ${name} sh -c ${shellQuote(`${buildCgroupFunctionsSnippet()}; ${remoteRead}`)} 2>&1); __scs_exit=$?; ` +
|
|
177
195
|
'if [ "$__scs_exit" = 0 ] && __scs_record "$__scs_remote"; then __scs_ok=1; ' +
|
|
178
|
-
`else __scs_detail=$(printf "%s" "$__scs_remote" | sed -n "1p" | cut -c 1-240); ${execReason};
|
|
196
|
+
`else __scs_detail=$(printf "%s" "$__scs_remote" | sed -n "1p" | cut -c 1-240); ${execReason}; ` +
|
|
197
|
+
`case "$__scs_exit" in ${permanent}) break;; esac; fi; fi; ` +
|
|
179
198
|
// A successful read needs no Docker liveness probe, and the completion
|
|
180
199
|
// watcher still stops us after docker wait.
|
|
181
200
|
`if [ -z "$__scs_ok" ] && [ "$(docker inspect -f '{{.State.Running}}' ${name} 2>/dev/null)" = false ]; then break; fi; ` +
|
|
182
|
-
|
|
201
|
+
// Back off while rounds keep failing, never below the interval.
|
|
202
|
+
`if [ -n "$__scs_ok$__scs_sampled" ]; then __scs_delay=$__scs_interval; else __scs_delay=$((__scs_delay*2)); ` +
|
|
203
|
+
`[ "$__scs_delay" -le ${backoff} ] || __scs_delay=${backoff}; [ "$__scs_delay" -ge "$__scs_interval" ] || __scs_delay=$__scs_interval; fi; ` +
|
|
204
|
+
`sleep "$__scs_delay"; done ) >/dev/null 2>&1 & ${v.sampler}=$!`,
|
|
183
205
|
].join('; ');
|
|
184
206
|
}
|
|
185
207
|
|
|
@@ -373,10 +395,12 @@ function formatCgroupMemory(memory) {
|
|
|
373
395
|
}
|
|
374
396
|
|
|
375
397
|
module.exports = {
|
|
398
|
+
CGROUP_SAMPLE_BACKOFF_MAX_SECONDS,
|
|
376
399
|
CGROUP_SAMPLE_INTERVAL_SECONDS,
|
|
377
400
|
CGROUP_SAMPLING_OFF_REASON,
|
|
378
401
|
CGROUP_SHELL_VARS,
|
|
379
402
|
CGROUP_STREAM_NAME,
|
|
403
|
+
CGROUP_UNREADABLE_EXIT,
|
|
380
404
|
OOM_SCOPE,
|
|
381
405
|
OOM_SCOPE_NOTES,
|
|
382
406
|
buildCgroupFunctionsSnippet,
|
|
@@ -100,6 +100,28 @@ function supportsLogfileOption() {
|
|
|
100
100
|
return version.patch >= 1;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* Polls without the session in `screen -ls` and without an exit code before
|
|
105
|
+
* the session counts as gone. `screen -dmS` can return before the session is
|
|
106
|
+
* listed, so a missing session alone is no proof that the command finished;
|
|
107
|
+
* its exit code is written just before the session ends.
|
|
108
|
+
*/
|
|
109
|
+
const SCREEN_UNLISTED_GRACE_POLLS = 20;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Whether an attached screen run is over.
|
|
113
|
+
* @param {boolean} listed - The session is in `screen -ls`
|
|
114
|
+
* @param {boolean} exitCodeReady - The exit-code sidecar has content (or no
|
|
115
|
+
* sidecar is expected, as for a bare shell)
|
|
116
|
+
* @param {number} unlistedPolls - Consecutive polls without the session
|
|
117
|
+
* @returns {boolean}
|
|
118
|
+
*/
|
|
119
|
+
function isScreenRunFinished(listed, exitCodeReady, unlistedPolls) {
|
|
120
|
+
return (
|
|
121
|
+
!listed && (exitCodeReady || unlistedPolls >= SCREEN_UNLISTED_GRACE_POLLS)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
103
125
|
/**
|
|
104
126
|
* Run command in GNU Screen using detached mode with log capture.
|
|
105
127
|
*
|
|
@@ -346,58 +368,64 @@ function runScreenWithLogCapture(
|
|
|
346
368
|
const checkInterval = 100; // ms
|
|
347
369
|
const maxWait = 300000; // 5 minutes max
|
|
348
370
|
let waited = 0;
|
|
371
|
+
let unlistedPolls = 0;
|
|
372
|
+
|
|
373
|
+
const exitCodeReady = () => {
|
|
374
|
+
if (isBareShell) {
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
try {
|
|
378
|
+
return fs.readFileSync(exitCodeFile, 'utf8').trim() !== '';
|
|
379
|
+
} catch {
|
|
380
|
+
return false;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const finish = () => {
|
|
385
|
+
readAndDisplayOutput().then((output) => {
|
|
386
|
+
const exitCode = readExitCode();
|
|
387
|
+
cleanupTempFiles();
|
|
388
|
+
resolve({
|
|
389
|
+
success: exitCode === 0,
|
|
390
|
+
sessionName,
|
|
391
|
+
message: `Screen session "${sessionName}" exited with code ${exitCode}`,
|
|
392
|
+
exitCode,
|
|
393
|
+
output,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
};
|
|
349
397
|
|
|
350
398
|
const checkCompletion = () => {
|
|
399
|
+
let listed;
|
|
351
400
|
try {
|
|
352
401
|
// Check if session still exists
|
|
353
|
-
|
|
402
|
+
listed = execSync('screen -ls', {
|
|
354
403
|
encoding: 'utf8',
|
|
355
404
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
if (!sessions.includes(sessionName)) {
|
|
359
|
-
// Session ended, read output and exit code
|
|
360
|
-
readAndDisplayOutput().then((output) => {
|
|
361
|
-
const exitCode = readExitCode();
|
|
362
|
-
cleanupTempFiles();
|
|
363
|
-
resolve({
|
|
364
|
-
success: exitCode === 0,
|
|
365
|
-
sessionName,
|
|
366
|
-
message: `Screen session "${sessionName}" exited with code ${exitCode}`,
|
|
367
|
-
exitCode,
|
|
368
|
-
output,
|
|
369
|
-
});
|
|
370
|
-
});
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
waited += checkInterval;
|
|
375
|
-
if (waited >= maxWait) {
|
|
376
|
-
cleanupTempFiles();
|
|
377
|
-
resolve({
|
|
378
|
-
success: false,
|
|
379
|
-
sessionName,
|
|
380
|
-
message: `Screen session "${sessionName}" timed out after ${maxWait / 1000} seconds`,
|
|
381
|
-
exitCode: 1,
|
|
382
|
-
});
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
setTimeout(checkCompletion, checkInterval);
|
|
405
|
+
}).includes(sessionName);
|
|
387
406
|
} catch {
|
|
388
407
|
// screen -ls failed, session probably ended
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
408
|
+
listed = false;
|
|
409
|
+
}
|
|
410
|
+
unlistedPolls = listed ? 0 : unlistedPolls + 1;
|
|
411
|
+
if (isScreenRunFinished(listed, exitCodeReady(), unlistedPolls)) {
|
|
412
|
+
finish();
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
waited += checkInterval;
|
|
417
|
+
if (waited >= maxWait) {
|
|
418
|
+
cleanupTempFiles();
|
|
419
|
+
resolve({
|
|
420
|
+
success: false,
|
|
421
|
+
sessionName,
|
|
422
|
+
message: `Screen session "${sessionName}" timed out after ${maxWait / 1000} seconds`,
|
|
423
|
+
exitCode: 1,
|
|
399
424
|
});
|
|
425
|
+
return;
|
|
400
426
|
}
|
|
427
|
+
|
|
428
|
+
setTimeout(checkCompletion, checkInterval);
|
|
401
429
|
};
|
|
402
430
|
|
|
403
431
|
// Start checking after a brief delay
|
|
@@ -522,7 +550,9 @@ function resetScreenVersionCache() {
|
|
|
522
550
|
}
|
|
523
551
|
|
|
524
552
|
module.exports = {
|
|
553
|
+
SCREEN_UNLISTED_GRACE_POLLS,
|
|
525
554
|
getScreenVersion,
|
|
555
|
+
isScreenRunFinished,
|
|
526
556
|
supportsLogfileOption,
|
|
527
557
|
runScreenWithLogCapture,
|
|
528
558
|
startDetachedScreenWithLogCapture,
|
package/test/isolation.js
CHANGED
|
@@ -812,3 +812,35 @@ describe('isInteractiveShellCommand (issue #84)', () => {
|
|
|
812
812
|
assert.strictEqual(isInteractiveShellCommand(' '), false);
|
|
813
813
|
});
|
|
814
814
|
});
|
|
815
|
+
|
|
816
|
+
describe('isScreenRunFinished', () => {
|
|
817
|
+
// `screen -dmS` can return before `screen -ls` lists the session; a failed
|
|
818
|
+
// command then read as exit 0 (Rust CI run 39873000384).
|
|
819
|
+
const {
|
|
820
|
+
SCREEN_UNLISTED_GRACE_POLLS,
|
|
821
|
+
isScreenRunFinished,
|
|
822
|
+
} = require('../src/lib/screen-isolation');
|
|
823
|
+
|
|
824
|
+
it('waits while the session is listed', () => {
|
|
825
|
+
assert.strictEqual(isScreenRunFinished(true, true, 0), false);
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
it('finishes once the session is gone and its exit code is written', () => {
|
|
829
|
+
assert.strictEqual(isScreenRunFinished(false, true, 1), true);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
it('keeps polling an unlisted session without an exit code', () => {
|
|
833
|
+
assert.strictEqual(isScreenRunFinished(false, false, 1), false);
|
|
834
|
+
assert.strictEqual(
|
|
835
|
+
isScreenRunFinished(false, false, SCREEN_UNLISTED_GRACE_POLLS - 1),
|
|
836
|
+
false
|
|
837
|
+
);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('gives up after the grace polls', () => {
|
|
841
|
+
assert.strictEqual(
|
|
842
|
+
isScreenRunFinished(false, false, SCREEN_UNLISTED_GRACE_POLLS),
|
|
843
|
+
true
|
|
844
|
+
);
|
|
845
|
+
});
|
|
846
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regression tests for issue #247: with cgroup counters unreadable on both
|
|
3
|
+
* sides (no local cgroup, no `memory.events` in the container because the
|
|
4
|
+
* `memory` controller is not delegated), the sampler kept starting one
|
|
5
|
+
* `docker exec` and one `docker inspect -f {{.State.Running}}` every interval
|
|
6
|
+
* for the whole life of the task, although no round could ever record a
|
|
7
|
+
* sample.
|
|
8
|
+
*
|
|
9
|
+
* Now exit 3 (unreadable counters) from the stream or from the per-round read
|
|
10
|
+
* records the reason and stops, as exit 126/127 already did, and other failed
|
|
11
|
+
* rounds back off exponentially. The no-memory fixture modes run the real
|
|
12
|
+
* in-container scripts against a cgroup directory without `memory.events`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const { describe, it, expect } = require('bun:test');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const os = require('os');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { spawnSync } = require('child_process');
|
|
20
|
+
const {
|
|
21
|
+
CGROUP_SAMPLE_BACKOFF_MAX_SECONDS,
|
|
22
|
+
CGROUP_UNREADABLE_EXIT,
|
|
23
|
+
buildCgroupSamplerStartSnippet,
|
|
24
|
+
buildCgroupSamplerStopSnippet,
|
|
25
|
+
buildCgroupMemoryLogSnippet,
|
|
26
|
+
} = require('../src/lib/cgroup-memory');
|
|
27
|
+
|
|
28
|
+
/** Runs the shared fixture of issue #238 and returns its trace. */
|
|
29
|
+
function sample(mode, interval) {
|
|
30
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sampler-247-'));
|
|
31
|
+
try {
|
|
32
|
+
const script = path.join(dir, 'sample.sh');
|
|
33
|
+
fs.writeFileSync(
|
|
34
|
+
script,
|
|
35
|
+
[
|
|
36
|
+
buildCgroupSamplerStartSnippet("task '238'"),
|
|
37
|
+
'wait "$__start_command_cgroup_sampler"',
|
|
38
|
+
buildCgroupSamplerStopSnippet(),
|
|
39
|
+
'printf "sample=%s\\n" "$__start_command_cgroup"',
|
|
40
|
+
buildCgroupMemoryLogSnippet('"$TMPDIR/memory.log"'),
|
|
41
|
+
'cat "$TMPDIR/memory.log" "$TMPDIR/calls"',
|
|
42
|
+
'sed "s/^/sleep=/" "$TMPDIR/sleeps"',
|
|
43
|
+
'for file in "$TMPDIR"/start-command-cgroup*; do [ ! -e "$file" ] || exit 1; done',
|
|
44
|
+
].join('; ')
|
|
45
|
+
);
|
|
46
|
+
const env = { ...process.env };
|
|
47
|
+
delete env.START_COMMAND_CGROUP_SAMPLE_INTERVAL_SECONDS;
|
|
48
|
+
if (interval !== undefined) {
|
|
49
|
+
env.START_COMMAND_CGROUP_SAMPLE_INTERVAL_SECONDS = interval;
|
|
50
|
+
}
|
|
51
|
+
const result = spawnSync(
|
|
52
|
+
'/bin/sh',
|
|
53
|
+
[
|
|
54
|
+
path.resolve(
|
|
55
|
+
__dirname,
|
|
56
|
+
'../../experiments/issue-238-cgroup-polling.sh'
|
|
57
|
+
),
|
|
58
|
+
mode,
|
|
59
|
+
script,
|
|
60
|
+
],
|
|
61
|
+
{ encoding: 'utf8', env, timeout: 30000 }
|
|
62
|
+
);
|
|
63
|
+
expect(result.status).toBe(0);
|
|
64
|
+
return result.stdout;
|
|
65
|
+
} finally {
|
|
66
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const count = (output, text) =>
|
|
71
|
+
output.split('\n').filter((line) => line.includes(text)).length;
|
|
72
|
+
const sleeps = (output) =>
|
|
73
|
+
output
|
|
74
|
+
.split('\n')
|
|
75
|
+
.filter((line) => line.startsWith('sleep='))
|
|
76
|
+
.map((line) => Number(line.slice('sleep='.length)));
|
|
77
|
+
const posix = process.platform === 'win32' ? describe.skip : describe;
|
|
78
|
+
const unreadable = `Memory: unavailable (local cgroup unavailable; Docker exec exit=${CGROUP_UNREADABLE_EXIT}: private cgroup v2 counters unreadable)`;
|
|
79
|
+
|
|
80
|
+
posix('issue #247: unreadable cgroup counters stop the sampler', () => {
|
|
81
|
+
it('stops after the stream exits 3, without per-round execs or liveness probes', () => {
|
|
82
|
+
const output = sample('no-memory');
|
|
83
|
+
expect(output).toContain('sample=\n');
|
|
84
|
+
expect(output).toContain(unreadable);
|
|
85
|
+
expect(count(output, "exec -i task '238' sh -c")).toBe(1);
|
|
86
|
+
expect(count(output, "task '238' sh -c")).toBe(1);
|
|
87
|
+
expect(count(output, '{{.State.Running}}')).toBe(0);
|
|
88
|
+
expect(sleeps(output)).toEqual([]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('stops after a per-round read exits 3', () => {
|
|
92
|
+
// The stream fails for a transient reason first, so one per-round exec
|
|
93
|
+
// runs; its real read finds no memory.events.
|
|
94
|
+
const output = sample('no-memory-outage');
|
|
95
|
+
expect(output).toContain(unreadable);
|
|
96
|
+
expect(count(output, "exec -i task '238' sh -c")).toBe(1);
|
|
97
|
+
expect(count(output, "exec task '238' sh -c")).toBe(1);
|
|
98
|
+
expect(count(output, '{{.State.Running}}')).toBe(1);
|
|
99
|
+
expect(sleeps(output)).toHaveLength(1);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('stops after a per-round exec finds no shell', () => {
|
|
103
|
+
const output = sample('no-shell-outage');
|
|
104
|
+
expect(output).toContain('Docker exec exit=127: sh: not found');
|
|
105
|
+
expect(count(output, "exec task '238' sh -c")).toBe(1);
|
|
106
|
+
expect(count(output, '{{.State.Running}}')).toBe(1);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('records the per-round reason as exit 3, not as an exec failure', () => {
|
|
110
|
+
const snippet = buildCgroupSamplerStartSnippet('task');
|
|
111
|
+
expect(snippet).toContain(
|
|
112
|
+
`__start_command_cgroup_read /sys/fs/cgroup || exit ${CGROUP_UNREADABLE_EXIT}`
|
|
113
|
+
);
|
|
114
|
+
expect(snippet).toContain('3|126|127) break;;');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
posix('issue #247: failed rounds back off exponentially', () => {
|
|
119
|
+
it('doubles the wait after each round without a sample', () => {
|
|
120
|
+
// Round 0 streams a sample; the stream, then per-round execs, fail.
|
|
121
|
+
expect(sleeps(sample('outage'))).toEqual([5, 10, 20]);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it(`caps the wait at ${CGROUP_SAMPLE_BACKOFF_MAX_SECONDS} seconds`, () => {
|
|
125
|
+
expect(sleeps(sample('outage', '40'))).toEqual([
|
|
126
|
+
40,
|
|
127
|
+
CGROUP_SAMPLE_BACKOFF_MAX_SECONDS,
|
|
128
|
+
CGROUP_SAMPLE_BACKOFF_MAX_SECONDS,
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('never waits less than a longer interval', () => {
|
|
133
|
+
expect(sleeps(sample('outage', '100'))).toEqual([100, 100, 100]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('keeps the interval while samples arrive', () => {
|
|
137
|
+
expect(sleeps(sample('remote', '2'))).toEqual([2, 2, 2]);
|
|
138
|
+
expect(sleeps(sample('local'))).toEqual([5, 5, 5]);
|
|
139
|
+
});
|
|
140
|
+
});
|