start-command 0.39.0 → 0.39.2

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.
@@ -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
+ });