hammerkit 2.0.0-next.5 → 2.0.0-next.7
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/dist/cache/explain.js +7 -5
- package/dist/dashboard/assets/graph-canvas-DLioOiRN.css +1 -0
- package/dist/dashboard/assets/graph-canvas-HmkW_-XJ.js +8 -0
- package/dist/dashboard/assets/index-gIBDC_m8.css +2 -0
- package/dist/dashboard/assets/index-vE22S5E_.js +17 -0
- package/dist/dashboard/index.html +2 -2
- package/dist/executer/docker-task.js +3 -0
- package/dist/executer/execute-work-task.js +3 -1
- package/dist/executer/local-task.js +3 -0
- package/dist/executer/scheduler/enqueue-next.js +6 -1
- package/dist/executer/scheduler/state-key.js +71 -34
- package/dist/executer/watch-loop.js +18 -5
- package/dist/file/get-file-context.js +5 -1
- package/dist/kubernetes/claim-layout.js +10 -1
- package/dist/kubernetes/ensure-persistent-data.js +84 -10
- package/dist/kubernetes/follow-pod-logs.js +124 -27
- package/dist/kubernetes/network-policy.js +4 -4
- package/dist/kubernetes/restore-kubernetes-data.js +21 -3
- package/dist/kubernetes/store-kubernetes-data.js +13 -2
- package/dist/kubernetes/volumes.js +34 -7
- package/dist/optimizer/calculate-checksum.js +46 -3
- package/dist/optimizer/get-work-cache-stats.js +36 -24
- package/dist/planner/work-item-executions.js +27 -0
- package/dist/planner/work-runtime-kubernetes.js +33 -38
- package/dist/server/capacity.js +63 -0
- package/dist/server/dashboard.js +2 -2
- package/dist/server/executor.js +10 -0
- package/dist/server/overview.js +42 -9
- package/dist/server/plan.js +16 -3
- package/dist/server/policy.js +9 -3
- package/dist/server/run-worker.js +37 -4
- package/dist/server/runs.js +49 -4
- package/dist/server/server-config.js +4 -1
- package/dist/server/server.js +10 -7
- package/dist/utils/limit.js +30 -0
- package/package.json +1 -1
- package/dist/dashboard/assets/index-D51NA6-h.js +0 -14
- package/dist/dashboard/assets/index-mmvC6u9O.css +0 -2
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
<meta name="robots" content="noindex" />
|
|
7
7
|
<link rel="icon" href="/logo.png" type="image/png" />
|
|
8
8
|
<title>hammerkit</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-vE22S5E_.js"></script>
|
|
10
|
+
<link rel="stylesheet" crossorigin href="/assets/index-gIBDC_m8.css">
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
13
13
|
<div id="root"></div>
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.dockerTask = exports.getCapabilityHostConfig = exports.buildCreateOptions = void 0;
|
|
4
|
+
const work_item_executions_1 = require("../planner/work-item-executions");
|
|
4
5
|
const run_labels_1 = require("../docker/run-labels");
|
|
5
6
|
const execute_docker_1 = require("./execute-docker");
|
|
6
7
|
const abort_1 = require("./abort");
|
|
@@ -106,7 +107,9 @@ async function dockerTask(docker, item, environment, options) {
|
|
|
106
107
|
for (const cmd of item.data.cmds) {
|
|
107
108
|
(0, abort_1.checkForAbort)(options.abort);
|
|
108
109
|
item.status.write('info', `execute cmd "${cmd.cmd}" in container`);
|
|
110
|
+
const commandStarted = new Date();
|
|
109
111
|
const result = await (0, execute_docker_1.execCommand)(item.status, environment, container, (0, execute_docker_1.convertToPosixPath)(cmd.cwd), [item.data.shell, '-c', cmd.cmd], item.data.user, undefined, options.abort);
|
|
112
|
+
(0, work_item_executions_1.recordExecution)(item.data, commandStarted, new Date());
|
|
110
113
|
if (result.type === 'timeout') {
|
|
111
114
|
throw new Error(`command ${cmd.cmd} timed out`);
|
|
112
115
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.executeWorkTask = void 0;
|
|
4
|
+
const work_item_executions_1 = require("../planner/work-item-executions");
|
|
4
5
|
const work_task_1 = require("../planner/work-task");
|
|
5
6
|
const states_1 = require("./states");
|
|
6
7
|
const abort_1 = require("./abort");
|
|
@@ -83,7 +84,7 @@ dependents = null) {
|
|
|
83
84
|
// build summary can show the cause column. Reporting only.
|
|
84
85
|
let missCauses;
|
|
85
86
|
if (options.explain) {
|
|
86
|
-
const explanation = await (0, explain_1.explainTask)(work, options.cacheDefault, environment);
|
|
87
|
+
const explanation = await (0, explain_1.explainTask)(work, options.cacheDefault, environment, undefined, undefined, (0, watch_loop_1.getSharedStateKeys)(work, options));
|
|
87
88
|
missCauses = explanation.causes.map(explain_1.describeCause);
|
|
88
89
|
for (const cause of missCauses) {
|
|
89
90
|
work.status.write('info', `cache miss: ${cause}`);
|
|
@@ -103,6 +104,7 @@ dependents = null) {
|
|
|
103
104
|
});
|
|
104
105
|
const timeout = (0, deadline_1.getTaskTimeout)(work.data.timeout, options);
|
|
105
106
|
const deadline = (0, deadline_1.withDeadline)(abort, timeout);
|
|
107
|
+
(0, work_item_executions_1.clearExecutions)(work.data);
|
|
106
108
|
try {
|
|
107
109
|
await work.runtime.execute(environment, {
|
|
108
110
|
cache: cacheState,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.localTask = void 0;
|
|
4
|
+
const work_item_executions_1 = require("../planner/work-item-executions");
|
|
4
5
|
const abort_1 = require("./abort");
|
|
5
6
|
const execute_command_1 = require("./execute-command");
|
|
6
7
|
const log_1 = require("../log");
|
|
@@ -18,7 +19,9 @@ async function localTask(item, environment, options) {
|
|
|
18
19
|
for (const cmd of item.data.cmds) {
|
|
19
20
|
(0, abort_1.checkForAbort)(options.abort);
|
|
20
21
|
item.status.write('info', `execute cmd "${cmd.cmd}" locally`);
|
|
22
|
+
const commandStarted = new Date();
|
|
21
23
|
const exitCode = await (0, execute_command_1.executeCommand)(item.status, options.abort, cmd.cwd, cmd.cmd, envs, environment);
|
|
24
|
+
(0, work_item_executions_1.recordExecution)(item.data, commandStarted, new Date());
|
|
22
25
|
if (exitCode !== 0) {
|
|
23
26
|
options.state.set({
|
|
24
27
|
type: 'crash',
|
|
@@ -18,6 +18,7 @@ async function checkCacheState(item, defaultCacheMethod, environment,
|
|
|
18
18
|
// the cache is off
|
|
19
19
|
// restore: false only looks a backend hit up, its outputs are downloaded once
|
|
20
20
|
// something that runs depends on them (`run --no-restore`, see deferred-restore)
|
|
21
|
+
// memo: the keys of the execution so far, see getExecutionStateKeys
|
|
21
22
|
options = {}) {
|
|
22
23
|
(0, deferred_restore_1.clearDeferredRestore)(item);
|
|
23
24
|
// A task whose cache is off is never looked up, so hashing its sources buys
|
|
@@ -28,7 +29,11 @@ options = {}) {
|
|
|
28
29
|
item.status.write('debug', `${item.name} is skipping cache check, because caching is disabled`);
|
|
29
30
|
return { cached: false, stateKey: exports.UNCACHED_STATE_KEY, resolved: declared, provable: false };
|
|
30
31
|
}
|
|
31
|
-
const { stateKey, stats: currentStats, resolved, provable, unmatched, } = await (0, state_key_1.computeStateKey)(item, defaultCacheMethod, environment);
|
|
32
|
+
const { stateKey, stats: currentStats, resolved, provable, unmatched, } = await (0, state_key_1.computeStateKey)(item, defaultCacheMethod, environment, options.memo);
|
|
33
|
+
// what depends on it takes its key from here
|
|
34
|
+
if (options.memo && !options.memo.has(item.id())) {
|
|
35
|
+
options.memo.set(item.id(), Promise.resolve({ stateKey, provable }));
|
|
36
|
+
}
|
|
32
37
|
for (const src of unmatched) {
|
|
33
38
|
item.status.write('warn', `src "${src.source}" matches no files`);
|
|
34
39
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.computeStateKey = exports.combineStateKeys = exports.isOwnProvable = exports.resolveEffective = void 0;
|
|
3
|
+
exports.computeStateKey = exports.combineStateKeys = exports.getExecutionStateKeys = exports.isOwnProvable = exports.resolveEffective = void 0;
|
|
4
4
|
const get_work_cache_stats_1 = require("../../optimizer/get-work-cache-stats");
|
|
5
5
|
const work_task_1 = require("../../planner/work-task");
|
|
6
6
|
// The state-key engine is shared by the scheduler's hit/miss decision
|
|
@@ -29,6 +29,43 @@ function isOwnProvable(item, unmatched) {
|
|
|
29
29
|
return unmatched.length < item.data.src.filter((src) => !src.inherited).length;
|
|
30
30
|
}
|
|
31
31
|
exports.isOwnProvable = isOwnProvable;
|
|
32
|
+
// What an item on its own way down stands for. Items that depend on or need each other in a
|
|
33
|
+
// loop fail to plan (checkForLoop) and never run: this only has to end. The loop is told by
|
|
34
|
+
// the path of one computation, never by the shared memo, which tasks checked at the same time
|
|
35
|
+
// read, so a placeholder can never end up in another task's key.
|
|
36
|
+
const LOOP = { stateKey: 'loop', provable: true };
|
|
37
|
+
// Keep a computation in the memo, and take it out again if it fails, so a later check
|
|
38
|
+
// computes it anew instead of failing the same way.
|
|
39
|
+
function memoized(memo, key, compute) {
|
|
40
|
+
const existing = memo.get(key);
|
|
41
|
+
if (existing) {
|
|
42
|
+
return existing;
|
|
43
|
+
}
|
|
44
|
+
const promise = compute();
|
|
45
|
+
memo.set(key, promise);
|
|
46
|
+
promise.catch(() => {
|
|
47
|
+
if (memo.get(key) === promise) {
|
|
48
|
+
memo.delete(key);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
return promise;
|
|
52
|
+
}
|
|
53
|
+
const executionKeys = new WeakMap();
|
|
54
|
+
// The keys of one execution (the options of `exec`, which all its items share): its items
|
|
55
|
+
// are checked once, before they run, from the files as they are then. Not for watch mode,
|
|
56
|
+
// where a key is computed again because files changed.
|
|
57
|
+
// The items it holds keys of must have no loop: checkForLoop fails the tree before it runs.
|
|
58
|
+
// Two tasks checked at once that enter a loop of needs from opposite ends would each wait for
|
|
59
|
+
// the other's computation.
|
|
60
|
+
function getExecutionStateKeys(options) {
|
|
61
|
+
let memo = executionKeys.get(options);
|
|
62
|
+
if (!memo) {
|
|
63
|
+
memo = new Map();
|
|
64
|
+
executionKeys.set(options, memo);
|
|
65
|
+
}
|
|
66
|
+
return memo;
|
|
67
|
+
}
|
|
68
|
+
exports.getExecutionStateKeys = getExecutionStateKeys;
|
|
32
69
|
// Fold a task's own state key together with the state keys of its dependencies.
|
|
33
70
|
// A change anywhere in the dependency subtree must change the resulting key, so
|
|
34
71
|
// that a downstream task is invalidated when an upstream source changes — even
|
|
@@ -51,34 +88,36 @@ function concreteMethod(resolved, defaultCacheMethod) {
|
|
|
51
88
|
// state keys of its dependencies, recursively. This is derived purely from the
|
|
52
89
|
// work tree and the filesystem, so it does not depend on scheduler/run state —
|
|
53
90
|
// important because it runs before dependencies are awaited.
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return existing;
|
|
91
|
+
function resolveEffectiveStateKey(item, defaultCacheMethod, environment, memo, path) {
|
|
92
|
+
if (path.has(item.id())) {
|
|
93
|
+
return Promise.resolve(LOOP);
|
|
58
94
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
95
|
+
return memoized(memo, item.id(), async () => {
|
|
96
|
+
const { stateKey, provable } = await computeStateKey(item, defaultCacheMethod, environment, memo, path);
|
|
97
|
+
return { stateKey, provable };
|
|
98
|
+
});
|
|
63
99
|
}
|
|
64
100
|
// Compute the combined state key for a single item together with the source
|
|
65
101
|
// stats it was derived from, sharing one filesystem walk with callers that also
|
|
66
102
|
// need the stats (the scheduler, the explain engine).
|
|
67
|
-
async function computeStateKey(item, defaultCacheMethod, environment, memo = new Map()
|
|
103
|
+
async function computeStateKey(item, defaultCacheMethod, environment, memo = new Map(),
|
|
104
|
+
// the items being computed on the way here, see LOOP
|
|
105
|
+
parentPath = new Set()) {
|
|
106
|
+
const path = new Set(parentPath).add(item.id());
|
|
68
107
|
const resolved = resolveEffective(item, defaultCacheMethod);
|
|
69
108
|
const { stats, unmatched } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(item, environment);
|
|
70
109
|
const ownKey = (0, get_work_cache_stats_1.getStateKey)(stats, concreteMethod(resolved, defaultCacheMethod));
|
|
71
110
|
const depKeys = [];
|
|
72
111
|
let provable = isOwnProvable(item, unmatched);
|
|
73
112
|
for (const dep of item.deps) {
|
|
74
|
-
const effective = await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo);
|
|
113
|
+
const effective = await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo, path);
|
|
75
114
|
depKeys.push(effective.stateKey);
|
|
76
115
|
provable = provable && effective.provable;
|
|
77
116
|
}
|
|
78
117
|
// what a task that needs a service passed against: the sources of the service and of the
|
|
79
118
|
// tasks it depends on (its definition is in the task id, getWorkServiceCacheIdentity)
|
|
80
119
|
for (const need of item.needs) {
|
|
81
|
-
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
|
|
120
|
+
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo, path);
|
|
82
121
|
depKeys.push(`${need.name}=${effective.stateKey}`);
|
|
83
122
|
}
|
|
84
123
|
return { stateKey: combineStateKeys(ownKey, depKeys), stats, resolved, provable, unmatched };
|
|
@@ -87,29 +126,27 @@ exports.computeStateKey = computeStateKey;
|
|
|
87
126
|
// A needed service's sources folded with the state keys of its dependencies and needs. It
|
|
88
127
|
// does not make the task that needs it unprovable: a dependency without `src` (resetting the
|
|
89
128
|
// service's data, say) is skipped with the service when the task is a cache hit.
|
|
90
|
-
|
|
129
|
+
function resolveNeedStateKey(service, defaultCacheMethod, environment, memo, parentPath) {
|
|
91
130
|
const memoKey = `need\0${service.name}`;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return existing;
|
|
95
|
-
}
|
|
96
|
-
// services that need each other in a loop fail to plan (checkForLoop): this only has to end
|
|
97
|
-
memo.set(memoKey, { stateKey: 'loop', provable: true });
|
|
98
|
-
const resolved = resolveEffective(service, defaultCacheMethod);
|
|
99
|
-
const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(service, environment);
|
|
100
|
-
const keys = [];
|
|
101
|
-
for (const dep of service.deps) {
|
|
102
|
-
keys.push((await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo)).stateKey);
|
|
103
|
-
}
|
|
104
|
-
for (const need of service.needs) {
|
|
105
|
-
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo);
|
|
106
|
-
keys.push(`${need.name}=${effective.stateKey}`);
|
|
131
|
+
if (parentPath.has(memoKey)) {
|
|
132
|
+
return Promise.resolve(LOOP);
|
|
107
133
|
}
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
134
|
+
const path = new Set(parentPath).add(memoKey);
|
|
135
|
+
return memoized(memo, memoKey, async () => {
|
|
136
|
+
const resolved = resolveEffective(service, defaultCacheMethod);
|
|
137
|
+
const { stats } = await (0, get_work_cache_stats_1.getWorkItemCacheStats)(service, environment);
|
|
138
|
+
const keys = [];
|
|
139
|
+
for (const dep of service.deps) {
|
|
140
|
+
keys.push((await resolveEffectiveStateKey(dep, defaultCacheMethod, environment, memo, path)).stateKey);
|
|
141
|
+
}
|
|
142
|
+
for (const need of service.needs) {
|
|
143
|
+
const effective = await resolveNeedStateKey(need.service, defaultCacheMethod, environment, memo, path);
|
|
144
|
+
keys.push(`${need.name}=${effective.stateKey}`);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
stateKey: combineStateKeys((0, get_work_cache_stats_1.getStateKey)(stats, concreteMethod(resolved, defaultCacheMethod)), keys),
|
|
148
|
+
provable: true,
|
|
149
|
+
};
|
|
150
|
+
});
|
|
114
151
|
}
|
|
115
152
|
//# sourceMappingURL=state-key.js.map
|
|
@@ -1,20 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.watchLoop = void 0;
|
|
3
|
+
exports.watchLoop = exports.getSharedStateKeys = exports.isWatchMode = void 0;
|
|
4
4
|
const watch_state_key_1 = require("./watch-state-key");
|
|
5
5
|
const state_1 = require("./state");
|
|
6
6
|
const abort_event_1 = require("../utils/abort-event");
|
|
7
7
|
const state_resolver_1 = require("./state-resolver");
|
|
8
8
|
const abort_1 = require("./abort");
|
|
9
9
|
const enqueue_next_1 = require("./scheduler/enqueue-next");
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
const state_key_1 = require("./scheduler/state-key");
|
|
11
|
+
// Whether an item is rechecked when its files change: not in daemon mode, and not for a
|
|
12
|
+
// container service that watches its own files (`continuous`); a kubernetes-service and a
|
|
13
|
+
// task have no such field
|
|
14
|
+
function isWatchMode(work, options) {
|
|
13
15
|
const continuous = 'continuous' in work.data ? work.data.continuous : false;
|
|
14
|
-
|
|
16
|
+
return !!options.watch && !options.daemon && !continuous;
|
|
17
|
+
}
|
|
18
|
+
exports.isWatchMode = isWatchMode;
|
|
19
|
+
// The keys of the execution to share, see getExecutionStateKeys: none in watch mode, where a
|
|
20
|
+
// key is computed again because files changed
|
|
21
|
+
function getSharedStateKeys(work, options) {
|
|
22
|
+
return isWatchMode(work, options) ? undefined : (0, state_key_1.getExecutionStateKeys)(options);
|
|
23
|
+
}
|
|
24
|
+
exports.getSharedStateKeys = getSharedStateKeys;
|
|
25
|
+
async function watchLoop(work, environment, options, factory) {
|
|
26
|
+
const watchMode = isWatchMode(work, options);
|
|
15
27
|
const currentStats = await (0, enqueue_next_1.checkCacheState)(work, options.cacheDefault, environment, {
|
|
16
28
|
watch: watchMode,
|
|
17
29
|
restore: options.restore,
|
|
30
|
+
memo: getSharedStateKeys(work, options),
|
|
18
31
|
});
|
|
19
32
|
(0, abort_1.checkForAbort)(environment.abortCtrl.signal);
|
|
20
33
|
const watchState = watchMode
|
|
@@ -39,7 +39,11 @@ function getFileContext(cwd) {
|
|
|
39
39
|
resolve({ type: 'directory' });
|
|
40
40
|
}
|
|
41
41
|
else if (stats.isFile()) {
|
|
42
|
-
resolve({
|
|
42
|
+
resolve({
|
|
43
|
+
type: 'file',
|
|
44
|
+
lastModified: stats.mtimeMs,
|
|
45
|
+
identity: { size: stats.size, changed: stats.ctimeMs, inode: stats.ino },
|
|
46
|
+
});
|
|
43
47
|
}
|
|
44
48
|
else {
|
|
45
49
|
resolve({ type: 'other' });
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.hasOutputsFlatteningAlike = exports.hasPathsEndingAlike = exports.byDepth = exports.getClaimSubPath = void 0;
|
|
3
|
+
exports.hasOutputsFlatteningAlike = exports.hasPathsEndingAlike = exports.byDepth = exports.getInheritedSourceSubPath = exports.getClaimSubPath = void 0;
|
|
4
4
|
const crypto_1 = require("crypto");
|
|
5
5
|
const path_1 = require("path");
|
|
6
6
|
const portable_path_1 = require("../planner/utils/portable-path");
|
|
@@ -45,6 +45,15 @@ function getClaimSubPath(work, path) {
|
|
|
45
45
|
return `${name}-${(0, crypto_1.createHash)('sha256').update(own).digest('hex').slice(0, 12)}`;
|
|
46
46
|
}
|
|
47
47
|
exports.getClaimSubPath = getClaimSubPath;
|
|
48
|
+
// Where a src of one of its deps is in the claim of `work`: always with the hash of its
|
|
49
|
+
// path, so it is apart from the paths of the task itself, which getClaimSubPath names
|
|
50
|
+
// without looking at them (they are not part of the task's id, see claimLayout), and from
|
|
51
|
+
// the other srcs of its deps (`package.json` and `core/package.json` of an `npm ci`)
|
|
52
|
+
function getInheritedSourceSubPath(work, path) {
|
|
53
|
+
const name = (0, path_1.basename)(path) || 'root';
|
|
54
|
+
return `${name}-${(0, crypto_1.createHash)('sha256').update((0, portable_path_1.portablePath)(work.data.projectRoot, path)).digest('hex').slice(0, 12)}`;
|
|
55
|
+
}
|
|
56
|
+
exports.getInheritedSourceSubPath = getInheritedSourceSubPath;
|
|
48
57
|
function byDepth(a, b) {
|
|
49
58
|
return a.split('/').length - b.split('/').length || (a < b ? -1 : a > b ? 1 : 0);
|
|
50
59
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ensurePersistentData = exports.packSource = exports.clearStateFiles = exports.restoreLiveFiles = exports.syncLiveFiles = exports.SYNC_FILES = exports.getStateDirsInUse = exports.REMOVE_OTHER_STATES = exports.execInPod = exports.clearClaim = exports.getPodForPersistence = void 0;
|
|
3
|
+
exports.ensurePersistentData = exports.packSource = exports.clearStateFiles = exports.ENSURE_OUTPUT_FILES = exports.restoreLiveFiles = exports.syncLiveFiles = exports.SYNC_FILES = exports.getStateDirsInUse = exports.REMOVE_OTHER_STATES = exports.execInPod = exports.clearClaim = exports.getPodForPersistence = exports.applyWithinQuota = exports.QUOTA_TIMEOUT = exports.TRANSFER_POD_RESOURCES = void 0;
|
|
4
4
|
const volumes_1 = require("./volumes");
|
|
5
5
|
const apply_1 = require("./apply");
|
|
6
6
|
const path_1 = require("path");
|
|
@@ -8,6 +8,7 @@ const tar_1 = require("../utils/tar");
|
|
|
8
8
|
const resources_1 = require("./resources");
|
|
9
9
|
const await_running_state_1 = require("./await-running-state");
|
|
10
10
|
const sleep_1 = require("../utils/sleep");
|
|
11
|
+
const abort_1 = require("../executer/abort");
|
|
11
12
|
const version_1 = require("../version");
|
|
12
13
|
const log_1 = require("../log");
|
|
13
14
|
const ensure_kubernetes_persistent_volume_claim_exists_1 = require("./ensure-kubernetes-persistent-volume-claim-exists");
|
|
@@ -42,7 +43,44 @@ async function removeStalePod(instance, namespace, name) {
|
|
|
42
43
|
await (0, sleep_1.sleep)(500);
|
|
43
44
|
}
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
// What the pod that copies files into and out of a claim gets: tar and find, through
|
|
47
|
+
// exec. Without its own, the default limit of the namespace is taken, which is sized for
|
|
48
|
+
// tasks (4Gi on a server), and a run's quota then has no room for the pods of its tasks.
|
|
49
|
+
exports.TRANSFER_POD_RESOURCES = {
|
|
50
|
+
requests: { cpu: '50m', memory: '64Mi' },
|
|
51
|
+
limits: { memory: '512Mi' },
|
|
52
|
+
};
|
|
53
|
+
// how long a transfer pod waits for room under the quota of its namespace, such as the
|
|
54
|
+
// pods of other tasks of the run ending, before its task fails
|
|
55
|
+
exports.QUOTA_TIMEOUT = 10 * 60 * 1000;
|
|
56
|
+
// A Job waits for room under a full quota, a pod is refused: so a transfer pod is
|
|
57
|
+
// created again until the pods of the tasks it runs beside are done and make room
|
|
58
|
+
async function applyWithinQuota(instance, podSpec, service, timeout = exports.QUOTA_TIMEOUT, pollInterval = 2000,
|
|
59
|
+
// a run stopped while it waits: it stops waiting
|
|
60
|
+
signal) {
|
|
61
|
+
const deadline = Date.now() + timeout;
|
|
62
|
+
for (let waiting = false;; waiting = true) {
|
|
63
|
+
if (signal) {
|
|
64
|
+
(0, abort_1.checkForAbort)(signal);
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
return await (0, apply_1.apply)(instance, podSpec);
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
if ((0, apply_1.statusCodeOf)(e) !== 403 || !/exceeded quota/i.test((0, log_1.getErrorMessage)(e)) || Date.now() >= deadline) {
|
|
71
|
+
throw e;
|
|
72
|
+
}
|
|
73
|
+
if (!waiting) {
|
|
74
|
+
service.status.write('info', `wait for room under the quota of ${podSpec.metadata.namespace}`);
|
|
75
|
+
}
|
|
76
|
+
await (0, sleep_1.sleep)(pollInterval);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.applyWithinQuota = applyWithinQuota;
|
|
81
|
+
async function getPodForPersistence(instance, env, service, persistence, type,
|
|
82
|
+
// stops a wait for room under the quota
|
|
83
|
+
signal, fn) {
|
|
46
84
|
if (persistence.volumes.length === 0) {
|
|
47
85
|
return;
|
|
48
86
|
}
|
|
@@ -50,7 +88,11 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
|
|
|
50
88
|
// and takes no upload
|
|
51
89
|
const own = persistence.mounts.filter((m) => !m.workspace);
|
|
52
90
|
const mounts = type === 'read'
|
|
53
|
-
?
|
|
91
|
+
? // what a task stores are its outputs, which it mounts writable: its src and the
|
|
92
|
+
// outputs of its deps are not mounted, so an output inside a folder of its src
|
|
93
|
+
// (pkg/coverage/lcov.info) has a place in the pod even when the claim holds no src,
|
|
94
|
+
// after a restore into a new claim
|
|
95
|
+
own.filter((m) => service.data.type !== 'container-task' || !m.mount.readOnly).map((m) => m.mount)
|
|
54
96
|
: own.reduce((array, v) => {
|
|
55
97
|
if (!array.some((i) => i.name === v.mount.name)) {
|
|
56
98
|
array.push({
|
|
@@ -61,11 +103,13 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
|
|
|
61
103
|
}
|
|
62
104
|
return array;
|
|
63
105
|
}, []);
|
|
106
|
+
// only the claims it mounts: one of a dep it doesn't read would only bind where it goes
|
|
107
|
+
const volumes = persistence.volumes.filter((volume) => mounts.some((mount) => mount.name === volume.name));
|
|
64
108
|
// Guarantee the claims exist right before scheduling the pod that mounts
|
|
65
109
|
// them. ensurePersistentData also creates them, but store/restore reach this
|
|
66
110
|
// function directly, and a claim shared with another work item can be removed
|
|
67
111
|
// concurrently — without this the pod would hang Pending on "pvc not found".
|
|
68
|
-
for (const volume of
|
|
112
|
+
for (const volume of volumes) {
|
|
69
113
|
await (0, ensure_kubernetes_persistent_volume_claim_exists_1.ensureKubernetesPersistentVolumeClaimExists)(instance, env, volume, service, persistence.sizes[volume.name], persistence.kept.includes(volume.name));
|
|
70
114
|
}
|
|
71
115
|
const name = (0, resources_1.getResourceName)(service, env, type === 'write' ? '-upload' : '-download');
|
|
@@ -73,7 +117,7 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
|
|
|
73
117
|
// commit): the pod is not one its Service sends to
|
|
74
118
|
const serving = !!env.server?.serviceRun && service.data.type === 'container-service';
|
|
75
119
|
// and on a service run, it goes where the pods that mount the same claims run
|
|
76
|
-
const claims = env.server?.serviceRun ? (0, volumes_1.getClaimPodSpec)(
|
|
120
|
+
const claims = env.server?.serviceRun ? (0, volumes_1.getClaimPodSpec)(volumes) : { labels: {} };
|
|
77
121
|
const podSpec = {
|
|
78
122
|
kind: 'Pod',
|
|
79
123
|
apiVersion: 'v1',
|
|
@@ -102,15 +146,16 @@ async function getPodForPersistence(instance, env, service, persistence, type, f
|
|
|
102
146
|
command: ['sh'],
|
|
103
147
|
tty: true,
|
|
104
148
|
volumeMounts: mounts,
|
|
149
|
+
resources: exports.TRANSFER_POD_RESOURCES,
|
|
105
150
|
...(0, job_placement_1.getContainerSecurity)(env),
|
|
106
151
|
},
|
|
107
152
|
],
|
|
108
|
-
volumes
|
|
153
|
+
volumes,
|
|
109
154
|
},
|
|
110
155
|
};
|
|
111
156
|
service.status.write('debug', `create pod ${name}`);
|
|
112
157
|
await removeStalePod(instance, env.namespace, name);
|
|
113
|
-
const pod = await (
|
|
158
|
+
const pod = await applyWithinQuota(instance, podSpec, service, exports.QUOTA_TIMEOUT, 2000, signal);
|
|
114
159
|
if (pod.status?.phase !== 'Running') {
|
|
115
160
|
service.status.write('debug', `await running state ${name}`);
|
|
116
161
|
await (0, await_running_state_1.awaitRunningState)(instance, env, name, 'Running');
|
|
@@ -273,7 +318,7 @@ exports.syncLiveFiles = syncLiveFiles;
|
|
|
273
318
|
// its files mounts them from: a move that did not get up leaves the service at the commit
|
|
274
319
|
// it was at
|
|
275
320
|
async function restoreLiveFiles(instance, env, work, persistence, from) {
|
|
276
|
-
await getPodForPersistence(instance, env, work, persistence, 'write', (pod) => syncLiveFiles(instance, env, work, persistence, pod, from));
|
|
321
|
+
await getPodForPersistence(instance, env, work, persistence, 'write', undefined, (pod) => syncLiveFiles(instance, env, work, persistence, pod, from));
|
|
277
322
|
}
|
|
278
323
|
exports.restoreLiveFiles = restoreLiveFiles;
|
|
279
324
|
// Prepares the folder of a state in the claim of a work item of a service run before its
|
|
@@ -290,6 +335,33 @@ async function prepareStateFiles(instance, env, work, pod, dir, inUse) {
|
|
|
290
335
|
await clearStateDir(instance, env, pod, `${targetPath}/${dir}`);
|
|
291
336
|
return true;
|
|
292
337
|
}
|
|
338
|
+
// Makes each of `files` (paths in the claim of `work`, see KubernetesPersistence.files) an
|
|
339
|
+
// empty file unless it is one: Kubernetes would make a directory of a subPath that isn't
|
|
340
|
+
// there, and the task could not write the file. A directory there is what an earlier
|
|
341
|
+
// version left in a kept claim, and goes. A path ending in `/` is made a folder.
|
|
342
|
+
exports.ENSURE_OUTPUT_FILES = `cd "$1" || exit 1
|
|
343
|
+
shift
|
|
344
|
+
for f; do
|
|
345
|
+
case "$f" in */) mkdir -p "$f" || exit 1; continue ;; esac
|
|
346
|
+
if [ -d "$f" ] && [ ! -L "$f" ]; then rm -rf "$f" || exit 1; fi
|
|
347
|
+
if [ ! -e "$f" ] && [ ! -L "$f" ]; then mkdir -p "$(dirname "$f")" && : > "$f" || exit 1; fi
|
|
348
|
+
done`;
|
|
349
|
+
async function ensureOutputFiles(instance, env, work, pod, files) {
|
|
350
|
+
if (files.length === 0) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const made = await execInPod(instance, env, pod, [
|
|
354
|
+
'sh',
|
|
355
|
+
'-c',
|
|
356
|
+
exports.ENSURE_OUTPUT_FILES,
|
|
357
|
+
'sh',
|
|
358
|
+
`/dev/hammerkit/${work.id()}`,
|
|
359
|
+
...files,
|
|
360
|
+
]);
|
|
361
|
+
if (made) {
|
|
362
|
+
throw new Error(`unable to make the output files ${files.join(', ')} of ${work.name}: ${made}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
293
365
|
// what a failed upload, restore or task left: tar does not remove files the commit does not have
|
|
294
366
|
async function clearStateDir(instance, env, pod, path) {
|
|
295
367
|
const cleared = await execInPod(instance, env, pod, ['rm', '-rf', path]);
|
|
@@ -358,7 +430,7 @@ options = {}) {
|
|
|
358
430
|
const files = persistence.volumes.some((volume) => volume.persistentVolumeClaim?.claimName === claim)
|
|
359
431
|
? options.files
|
|
360
432
|
: undefined;
|
|
361
|
-
if (persistence.sources.length === 0 && !options.reset && !files) {
|
|
433
|
+
if (persistence.sources.length === 0 && persistence.files.length === 0 && !options.reset && !files) {
|
|
362
434
|
return;
|
|
363
435
|
}
|
|
364
436
|
// the folders of the states the pods of the run mount, before the upload pod mounts the claim
|
|
@@ -367,7 +439,8 @@ options = {}) {
|
|
|
367
439
|
inUse.add(dir);
|
|
368
440
|
}
|
|
369
441
|
service.status.write('debug', 'start upload container');
|
|
370
|
-
|
|
442
|
+
const { signal } = environment.abortCtrl;
|
|
443
|
+
await getPodForPersistence(instance, env, service, persistence, 'write', signal, async (name) => {
|
|
371
444
|
if (options.reset) {
|
|
372
445
|
await clearClaim(instance, env, service, name);
|
|
373
446
|
}
|
|
@@ -407,6 +480,7 @@ options = {}) {
|
|
|
407
480
|
}
|
|
408
481
|
});
|
|
409
482
|
}
|
|
483
|
+
await ensureOutputFiles(instance, env, service, name, persistence.files);
|
|
410
484
|
if (files) {
|
|
411
485
|
const dir = `/dev/hammerkit/${service.id()}/${files.dir}`;
|
|
412
486
|
const marked = await execInPod(instance, env, name, [
|