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.
Files changed (38) hide show
  1. package/dist/cache/explain.js +7 -5
  2. package/dist/dashboard/assets/graph-canvas-DLioOiRN.css +1 -0
  3. package/dist/dashboard/assets/graph-canvas-HmkW_-XJ.js +8 -0
  4. package/dist/dashboard/assets/index-gIBDC_m8.css +2 -0
  5. package/dist/dashboard/assets/index-vE22S5E_.js +17 -0
  6. package/dist/dashboard/index.html +2 -2
  7. package/dist/executer/docker-task.js +3 -0
  8. package/dist/executer/execute-work-task.js +3 -1
  9. package/dist/executer/local-task.js +3 -0
  10. package/dist/executer/scheduler/enqueue-next.js +6 -1
  11. package/dist/executer/scheduler/state-key.js +71 -34
  12. package/dist/executer/watch-loop.js +18 -5
  13. package/dist/file/get-file-context.js +5 -1
  14. package/dist/kubernetes/claim-layout.js +10 -1
  15. package/dist/kubernetes/ensure-persistent-data.js +84 -10
  16. package/dist/kubernetes/follow-pod-logs.js +124 -27
  17. package/dist/kubernetes/network-policy.js +4 -4
  18. package/dist/kubernetes/restore-kubernetes-data.js +21 -3
  19. package/dist/kubernetes/store-kubernetes-data.js +13 -2
  20. package/dist/kubernetes/volumes.js +34 -7
  21. package/dist/optimizer/calculate-checksum.js +46 -3
  22. package/dist/optimizer/get-work-cache-stats.js +36 -24
  23. package/dist/planner/work-item-executions.js +27 -0
  24. package/dist/planner/work-runtime-kubernetes.js +33 -38
  25. package/dist/server/capacity.js +63 -0
  26. package/dist/server/dashboard.js +2 -2
  27. package/dist/server/executor.js +10 -0
  28. package/dist/server/overview.js +42 -9
  29. package/dist/server/plan.js +16 -3
  30. package/dist/server/policy.js +9 -3
  31. package/dist/server/run-worker.js +37 -4
  32. package/dist/server/runs.js +49 -4
  33. package/dist/server/server-config.js +4 -1
  34. package/dist/server/server.js +10 -7
  35. package/dist/utils/limit.js +30 -0
  36. package/package.json +1 -1
  37. package/dist/dashboard/assets/index-D51NA6-h.js +0 -14
  38. 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-D51NA6-h.js"></script>
10
- <link rel="stylesheet" crossorigin href="/assets/index-mmvC6u9O.css">
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
- async function resolveEffectiveStateKey(item, defaultCacheMethod, environment, memo) {
55
- const existing = memo.get(item.id());
56
- if (existing !== undefined) {
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
- const { stateKey, provable } = await computeStateKey(item, defaultCacheMethod, environment, memo);
60
- const effective = { stateKey, provable };
61
- memo.set(item.id(), effective);
62
- return effective;
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
- async function resolveNeedStateKey(service, defaultCacheMethod, environment, memo) {
129
+ function resolveNeedStateKey(service, defaultCacheMethod, environment, memo, parentPath) {
91
130
  const memoKey = `need\0${service.name}`;
92
- const existing = memo.get(memoKey);
93
- if (existing !== undefined) {
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 effective = {
109
- stateKey: combineStateKeys((0, get_work_cache_stats_1.getStateKey)(stats, concreteMethod(resolved, defaultCacheMethod)), keys),
110
- provable: true,
111
- };
112
- memo.set(memoKey, effective);
113
- return effective;
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
- async function watchLoop(work, environment, options, factory) {
11
- // a container service that watches its own files (`continuous`); a kubernetes-service and a
12
- // task have no such field
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
- const watchMode = options.watch && !options.daemon && !continuous;
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({ type: 'file', lastModified: stats.mtimeMs });
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
- async function getPodForPersistence(instance, env, service, persistence, type, fn) {
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
- ? own.map((m) => m.mount)
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 persistence.volumes) {
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)(persistence.volumes) : { labels: {} };
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: persistence.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 (0, apply_1.apply)(instance, podSpec);
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
- await getPodForPersistence(instance, env, service, persistence, 'write', async (name) => {
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, [