hammerkit 1.27.0-next.26 → 1.27.0-next.27

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.
@@ -288,6 +288,9 @@ class EndpointGateway {
288
288
  return page(res, 503, 'Starting', `<p>The services of this endpoint are ${run.wakes ? 'waking up' : 'starting'} (${escape(run.state)}), this page reloads until they are up.</p>`);
289
289
  }
290
290
  if (run.state === 'asleep') {
291
+ // it may wake on a later request, once there is a place for it
292
+ res.setHeader('refresh', '30');
293
+ res.setHeader('retry-after', '30');
291
294
  return page(res, 503, 'Asleep', `<p>The services of this endpoint sleep and can not wake now: ${escape(refused ?? 'try again')}.</p>`);
292
295
  }
293
296
  const why = run.stoppedBy === 'ttl'
@@ -124,6 +124,11 @@ class RunRegistry {
124
124
  this.expireLater(entry);
125
125
  }
126
126
  }
127
+ else if (run.state === 'asleep') {
128
+ // its time ran out while the server was down: it ends now, and gives its place back
129
+ run.stoppedBy ?? (run.stoppedBy = 'ttl');
130
+ this.finish(entry, 'succeeded');
131
+ }
127
132
  else if (run.state === 'queued' || run.state === 'running') {
128
133
  this.finish(entry, 'failed', `the server stopped while the run was ${run.state}, submit it again`);
129
134
  }
@@ -146,7 +151,7 @@ class RunRegistry {
146
151
  }
147
152
  this.checkLimits(owner, waiting);
148
153
  if (spec.up) {
149
- this.checkUpLimits(owner);
154
+ this.checkUpLimits(owner, spec.up.name);
150
155
  this.checkHosts(owner, spec.up);
151
156
  }
152
157
  const run = {
@@ -515,20 +520,59 @@ class RunRegistry {
515
520
  throw new QuotaError(`you have ${limits.queued} runs in the queue, wait for one to start or cancel one`);
516
521
  }
517
522
  }
