hammerkit 2.0.0-next.2 → 2.0.0-next.4

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ensurePersistentData = exports.packSource = 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.restoreLiveFiles = exports.syncLiveFiles = exports.SYNC_FILES = exports.getStateDirsInUse = exports.REMOVE_OTHER_STATES = exports.execInPod = exports.clearClaim = exports.getPodForPersistence = void 0;
4
4
  const volumes_1 = require("./volumes");
5
5
  const apply_1 = require("./apply");
6
6
  const path_1 = require("path");
@@ -282,6 +282,31 @@ exports.restoreLiveFiles = restoreLiveFiles;
282
282
  // Returns whether the upload is needed.
283
283
  async function prepareStateFiles(instance, env, work, pod, dir, inUse) {
284
284
  const targetPath = `/dev/hammerkit/${work.id()}`;
285
+ await removeOtherStates(instance, env, pod, targetPath, dir, inUse);
286
+ if (inUse.has(dir) && !(await execInPod(instance, env, pod, ['test', '-e', `${targetPath}/${dir}/${UPLOADED}`]))) {
287
+ work.status.write('info', `the files of ${work.name} are there already`);
288
+ return false;
289
+ }
290
+ await clearStateDir(instance, env, pod, `${targetPath}/${dir}`);
291
+ return true;
292
+ }
293
+ // what a failed upload, restore or task left: tar does not remove files the commit does not have
294
+ async function clearStateDir(instance, env, pod, path) {
295
+ const cleared = await execInPod(instance, env, pod, ['rm', '-rf', path]);
296
+ if (cleared) {
297
+ throw new Error(`unable to clear ${path}: ${cleared}`);
298
+ }
299
+ }
300
+ // Before a restore of a service run into the folder `dir` of the claim of `work` (in its upload
301
+ // pod `pod`): the folders of the other states go but those of `inUse`, which pods of the run
302
+ // mount, and `dir` is emptied. A claim that took only restores holds one state, as after a Job.
303
+ async function clearStateFiles(instance, env, work, pod, dir, inUse) {
304
+ const targetPath = `/dev/hammerkit/${work.id()}`;
305
+ await removeOtherStates(instance, env, pod, targetPath, dir, inUse);
306
+ await clearStateDir(instance, env, pod, `${targetPath}/${dir}`);
307
+ }
308
+ exports.clearStateFiles = clearStateFiles;
309
+ async function removeOtherStates(instance, env, pod, targetPath, dir, inUse) {
285
310
  const removed = await execInPod(instance, env, pod, [
286
311
  'sh',
287
312
  '-c',
@@ -294,16 +319,6 @@ async function prepareStateFiles(instance, env, work, pod, dir, inUse) {
294
319
  if (removed) {
295
320
  throw new Error(`unable to remove the files of other commits in ${targetPath}: ${removed}`);
296
321
  }
297
- if (inUse.has(dir) && !(await execInPod(instance, env, pod, ['test', '-e', `${targetPath}/${dir}/${UPLOADED}`]))) {
298
- work.status.write('info', `the files of ${work.name} are there already`);
299
- return false;
300
- }
301
- // what a failed upload or task left: tar does not remove files the commit does not have
302
- const cleared = await execInPod(instance, env, pod, ['rm', '-rf', `${targetPath}/${dir}`]);
303
- if (cleared) {
304
- throw new Error(`unable to clear ${targetPath}/${dir}: ${cleared}`);
305
- }
306
- return true;
307
322
  }
308
323
  // The archive that uploads a source into the claim: its file or directory, at its place in
309
324
  // the claim (see getClaimSubPath) instead of under its own name
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.evictKeptClaims = exports.sweepKeptClaims = exports.expireKeptClaims = exports.recordClaimState = exports.forgetClaimState = exports.useClaimState = exports.isKeptClaim = exports.getClaimName = exports.EXPIRES_ANNOTATION = exports.STATE_ANNOTATION = exports.KEPT_BY_LABEL = exports.VOLUME_SCOPE_LABEL = void 0;
3
+ exports.evictKeptClaims = exports.sweepKeptClaims = exports.expireKeptClaims = exports.recordClaimState = exports.forgetClaimState = exports.rememberedClaimState = exports.useClaimState = exports.isKeptClaim = exports.getClaimName = exports.EXPIRES_ANNOTATION = exports.STATE_ANNOTATION = exports.KEPT_BY_LABEL = exports.VOLUME_SCOPE_LABEL = void 0;
4
4
  const crypto_1 = require("crypto");
5
5
  const apply_1 = require("./apply");
6
6
  const sleep_1 = require("../utils/sleep");
@@ -98,6 +98,21 @@ async function annotate(instance, namespace, name, annotations) {
98
98
  throw e;
99
99
  }
100
100
  }
101
+ // The state key of the outputs whose folder is whole in the claim of a task of a service run
102
+ // (see getStateFilesDir), null when it names none (also while the claim is being removed),
103
+ // undefined when there is no claim. The run's namespace is its own, and only the run writes
104
+ // to the claim: it forgets the state before it writes, records it after.
105
+ async function rememberedClaimState(instance, kubernetes, name) {
106
+ const claim = await readClaim(instance, kubernetes.namespace, name);
107
+ if (!claim) {
108
+ return undefined;
109
+ }
110
+ if (claim.metadata?.deletionTimestamp) {
111
+ return null;
112
+ }
113
+ return claim.metadata?.annotations?.[exports.STATE_ANNOTATION] ?? null;
114
+ }
115
+ exports.rememberedClaimState = rememberedClaimState;
101
116
  // Before a task writes to its kept claim: the outputs in it stop being those of a state.
102
117
  // Whether the claim was there, with what an earlier run left in it.
103
118
  function forgetClaimState(instance, kubernetes, name) {
@@ -16,7 +16,9 @@ stateDir) {
16
16
  exports.getRestoreTarget = getRestoreTarget;
17
17
  async function restoreKubernetesData(work, kubernetes, instance, environment, path,
18
18
  // the kept claim of the work holds what an earlier run left, which goes first; on a
19
- // service run, the outputs go into the folder of `stateKey` (see getStateFilesDir)
19
+ // service run, the outputs go into the folder of `stateKey` (see getStateFilesDir);
20
+ // `otherStates`: the folders of the other states go but these (the ones pods of the run
21
+ // mount), and the folder of `stateKey` is emptied first
20
22
  options = {}) {
21
23
  const persistence = await (0, volumes_1.getKubernetesPersistence)(work, kubernetes, options.stateKey);
22
24
  const stateDir = kubernetes.server?.serviceRun ? (0, volumes_1.getItemFilesDir)(work, options.stateKey) : undefined;
@@ -24,6 +26,9 @@ options = {}) {
24
26
  if (options.reset) {
25
27
  await (0, ensure_persistent_data_1.clearClaim)(instance, kubernetes, work, name);
26
28
  }
29
+ if (stateDir && options.otherStates) {
30
+ await (0, ensure_persistent_data_1.clearStateFiles)(instance, kubernetes, work, name, stateDir, options.otherStates);
31
+ }
27
32
  for (const generatedArchive of (0, event_cache_1.getArchivePaths)(work.data, path)) {
28
33
  if (!(await environment.file.exists(generatedArchive.filename))) {
29
34
  continue;
@@ -170,6 +170,9 @@ function kubernetesTaskRuntime(task, kubernetes) {
170
170
  // on a server that keeps them, the task's claim stays after the run with the state of its
171
171
  // outputs, and the next run of the same source mounts them as they are (see kept-claims)
172
172
  const kept = (0, kept_claims_1.isKeptClaim)(task, kubernetes);
173
+ // on a service run, the claim names the last state whose folder is whole (see
174
+ // rememberedClaimState): a wake or a move at that state mounts it without an upload
175
+ const remembers = kept || !!kubernetes.server?.serviceRun;
173
176
  const claim = (0, volumes_1.getVolumeName)(task, kubernetes);
174
177
  return {
175
178
  initialize() {
@@ -182,6 +185,21 @@ function kubernetesTaskRuntime(task, kubernetes) {
182
185
  stateKey &&
183
186
  (await (0, ensure_persistent_data_1.getStateDirsInUse)(instance, kubernetes.namespace, claim)).has((0, volumes_1.getStateFilesDir)(stateKey))) {
184
187
  task.status.write('info', `${task.name}: services of the run use its outputs of this state, they stay`);
188
+ await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, stateKey);
189
+ return;
190
+ }
191
+ if (remembers && !kept) {
192
+ // the folder of the state is written now: until it is whole, the claim names no state
193
+ // (a restore without a state key says none, and the next run uploads again)
194
+ await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim);
195
+ await (0, restore_kubernetes_data_1.restoreKubernetesData)(task, kubernetes, instance, environment, path, {
196
+ stateKey,
197
+ // the folders of other states no pod of the run mounts go, as before a Job
198
+ ...(stateKey ? { otherStates: await (0, ensure_persistent_data_1.getStateDirsInUse)(instance, kubernetes.namespace, claim) } : {}),
199
+ });
200
+ if (stateKey) {
201
+ await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, stateKey);
202
+ }
185
203
  return;
186
204
  }
187
205
  if (!kept) {
@@ -231,11 +249,14 @@ function kubernetesTaskRuntime(task, kubernetes) {
231
249
  try {
232
250
  if (files && (await (0, ensure_persistent_data_1.getStateDirsInUse)(instance, kubernetes.namespace, claim)).has(files.dir)) {
233
251
  task.status.write('info', `${task.name}: services of the run use its outputs of this state, they stay`);
252
+ await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, options.stateKey);
234
253
  return;
235
254
  }
236
255
  // the task writes to it now: what it holds stops being the outputs of a state, and
237
- // goes, as a new claim would hold nothing
238
- const reset = kept && (await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim));
256
+ // goes, as a new claim would hold nothing (on a service run, the folders of other
257
+ // states that no pod mounts go)
258
+ const forgotten = remembers && (await (0, kept_claims_1.forgetClaimState)(instance, kubernetes, claim));
259
+ const reset = kept && forgotten;
239
260
  await (0, ensure_persistent_data_1.ensurePersistentData)(instance, kubernetes, environment, task, persistence, { reset, files });
240
261
  }
241
262
  catch (e) {
@@ -415,7 +436,7 @@ function kubernetesTaskRuntime(task, kubernetes) {
415
436
  }
416
437
  // the finished job stays for its state, the values it read need not
417
438
  await removeTaskSecrets(instance, kubernetes, task);
418
- if (kept) {
439
+ if (remembers) {
419
440
  await (0, kept_claims_1.recordClaimState)(instance, kubernetes, claim, options.stateKey);
420
441
  }
421
442
  }
@@ -471,6 +492,15 @@ function kubernetesTaskRuntime(task, kubernetes) {
471
492
  // the server removes the Jobs of a run at its end, the claim says
472
493
  return (0, kept_claims_1.useClaimState)(instance, kubernetes, claim);
473
494
  }
495
+ if (remembers) {
496
+ // a service run that sleeps loses its Jobs but keeps its claims, the claim says. One
497
+ // that names no state is being written (or was, and that failed): the folder of the
498
+ // state of an earlier Job may be gone. Only a task without a claim goes by its Jobs.
499
+ const remembered = await (0, kept_claims_1.rememberedClaimState)(instance, kubernetes, claim);
500
+ if (remembered !== undefined) {
501
+ return remembered;
502
+ }
503
+ }
474
504
  const jobs = await instance.batchApi.listNamespacedJob({
475
505
  namespace: kubernetes.namespace,
476
506
  labelSelector: (0, resources_1.getItemSelector)(task, kubernetes),
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "hammerkit",
3
3
  "description": "Containerized build tool with incremental caching that runs the same build file locally, in Docker and on Kubernetes.",
4
- "version": "2.0.0-next.2",
4
+ "version": "2.0.0-next.4",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",