start-command 0.37.0 → 0.37.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.
- package/CHANGELOG.md +12 -0
- package/package.json +1 -1
- package/src/lib/cpu-penalty-monitor.js +209 -87
- package/src/lib/cpu-penalty.js +30 -1
- package/src/lib/execution-attempt.js +27 -0
- package/src/lib/execution-recovery.js +13 -7
- package/src/lib/exit-evidence.js +62 -14
- package/src/lib/exit-reason.js +7 -0
- package/src/lib/isolation-log-utils.js +7 -2
- package/src/lib/resume-resources.js +15 -3
- package/src/lib/status-formatter.js +10 -1
- package/src/lib/store-lock.js +54 -3
- package/test/regression-191.js +18 -1
- package/test/regression-207.js +249 -0
- package/test/regression-209.js +240 -0
- package/test/shared-golden.js +48 -1
|
@@ -240,11 +240,16 @@ const FATAL_MARKER_TAIL_BYTES = 64 * 1024;
|
|
|
240
240
|
* @param {number} [bytes] - Maximum number of trailing bytes to read
|
|
241
241
|
* @returns {string|null} Tail content, or null when the file cannot be read
|
|
242
242
|
*/
|
|
243
|
-
function readLogTail(
|
|
243
|
+
function readLogTail(
|
|
244
|
+
logPath,
|
|
245
|
+
bytes = LOG_TAIL_BYTES,
|
|
246
|
+
offset = 0,
|
|
247
|
+
end = Infinity
|
|
248
|
+
) {
|
|
244
249
|
let fd;
|
|
245
250
|
try {
|
|
246
251
|
fd = fs.openSync(logPath, 'r');
|
|
247
|
-
const size = fs.fstatSync(fd).size;
|
|
252
|
+
const size = Math.min(fs.fstatSync(fd).size, end);
|
|
248
253
|
const length = Math.min(Math.max(0, size - offset), bytes);
|
|
249
254
|
const buffer = Buffer.alloc(length);
|
|
250
255
|
fs.readSync(fd, buffer, 0, length, size - length);
|
|
@@ -7,13 +7,23 @@ const {
|
|
|
7
7
|
const { resolveResourceOptions } = require('./docker-resource-options');
|
|
8
8
|
|
|
9
9
|
// Temporary penalty quotas must never become a resumed execution's base quota.
|
|
10
|
-
function restoreBaseCpu(limits, options) {
|
|
10
|
+
function restoreBaseCpu(limits, options, saved = null, inspected = false) {
|
|
11
11
|
if (!options.cpuPenaltyConfig) {
|
|
12
12
|
return limits;
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
if (
|
|
15
|
+
saved &&
|
|
16
|
+
inspected &&
|
|
17
|
+
!(saved.phase === 'penalized' && cpuCount(limits) === saved.limitCpus)
|
|
18
|
+
) {
|
|
19
|
+
return limits;
|
|
20
|
+
}
|
|
21
|
+
let base = normalizeResourceLimits(
|
|
15
22
|
options.baseResourceLimits || options.resourceLimits
|
|
16
23
|
);
|
|
24
|
+
if (saved && Object.hasOwn(saved, 'baseCpus')) {
|
|
25
|
+
base = saved.baseCpus > 0 ? withCpuLimit(limits, saved.baseCpus) : [];
|
|
26
|
+
}
|
|
17
27
|
return [
|
|
18
28
|
...limits.filter((s) => !/^--(?:cpus|cpu-quota|cpu-period)=/.test(s)),
|
|
19
29
|
...base.filter((s) => /^--(?:cpus|cpu-quota|cpu-period)=/.test(s)),
|
|
@@ -38,7 +48,9 @@ function prepareResumeResources(record, probe, overrides, runner, random) {
|
|
|
38
48
|
: null;
|
|
39
49
|
const base = restoreBaseCpu(
|
|
40
50
|
normalizeResourceLimits(live || options.resourceLimits),
|
|
41
|
-
options
|
|
51
|
+
options,
|
|
52
|
+
require('./cpu-penalty-monitor').readState(record),
|
|
53
|
+
live !== null
|
|
42
54
|
);
|
|
43
55
|
const oldMemory = base.find((s) => s.startsWith('--memory='))?.slice(9);
|
|
44
56
|
const result = resolveResourceOptions(
|
|
@@ -500,7 +500,8 @@ function attachMemoryExhaustion(record, logTail) {
|
|
|
500
500
|
if (!memory) {
|
|
501
501
|
if (
|
|
502
502
|
record.memoryExhaustedReason?.startsWith('Docker reported') ||
|
|
503
|
-
record.memoryExhaustedReason?.startsWith('cgroup memory.events')
|
|
503
|
+
record.memoryExhaustedReason?.startsWith('cgroup memory.events') ||
|
|
504
|
+
record.memoryExhaustedReason?.startsWith('Exit-time evidence')
|
|
504
505
|
) {
|
|
505
506
|
const cleared = Object.assign(
|
|
506
507
|
Object.create(Object.getPrototypeOf(record)),
|
|
@@ -558,6 +559,14 @@ function enrichDetachedStatus(record) {
|
|
|
558
559
|
Object.create(Object.getPrototypeOf(enriched)),
|
|
559
560
|
enriched
|
|
560
561
|
);
|
|
562
|
+
if (record.attemptHistory) {
|
|
563
|
+
copy.attemptHistory = require('./execution-attempt').enrichAttemptHistory(
|
|
564
|
+
record,
|
|
565
|
+
(attempt, tail) =>
|
|
566
|
+
attachMemoryExhaustion(attachExitReason(attempt, tail), tail),
|
|
567
|
+
FATAL_MARKER_TAIL_BYTES
|
|
568
|
+
);
|
|
569
|
+
}
|
|
561
570
|
return readAttemptActivity(copy);
|
|
562
571
|
}
|
|
563
572
|
|
package/src/lib/store-lock.js
CHANGED
|
@@ -7,11 +7,13 @@ const LOCK_STALE_MS = 60000;
|
|
|
7
7
|
const MALFORMED_LOCK_GRACE_MS = 3000;
|
|
8
8
|
|
|
9
9
|
class LockManager {
|
|
10
|
-
constructor(lockFilePath, filesystem = fs) {
|
|
10
|
+
constructor(lockFilePath, filesystem = fs, monitorOwner = false) {
|
|
11
11
|
this.lockFilePath = lockFilePath;
|
|
12
12
|
this.fs = filesystem;
|
|
13
13
|
this.lockAcquired = false;
|
|
14
14
|
this.identity = null;
|
|
15
|
+
this.token = null;
|
|
16
|
+
this.monitorOwner = monitorOwner;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
acquire(timeout = LOCK_TIMEOUT_MS) {
|
|
@@ -22,7 +24,8 @@ class LockManager {
|
|
|
22
24
|
}
|
|
23
25
|
};
|
|
24
26
|
while (Date.now() - started < timeout) {
|
|
25
|
-
const
|
|
27
|
+
const token = crypto.randomUUID();
|
|
28
|
+
const temp = `${this.lockFilePath}.${process.pid}.${token}`;
|
|
26
29
|
try {
|
|
27
30
|
try {
|
|
28
31
|
const stat = this.fs.statSync(this.lockFilePath);
|
|
@@ -44,6 +47,8 @@ class LockManager {
|
|
|
44
47
|
pid: process.pid,
|
|
45
48
|
timestamp: Date.now(),
|
|
46
49
|
hostname: os.hostname(),
|
|
50
|
+
processStart: this.processInfo(process.pid)?.start,
|
|
51
|
+
token,
|
|
47
52
|
}),
|
|
48
53
|
{ flag: 'wx' }
|
|
49
54
|
);
|
|
@@ -56,6 +61,7 @@ class LockManager {
|
|
|
56
61
|
const identity = this.fs.statSync(temp);
|
|
57
62
|
this.fs.linkSync(temp, this.lockFilePath);
|
|
58
63
|
this.identity = identity;
|
|
64
|
+
this.token = token;
|
|
59
65
|
this.lockAcquired = true;
|
|
60
66
|
debug(`acquired after ${Date.now() - started}ms`);
|
|
61
67
|
return true;
|
|
@@ -84,7 +90,8 @@ class LockManager {
|
|
|
84
90
|
const current = this.fs.statSync(this.lockFilePath);
|
|
85
91
|
if (
|
|
86
92
|
current.dev === this.identity.dev &&
|
|
87
|
-
current.ino === this.identity.ino
|
|
93
|
+
current.ino === this.identity.ino &&
|
|
94
|
+
this.readLockFile()?.token === this.token
|
|
88
95
|
) {
|
|
89
96
|
this.fs.unlinkSync(this.lockFilePath);
|
|
90
97
|
}
|
|
@@ -92,6 +99,7 @@ class LockManager {
|
|
|
92
99
|
/* The lock may already be gone. */
|
|
93
100
|
}
|
|
94
101
|
this.lockAcquired = false;
|
|
102
|
+
this.token = null;
|
|
95
103
|
}
|
|
96
104
|
|
|
97
105
|
readLockFile() {
|
|
@@ -117,6 +125,32 @@ class LockManager {
|
|
|
117
125
|
if (data.hostname === os.hostname()) {
|
|
118
126
|
try {
|
|
119
127
|
process.kill(data.pid, 0);
|
|
128
|
+
const info = this.processInfo(data.pid);
|
|
129
|
+
if (
|
|
130
|
+
info &&
|
|
131
|
+
(['Z', 'X', 'x'].includes(info.state) ||
|
|
132
|
+
(data.processStart && data.processStart !== info.start))
|
|
133
|
+
) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (
|
|
137
|
+
this.monitorOwner &&
|
|
138
|
+
!data.processStart &&
|
|
139
|
+
process.platform === 'linux'
|
|
140
|
+
) {
|
|
141
|
+
try {
|
|
142
|
+
const command = this.fs.readFileSync(
|
|
143
|
+
`/proc/${data.pid}/cmdline`,
|
|
144
|
+
'utf8'
|
|
145
|
+
);
|
|
146
|
+
return (
|
|
147
|
+
!command.includes('cpu-penalty-monitor') &&
|
|
148
|
+
!command.includes('__start-cpu-monitor')
|
|
149
|
+
);
|
|
150
|
+
} catch {
|
|
151
|
+
/* Proc may be restricted; do not evict an unverified live owner. */
|
|
152
|
+
}
|
|
153
|
+
}
|
|
120
154
|
return false;
|
|
121
155
|
} catch (error) {
|
|
122
156
|
return error.code === 'ESRCH';
|
|
@@ -125,6 +159,23 @@ class LockManager {
|
|
|
125
159
|
return Date.now() - data.timestamp > LOCK_STALE_MS;
|
|
126
160
|
}
|
|
127
161
|
|
|
162
|
+
processInfo(pid) {
|
|
163
|
+
if (process.platform !== 'linux') {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
try {
|
|
167
|
+
const stat = this.fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
168
|
+
// comm is parenthesized and can contain spaces and closing parentheses.
|
|
169
|
+
const fields = stat
|
|
170
|
+
.slice(stat.lastIndexOf(')') + 2)
|
|
171
|
+
.trim()
|
|
172
|
+
.split(/\s+/);
|
|
173
|
+
return { state: fields[0], start: fields[19] };
|
|
174
|
+
} catch {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
128
179
|
sleep(ms) {
|
|
129
180
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
130
181
|
}
|
package/test/regression-191.js
CHANGED
|
@@ -97,15 +97,32 @@ it('manual snapshot override applies limits to the new container at creation', a
|
|
|
97
97
|
fs.rmSync(f.dir, { recursive: true, force: true });
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
|
-
for (const [exitCode, oomKilled, sample, change] of [
|
|
100
|
+
for (const [exitCode, oomKilled, sample, change, marker] of [
|
|
101
101
|
[137, true, '67108864 66000000 1 1 100 100', true],
|
|
102
102
|
[137, true, '67108864 66000000 1 1 0 100', false],
|
|
103
103
|
[137, false, '67108864 66000000 0 0 0 100', false],
|
|
104
104
|
[1, true, '67108864 66000000 1 1 100 100', false],
|
|
105
|
+
[
|
|
106
|
+
137,
|
|
107
|
+
true,
|
|
108
|
+
'67108864 66000000 1 1 62 62',
|
|
109
|
+
true,
|
|
110
|
+
'Exit evidence: main-oom (recent Docker oom event)',
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
137,
|
|
114
|
+
true,
|
|
115
|
+
'67108864 66000000 1 1 62 62',
|
|
116
|
+
false,
|
|
117
|
+
'Exit evidence: docker-daemon-restart (Docker service journal)',
|
|
118
|
+
],
|
|
105
119
|
]) {
|
|
106
120
|
it(`automatic memory change requires a qualifying OOM (exit=${exitCode}, fresh=${change})`, () => {
|
|
107
121
|
const f = fixture();
|
|
108
122
|
try {
|
|
123
|
+
if (marker) {
|
|
124
|
+
fs.writeFileSync(f.record.logPath, `${marker}\n`);
|
|
125
|
+
}
|
|
109
126
|
const result = recoverKilledExecution({
|
|
110
127
|
store: f.store,
|
|
111
128
|
executionId: f.record.uuid,
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
const { it, expect } = require('bun:test');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { spawnSync } = require('child_process');
|
|
6
|
+
const { initialState, transitionLog } = require('../src/lib/cpu-penalty');
|
|
7
|
+
const cpu = require('../src/lib/cpu-penalty-monitor');
|
|
8
|
+
|
|
9
|
+
it('restarted monitors retain an observed base absent from launch flags', () => {
|
|
10
|
+
const saved = { ...initialState(4, 0), phase: 'penalized', limitCpus: 2 };
|
|
11
|
+
expect(initialState(null, 100, saved).baseCpus).toBe(4);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('saved observing limits preserve the base after a daemon-clamped lift', () => {
|
|
15
|
+
const saved = { ...initialState(4, 0), limitCpus: 1 };
|
|
16
|
+
const { observeBase } = require('../src/lib/cpu-penalty');
|
|
17
|
+
expect(observeBase(initialState(null, 100, saved), 1, 100).baseCpus).toBe(4);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
for (const mode of [
|
|
21
|
+
'nano',
|
|
22
|
+
'quota',
|
|
23
|
+
'restart',
|
|
24
|
+
'zombie',
|
|
25
|
+
'reused',
|
|
26
|
+
'supervise',
|
|
27
|
+
'publish-fault',
|
|
28
|
+
'graceful',
|
|
29
|
+
'override',
|
|
30
|
+
]) {
|
|
31
|
+
it(`CPU monitor recovers external base and idle release: ${mode}`, () => {
|
|
32
|
+
if (
|
|
33
|
+
process.platform === 'win32' ||
|
|
34
|
+
(['zombie', 'reused'].includes(mode) && process.platform !== 'linux')
|
|
35
|
+
) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-monitor-207-'));
|
|
39
|
+
try {
|
|
40
|
+
const logPath = path.join(dir, 'task.log');
|
|
41
|
+
fs.writeFileSync(logPath, '');
|
|
42
|
+
const record = {
|
|
43
|
+
logPath,
|
|
44
|
+
options: {
|
|
45
|
+
sessionName: 'cpu-task',
|
|
46
|
+
isolated: 'docker',
|
|
47
|
+
cpuPenaltyConfig: {
|
|
48
|
+
cpus: 2,
|
|
49
|
+
trigger: 95,
|
|
50
|
+
release: 65,
|
|
51
|
+
triggerWindowMs: 400,
|
|
52
|
+
releaseWindowMs: 400,
|
|
53
|
+
},
|
|
54
|
+
baseResourceLimits: [],
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
const command = [
|
|
58
|
+
process.execPath,
|
|
59
|
+
require.resolve('../src/lib/cpu-penalty-monitor'),
|
|
60
|
+
'',
|
|
61
|
+
'cpu-task',
|
|
62
|
+
'1',
|
|
63
|
+
JSON.stringify(record),
|
|
64
|
+
];
|
|
65
|
+
if (mode === 'supervise') {
|
|
66
|
+
const script = `${cpu.startSnippet(null, 'cpu-task', 1, { ...record.options, logPath })}; read done; ${cpu.stopSnippet()}`;
|
|
67
|
+
command.splice(0, command.length, 'sh', '-c', script);
|
|
68
|
+
}
|
|
69
|
+
const result = spawnSync(
|
|
70
|
+
'python3',
|
|
71
|
+
[
|
|
72
|
+
path.resolve(__dirname, '../../experiments/issue-207-monitor.py'),
|
|
73
|
+
mode,
|
|
74
|
+
logPath,
|
|
75
|
+
...command,
|
|
76
|
+
],
|
|
77
|
+
{ encoding: 'utf8' }
|
|
78
|
+
);
|
|
79
|
+
expect(result.status, result.stderr).toBe(0);
|
|
80
|
+
const state = cpu.readState(record);
|
|
81
|
+
const expected = mode === 'override' ? 3 : 4;
|
|
82
|
+
expect(state.baseCpus).toBe(expected);
|
|
83
|
+
expect(state.phase).toBe('observing');
|
|
84
|
+
expect(state.limitCpus).toBe(expected);
|
|
85
|
+
if (mode !== 'override') {
|
|
86
|
+
expect(fs.readFileSync(logPath, 'utf8')).toContain(
|
|
87
|
+
'CPU penalty lifted: 4'
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const { ExecutionRecord } = require('../src/lib/execution-store');
|
|
91
|
+
const {
|
|
92
|
+
enrichDetachedStatus,
|
|
93
|
+
formatRecord,
|
|
94
|
+
} = require('../src/lib/status-formatter');
|
|
95
|
+
const statusRecord = new ExecutionRecord({
|
|
96
|
+
...record,
|
|
97
|
+
options: { ...record.options, isolated: undefined },
|
|
98
|
+
});
|
|
99
|
+
const status = JSON.parse(
|
|
100
|
+
formatRecord(enrichDetachedStatus(statusRecord), 'json')
|
|
101
|
+
);
|
|
102
|
+
expect(status.cpuPenalty.baseCpus).toBe(expected);
|
|
103
|
+
expect(status.cpuPenalty.limitCpus).toBe(expected);
|
|
104
|
+
} finally {
|
|
105
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
106
|
+
}
|
|
107
|
+
}, 15000);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
for (const failure of ['inspect', 'stats', 'update', 'publish']) {
|
|
111
|
+
it(`an exception in ${failure} resets coverage and monitoring continues`, async () => {
|
|
112
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-tick-207-'));
|
|
113
|
+
const logPath = path.join(dir, 'task.log');
|
|
114
|
+
const record = {
|
|
115
|
+
logPath,
|
|
116
|
+
options: {
|
|
117
|
+
sessionName: 'cpu-task',
|
|
118
|
+
cpuPenaltyConfig: {
|
|
119
|
+
cpus: 2,
|
|
120
|
+
trigger: 95,
|
|
121
|
+
release: 65,
|
|
122
|
+
triggerWindowMs: 100,
|
|
123
|
+
releaseWindowMs: 100,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
let ticks = 0;
|
|
128
|
+
let injected = false,
|
|
129
|
+
limit = 4,
|
|
130
|
+
lifted = false;
|
|
131
|
+
const filesystem = Object.create(fs);
|
|
132
|
+
filesystem.renameSync = (from, to) => {
|
|
133
|
+
if (
|
|
134
|
+
failure === 'publish' &&
|
|
135
|
+
!injected &&
|
|
136
|
+
JSON.parse(fs.readFileSync(from)).phase === 'penalized'
|
|
137
|
+
) {
|
|
138
|
+
injected = true;
|
|
139
|
+
throw new Error('injected ENOSPC');
|
|
140
|
+
}
|
|
141
|
+
fs.renameSync(from, to);
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
await cpu.monitor('', 'cpu-task', 1, JSON.stringify(record), {
|
|
145
|
+
fs: filesystem,
|
|
146
|
+
run: (_command, args) => {
|
|
147
|
+
if (args[0] === failure && !injected) {
|
|
148
|
+
injected = true;
|
|
149
|
+
throw new Error(`injected ${failure}`);
|
|
150
|
+
}
|
|
151
|
+
const result = (stdout) => ({ success: true, stdout });
|
|
152
|
+
if (args[0] === 'inspect') {
|
|
153
|
+
if (!args[2].includes('HostConfig') && ++ticks > 60) {
|
|
154
|
+
lifted = false;
|
|
155
|
+
return result('false');
|
|
156
|
+
}
|
|
157
|
+
return result(
|
|
158
|
+
args[2].includes('HostConfig')
|
|
159
|
+
? JSON.stringify({ NanoCpus: limit * 1e9 })
|
|
160
|
+
: String(!lifted)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
if (args[0] === 'info') {
|
|
164
|
+
return result('{"NCPU":6}');
|
|
165
|
+
}
|
|
166
|
+
if (args[0] === 'stats') {
|
|
167
|
+
return result(
|
|
168
|
+
JSON.stringify({
|
|
169
|
+
Name: 'cpu-task',
|
|
170
|
+
CPUPerc: limit === 2 ? '20%' : '400%',
|
|
171
|
+
})
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (args[0] === 'update') {
|
|
175
|
+
limit = Number(args[2]);
|
|
176
|
+
lifted = limit === 4;
|
|
177
|
+
return result('');
|
|
178
|
+
}
|
|
179
|
+
throw new Error('unexpected command');
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
expect(injected).toBe(true);
|
|
183
|
+
expect(lifted).toBe(true);
|
|
184
|
+
expect(cpu.readState(record).baseCpus).toBe(4);
|
|
185
|
+
} finally {
|
|
186
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
187
|
+
}
|
|
188
|
+
}, 5000);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
it('stopped resume uses the observed base rather than the temporary quota', () => {
|
|
192
|
+
const { restoreBaseCpu } = require('../src/lib/resume-resources');
|
|
193
|
+
expect(
|
|
194
|
+
restoreBaseCpu(
|
|
195
|
+
['--memory=512m', '--cpus=2'],
|
|
196
|
+
{ cpuPenaltyConfig: {}, baseResourceLimits: [] },
|
|
197
|
+
{ baseCpus: 4 }
|
|
198
|
+
)
|
|
199
|
+
).toEqual(['--memory=512m', '--cpus=4']);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it('stopped resume preserves a later supervisor override instead of the stale cap', () => {
|
|
203
|
+
const { restoreBaseCpu } = require('../src/lib/resume-resources');
|
|
204
|
+
const options = { cpuPenaltyConfig: {}, baseResourceLimits: [] };
|
|
205
|
+
const saved = { phase: 'penalized', baseCpus: 4, limitCpus: 2 };
|
|
206
|
+
expect(restoreBaseCpu(['--cpus=2'], options, saved, true)).toEqual([
|
|
207
|
+
'--cpus=4',
|
|
208
|
+
]);
|
|
209
|
+
expect(restoreBaseCpu(['--cpus=3'], options, saved, true)).toEqual([
|
|
210
|
+
'--cpus=3',
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('transition logs explain the window and phase denominator', () => {
|
|
215
|
+
const config = { cpus: 2, triggerWindowMs: 900000, releaseWindowMs: 400 };
|
|
216
|
+
expect(
|
|
217
|
+
transitionLog({ kind: 'apply', cpus: 2, capacity: 4, average: 100 }, config)
|
|
218
|
+
).toContain('of 4 CPUs for 15m');
|
|
219
|
+
expect(
|
|
220
|
+
transitionLog({ kind: 'lift', cpus: 4, capacity: 4, average: 10 }, config)
|
|
221
|
+
).toContain('of 2 CPUs for 400ms');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('process birth identity reclaims reused PIDs without evicting a live lock', () => {
|
|
225
|
+
if (process.platform !== 'linux') {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
const { LockManager } = require('../src/lib/store-lock');
|
|
229
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'cpu-lock-207-'));
|
|
230
|
+
const file = path.join(dir, 'lock');
|
|
231
|
+
const first = new LockManager(file);
|
|
232
|
+
const replacement = new LockManager(file);
|
|
233
|
+
try {
|
|
234
|
+
expect(first.acquire(100)).toBe(true);
|
|
235
|
+
const data = JSON.parse(fs.readFileSync(file));
|
|
236
|
+
expect(data.processStart).toBeTruthy();
|
|
237
|
+
expect(replacement.acquire(100)).toBe(false);
|
|
238
|
+
fs.writeFileSync(
|
|
239
|
+
file,
|
|
240
|
+
JSON.stringify({ ...data, processStart: 'different-birth' })
|
|
241
|
+
);
|
|
242
|
+
expect(replacement.acquire(100)).toBe(true);
|
|
243
|
+
first.release();
|
|
244
|
+
expect(fs.existsSync(file)).toBe(true);
|
|
245
|
+
} finally {
|
|
246
|
+
replacement.release();
|
|
247
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
248
|
+
}
|
|
249
|
+
});
|