518
- // Service runs that are not over, of the caller and of everybody, against what may be:
519
- // one that is asleep counts, it wakes without asking for a place.
520
- checkUpLimits(owner) {
523
+ // Service runs that are starting or up, of the caller and of everybody, against what may
524
+ // be. One that is asleep does not count: it asks for a place when it wakes (visit). The runs
525
+ // of a caller with one name take one place: the newest replaces the others once it is up,
526
+ // and an older one that gets up after it goes (markUp), so a new run of a name the caller
527
+ // has does not need another. Several of one name that are still starting share it too:
528
+ // what they take while they start is held by runs.limits.running and maxConcurrent.
529
+ checkUpLimits(owner, name) {
530
+ // what a caller keeps asleep is held by a limit of its own: a run that sleeps may keep the
531
+ // volumes of its services. A run of a name the caller has a run of (asleep, up or
532
+ // starting) replaces it, it is not one more.
533
+ const asleep = this.options.up?.asleep;
534
+ if (asleep !== undefined) {
535
+ const open = [...this.entries.values()].filter((entry) => entry.run.up && !isFinal(entry.run.state) && this.owns(owner, entry));
536
+ const sleeping = new Set(open.filter((entry) => entry.run.state === 'asleep').map((entry) => entry.run.up?.name));
537
+ if (!open.some((entry) => entry.run.up?.name === name) && sleeping.size >= asleep) {
538
+ throw new QuotaError(`you have ${asleep} service runs asleep, stop one or wait for its end`);
539
+ }
540
+ }
541
+ const full = this.upLimitReached(owner, name);
542
+ if (full === 'caller') {
543
+ throw new QuotaError(`you have ${this.options.up?.perCaller} service runs starting or up, stop one or wait for its end`);
544
+ }
545
+ if (full === 'server') {
546
+ throw new RunError(`the server has ${this.options.up?.total} service runs starting or up, try again later`);
547
+ }
548
+ }
549
+ // Whose limit a service run of the caller with this name would go over, if any
550
+ upLimitReached(owner, name) {
521
551
  const limits = this.options.up;
522
552
  if (!limits) {
523
- return;
553
+ return undefined;
524
554
  }
525
- const open = [...this.entries.values()].filter((entry) => entry.run.up && !isFinal(entry.run.state));
526
- if (open.filter((entry) => this.owns(owner, entry)).length >= limits.perCaller) {
527
- throw new QuotaError(`you have ${limits.perCaller} service runs starting, up or asleep, stop one or wait for its end`);
555
+ const places = new Set();
556
+ const mine = new Set();
557
+ for (const entry of this.entries.values()) {
558
+ if (!entry.run.up || isFinal(entry.run.state) || entry.run.state === 'asleep') {
559
+ continue;
560
+ }
561
+ places.add(JSON.stringify([entry.run.owner.iss, entry.run.owner.sub, entry.run.up.name]));
562
+ if (this.owns(owner, entry)) {
563
+ mine.add(entry.run.up.name);
564
+ }
565
+ }
566
+ if (mine.has(name)) {
567
+ return undefined;
568
+ }
569
+ if (mine.size >= limits.perCaller) {
570
+ return 'caller';
528
571
  }
529
- if (open.length >= limits.total) {
530
- throw new RunError(`the server has ${limits.total} service runs starting, up or asleep, try again later`);
572
+ if (places.size >= limits.total) {
573
+ return 'server';
531
574
  }
575
+ return undefined;
532
576
  }
533
577
  // A host is one run's, or a run's that replaces it (same owner and name) once it is up:
534
578
  // two labels that come out the same are refused, they would reach each other's services.
@@ -666,6 +710,18 @@ class RunRegistry {
666
710
  this.stop(other, `replaced by ${entry.run.id}`);
667
711
  }
668
712
  }
713
+ // A newer run of the name that is up or asleep already (this one waited for an approval
714
+ // while it got up) is what the owner started last: this one goes, so the runs of a name,
715
+ // which take one place of runs.up.limits, never hold more than one set of services.
716
+ const newer = entries
717
+ .slice(entries.indexOf(entry) + 1)
718
+ .find((other) => other.run.up?.name === spec.name &&
719
+ this.owns(entry.run.owner, other) &&
720
+ (other.run.state === 'up' || other.run.state === 'asleep'));
721
+ if (newer) {
722
+ entry.run.stoppedBy = 'replaced';
723
+ this.stop(entry, `replaced by ${newer.run.id}`);
724
+ }
669
725
  this.dispatch();
670
726
  }
671
727
  // Stops a service run at its expiry, and puts one that is up to sleep at its sleepsAt. A
@@ -719,9 +775,9 @@ class RunRegistry {
719
775
  }
720
776
  /**
721
777
  * A request to an endpoint of a service run: one that is up stays up for its sleepTtl from
722
- * now, one that is asleep is started again (it holds its place of runs.up.limits while it
723
- * sleeps), if `mayWake` finds its owner may still run it: else it ends. Answers why it was
724
- * not woken, or undefined.
778
+ * now, one that is asleep is started again, if `mayWake` finds its owner may still run it
779
+ * (else it ends) and there is a place of runs.up.limits for it (else it sleeps on, and the
780
+ * next request asks again). Answers why it was not woken, or undefined.
725
781
  */
726
782
  visit(id, mayWake) {
727
783
  const entry = this.entries.get(id);
@@ -746,6 +802,13 @@ class RunRegistry {
746
802
  this.finish(entry, 'failed', refused);
747
803
  return refused;
748
804
  }
805
+ const full = this.upLimitReached(entry.run.owner, entry.run.up.name);
806
+ if (full === 'caller') {
807
+ return 'its owner has as many service runs starting or up as they may, it wakes once one of them stops or sleeps';
808
+ }
809
+ if (full === 'server') {
810
+ return 'the server has as many service runs starting or up as it may, it wakes once one of them stops or sleeps';
811
+ }
749
812
  this.clearTimer(entry);
750
813
  entry.abort = new AbortController();
751
814
  entry.released = false;
@@ -525,6 +525,9 @@ exports.serverConfigSchema = zod_1.z
525
525
  .object({
526
526
  // service runs one caller has starting or up at a time; one more is 429
527
527
  up: zod_1.z.number().int().min(1).default(3),
528
+ // service runs one caller has asleep, which take no place of `up`; a new run
529
+ // when there are this many is 429
530
+ asleep: zod_1.z.number().int().min(1).default(10),
528
531
  })
529
532
  .strict()
530
533
  .default({}),
@@ -707,6 +707,7 @@ async function startServer(config, environment, options = {}) {
707
707
  up: {
708
708
  maxTtl: (0, units_1.parseDuration)(config.runs.up.maxTtl),
709
709
  perCaller: config.runs.up.limits.up,
710
+ asleep: config.runs.up.limits.asleep,
710
711
  total: config.runs.up.total,
711
712
  },
712
713
  store: new run_store_1.FileRunStore((0, path_1.join)(storage, 'records')),
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": "1.27.0-next.26",
4
+ "version": "1.27.0-next.27",
5
5
  "keywords": [
6
6
  "build",
7
7
  "build-tool",