hammerkit 1.27.0-next.13 → 1.27.0-next.15
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/docker/item-network.js +13 -5
- package/dist/executer/docker-service.js +30 -8
- package/dist/harness/connect.js +19 -5
- package/dist/harness/mcp-command.js +22 -6
- package/dist/harness/mcp.js +62 -114
- package/dist/harness/opencode.js +151 -50
- package/dist/harness/pi.js +14 -0
- package/dist/harness/sandbox-client.js +81 -2
- package/dist/harness/service-tools.js +141 -0
- package/dist/harness/task-tools.js +210 -0
- package/dist/kubernetes/ensure-kubernetes-service-exists.js +2 -0
- package/dist/planner/utils/parse-work-port.js +4 -1
- package/dist/remote/remote-work-commands.js +98 -12
- package/dist/schema/work-scope-parser.js +6 -0
- package/dist/server/accounts-api.js +2 -1
- package/dist/server/approvals-api.js +1 -1
- package/dist/server/definitions-api.js +4 -2
- package/dist/server/endpoints.js +612 -0
- package/dist/server/executor.js +14 -1
- package/dist/server/kubernetes-backend.js +200 -33
- package/dist/server/plan.js +1 -0
- package/dist/server/policy.js +24 -2
- package/dist/server/repo-access.js +5 -0
- package/dist/server/run-plan.js +38 -4
- package/dist/server/run-request.js +44 -2
- package/dist/server/run-worker.js +130 -9
- package/dist/server/runs.js +317 -24
- package/dist/server/server-config.js +160 -9
- package/dist/server/server.js +210 -14
- package/dist/server/service-runs.js +82 -0
- package/package.json +1 -1
package/dist/server/runs.js
CHANGED
|
@@ -112,9 +112,22 @@ class RunRegistry {
|
|
|
112
112
|
this.finish(entry, 'rejected', 'nobody approved it in time');
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
|
+
else if (this.maySleep(entry) && ['queued', 'running', 'up', 'asleep'].includes(run.state)) {
|
|
116
|
+
// the services went with the process that held them, the next request wakes them
|
|
117
|
+
if (run.state === 'queued' || run.state === 'running' || run.state === 'up') {
|
|
118
|
+
this.sleep(entry);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
this.expireLater(entry);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
115
124
|
else if (run.state === 'queued' || run.state === 'running') {
|
|
116
125
|
this.finish(entry, 'failed', `the server stopped while the run was ${run.state}, submit it again`);
|
|
117
126
|
}
|
|
127
|
+
else if (run.state === 'up') {
|
|
128
|
+
// the services went with the process that held them
|
|
129
|
+
this.finish(entry, 'failed', 'the server stopped while the services were up, start them again');
|
|
130
|
+
}
|
|
118
131
|
}
|
|
119
132
|
this.evict();
|
|
120
133
|
}
|
|
@@ -129,10 +142,18 @@ class RunRegistry {
|
|
|
129
142
|
throw new RunError('too many runs are waiting for an approval');
|
|
130
143
|
}
|
|
131
144
|
this.checkLimits(owner, waiting);
|
|
145
|
+
if (spec.up) {
|
|
146
|
+
this.checkUpLimits(owner);
|
|
147
|
+
this.checkHosts(owner, spec.up);
|
|
148
|
+
}
|
|
132
149
|
const run = {
|
|
133
150
|
...spec,
|
|
134
151
|
id: `run_${(0, crypto_1.randomBytes)(12).toString('base64url')}`,
|
|
135
|
-
owner: {
|
|
152
|
+
owner: {
|
|
153
|
+
iss: owner.iss,
|
|
154
|
+
sub: owner.sub,
|
|
155
|
+
...(spec.up?.sleepTtl ? { groups: [...(owner.groups ?? [])], claims: { ...(owner.claims ?? {}) } } : {}),
|
|
156
|
+
},
|
|
136
157
|
state: waiting ? 'awaiting-approval' : 'queued',
|
|
137
158
|
createdAt: this.timestamp(),
|
|
138
159
|
};
|
|
@@ -179,30 +200,79 @@ class RunRegistry {
|
|
|
179
200
|
.map((entry) => this.copy(entry.run))
|
|
180
201
|
.reverse();
|
|
181
202
|
}
|
|
182
|
-
// A queued run is cancelled at once; a running one when its executor stops.
|
|
203
|
+
// A queued run is cancelled at once; a running one when its executor stops. A service run
|
|
204
|
+
// that is up stops its services, and says who stopped it.
|
|
183
205
|
cancel(owner, id, canUse) {
|
|
184
206
|
const entry = this.find(owner, id, canUse);
|
|
185
207
|
if (!entry) {
|
|
186
208
|
return undefined;
|
|
187
209
|
}
|
|
188
|
-
this.stop(entry);
|
|
210
|
+
this.stop(entry, undefined, { iss: owner.iss, sub: owner.sub });
|
|
189
211
|
return this.copy(entry.run);
|
|
190
212
|
}
|
|
213
|
+
/**
|
|
214
|
+
* Moves the end of a service run that is up (or asleep) to `ttl` (ms) from now, at most
|
|
215
|
+
* the server's maxTtl, and never sooner than it was. Its owner may, and whoever `mayOpen`
|
|
216
|
+
* says may open its endpoints.
|
|
217
|
+
*/
|
|
218
|
+
extend(caller, id, ttl, mayOpen) {
|
|
219
|
+
const entry = this.entries.get(id);
|
|
220
|
+
if (!entry || !(this.owns(caller, entry) || (entry.run.up && mayOpen?.(this.copy(entry.run))))) {
|
|
221
|
+
return { status: 'not-found' };
|
|
222
|
+
}
|
|
223
|
+
// up, asleep, or woken and starting again
|
|
224
|
+
if (!entry.run.upAt || isFinal(entry.run.state)) {
|
|
225
|
+
return { status: 'not-up', state: entry.run.state };
|
|
226
|
+
}
|
|
227
|
+
// never sooner than it was: who wants it gone stops it
|
|
228
|
+
const ms = Math.min(ttl, this.options.up?.maxTtl ?? ttl);
|
|
229
|
+
const expiresAt = Math.max(Date.parse(entry.run.expiresAt ?? '') || 0, this.now() + ms);
|
|
230
|
+
entry.run.expiresAt = new Date(expiresAt).toISOString();
|
|
231
|
+
this.record(entry, { type: 'extended', by: { iss: caller.iss, sub: caller.sub }, expiresAt: entry.run.expiresAt });
|
|
232
|
+
this.expireLater(entry);
|
|
233
|
+
return { status: 'ok', run: this.copy(entry.run) };
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* The service run an endpoint host belongs to: the newest that is up, so one that replaces
|
|
237
|
+
* a run of the same name takes its hosts over as soon as it is up; else the newest that
|
|
238
|
+
* had it, for a page that says how it stands. With the address of the endpoint when up.
|
|
239
|
+
*/
|
|
240
|
+
endpoint(host) {
|
|
241
|
+
let found;
|
|
242
|
+
for (const entry of this.entries.values()) {
|
|
243
|
+
const endpoint = entry.run.up?.endpoints.find((candidate) => candidate.host === host);
|
|
244
|
+
if (endpoint && (entry.run.state === 'up' || found?.entry.run.state !== 'up')) {
|
|
245
|
+
found = { entry, endpoint };
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (!found) {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
const { entry, endpoint } = found;
|
|
252
|
+
const target = entry.run.state === 'up'
|
|
253
|
+
? entry.targets?.find((candidate) => candidate.service === endpoint.service && candidate.port === endpoint.port)
|
|
254
|
+
: undefined;
|
|
255
|
+
return { run: this.copy(entry.run), endpoint: { ...endpoint }, ...(target ? { target } : {}) };
|
|
256
|
+
}
|
|
191
257
|
// The run that holds a sandbox: the oldest on it that is not over. It holds it from
|
|
192
|
-
// when it is submitted, so the files are not moved under what an approver saw.
|
|
258
|
+
// when it is submitted, so the files are not moved under what an approver saw. A service
|
|
259
|
+
// run that is up works on a copy of the files, and holds it no longer.
|
|
193
260
|
holder(sandbox) {
|
|
194
|
-
return [...this.entries.values()].find((entry) => onSandbox(entry.run, sandbox) &&
|
|
195
|
-
.
|
|
261
|
+
return [...this.entries.values()].find((entry) => onSandbox(entry.run, sandbox) &&
|
|
262
|
+
!isFinal(entry.run.state) &&
|
|
263
|
+
entry.run.state !== 'up' &&
|
|
264
|
+
entry.run.state !== 'asleep')?.run.id;
|
|
196
265
|
}
|
|
197
266
|
// Cancels every run on a sandbox and resolves when none of them runs anymore.
|
|
198
267
|
async stopSandbox(sandbox, reason) {
|
|
199
268
|
const on = () => [...this.entries.values()].filter((entry) => onSandbox(entry.run, sandbox));
|
|
200
269
|
on().forEach((entry) => this.stop(entry, reason));
|
|
201
|
-
while (on().some((entry) => entry.run.state === 'running')) {
|
|
270
|
+
while (on().some((entry) => entry.run.state === 'running' || entry.run.state === 'up')) {
|
|
202
271
|
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
203
272
|
}
|
|
204
273
|
}
|
|
205
|
-
stop(entry, reason) {
|
|
274
|
+
stop(entry, reason, by) {
|
|
275
|
+
var _a, _b;
|
|
206
276
|
if (entry.run.state === 'queued') {
|
|
207
277
|
this.queue.splice(this.queue.indexOf(entry), 1);
|
|
208
278
|
this.finish(entry, 'cancelled', reason);
|
|
@@ -213,6 +283,15 @@ class RunRegistry {
|
|
|
213
283
|
else if (entry.run.state === 'running') {
|
|
214
284
|
entry.abort.abort();
|
|
215
285
|
}
|
|
286
|
+
else if (entry.run.state === 'up') {
|
|
287
|
+
(_a = entry.run).stoppedBy ?? (_a.stoppedBy = by ?? 'replaced');
|
|
288
|
+
entry.sleeping = false;
|
|
289
|
+
entry.abort.abort();
|
|
290
|
+
}
|
|
291
|
+
else if (entry.run.state === 'asleep') {
|
|
292
|
+
(_b = entry.run).stoppedBy ?? (_b.stoppedBy = by ?? 'replaced');
|
|
293
|
+
this.finish(entry, 'succeeded');
|
|
294
|
+
}
|
|
216
295
|
}
|
|
217
296
|
// The runs that wait for an approval, of anybody, oldest first. Who may see
|
|
218
297
|
// them is for the caller to decide.
|
|
@@ -289,18 +368,20 @@ class RunRegistry {
|
|
|
289
368
|
return { events, finished, stop: () => entry.listeners.delete(listener) };
|
|
290
369
|
}
|
|
291
370
|
/**
|
|
292
|
-
* The run once it is over, or as it is after `ms` or when
|
|
293
|
-
* that can not hold a stream of events open (a tool call of
|
|
371
|
+
* The run once it is over (a service run: once it is up), or as it is after `ms` or when
|
|
372
|
+
* `signal` aborts, for clients that can not hold a stream of events open (a tool call of
|
|
373
|
+
* an agent).
|
|
294
374
|
*/
|
|
295
375
|
async wait(owner, id, ms, canUse, signal) {
|
|
296
376
|
const entry = this.find(owner, id, canUse);
|
|
297
377
|
if (!entry) {
|
|
298
378
|
return undefined;
|
|
299
379
|
}
|
|
300
|
-
if (!isFinal(entry.run.state) && ms > 0 && !this.closed && !signal?.aborted) {
|
|
380
|
+
if (!isFinal(entry.run.state) && entry.run.state !== 'up' && ms > 0 && !this.closed && !signal?.aborted) {
|
|
301
381
|
await new Promise((resolve) => {
|
|
302
382
|
const listener = (event) => {
|
|
303
|
-
|
|
383
|
+
// a service run is there once it is up
|
|
384
|
+
if (event.type === 'state' && (isFinal(event.state) || event.state === 'up')) {
|
|
304
385
|
done();
|
|
305
386
|
}
|
|
306
387
|
};
|
|
@@ -335,10 +416,19 @@ class RunRegistry {
|
|
|
335
416
|
async close() {
|
|
336
417
|
this.closed = true;
|
|
337
418
|
for (const entry of this.queue.splice(0)) {
|
|
338
|
-
this.
|
|
419
|
+
if (this.maySleep(entry) && this.options.store) {
|
|
420
|
+
// a woken service run sleeps again, the next start keeps it
|
|
421
|
+
this.sleep(entry);
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
this.finish(entry, 'cancelled');
|
|
425
|
+
}
|
|
339
426
|
}
|
|
340
427
|
for (const entry of this.entries.values()) {
|
|
341
|
-
if (entry.run.state === '
|
|
428
|
+
if (entry.run.state === 'asleep') {
|
|
429
|
+
this.clearTimer(entry);
|
|
430
|
+
}
|
|
431
|
+
else if (entry.run.state === 'awaiting-approval') {
|
|
342
432
|
if (this.options.store) {
|
|
343
433
|
// the record keeps it waiting, the next start takes it back
|
|
344
434
|
this.clearTimer(entry);
|
|
@@ -353,11 +443,15 @@ class RunRegistry {
|
|
|
353
443
|
done();
|
|
354
444
|
}
|
|
355
445
|
for (const entry of this.entries.values()) {
|
|
356
|
-
if (entry.run.state === 'running') {
|
|
446
|
+
if (entry.run.state === 'running' || entry.run.state === 'up') {
|
|
447
|
+
// a service run that may sleep is asleep in its record, the next request after the
|
|
448
|
+
// start wakes it
|
|
449
|
+
entry.sleeping = this.maySleep(entry) && !!this.options.store;
|
|
357
450
|
entry.abort.abort();
|
|
358
451
|
}
|
|
359
452
|
}
|
|
360
|
-
while (this.running > 0
|
|
453
|
+
while (this.running > 0 ||
|
|
454
|
+
[...this.entries.values()].some((entry) => entry.run.state === 'running' || entry.run.state === 'up')) {
|
|
361
455
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
362
456
|
}
|
|
363
457
|
// the runs that were stopped are written down before the server is gone
|
|
@@ -388,6 +482,8 @@ class RunRegistry {
|
|
|
388
482
|
copy(run) {
|
|
389
483
|
return {
|
|
390
484
|
...run,
|
|
485
|
+
...(run.up ? { up: JSON.parse(JSON.stringify(run.up)) } : {}),
|
|
486
|
+
...(run.stoppedBy && typeof run.stoppedBy === 'object' ? { stoppedBy: { ...run.stoppedBy } } : {}),
|
|
391
487
|
tasks: [...run.tasks],
|
|
392
488
|
source: { ...run.source },
|
|
393
489
|
options: JSON.parse(JSON.stringify(run.options)),
|
|
@@ -415,6 +511,37 @@ class RunRegistry {
|
|
|
415
511
|
throw new QuotaError(`you have ${limits.queued} runs in the queue, wait for one to start or cancel one`);
|
|
416
512
|
}
|
|
417
513
|
}
|
|
514
|
+
// Service runs that are not over, of the caller and of everybody, against what may be:
|
|
515
|
+
// one that is asleep counts, it wakes without asking for a place.
|
|
516
|
+
checkUpLimits(owner) {
|
|
517
|
+
const limits = this.options.up;
|
|
518
|
+
if (!limits) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const open = [...this.entries.values()].filter((entry) => entry.run.up && !isFinal(entry.run.state));
|
|
522
|
+
if (open.filter((entry) => this.owns(owner, entry)).length >= limits.perCaller) {
|
|
523
|
+
throw new QuotaError(`you have ${limits.perCaller} service runs starting, up or asleep, stop one or wait for its end`);
|
|
524
|
+
}
|
|
525
|
+
if (open.length >= limits.total) {
|
|
526
|
+
throw new RunError(`the server has ${limits.total} service runs starting, up or asleep, try again later`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
// A host is one run's, or a run's that replaces it (same owner and name) once it is up:
|
|
530
|
+
// two labels that come out the same are refused, they would reach each other's services.
|
|
531
|
+
checkHosts(owner, up) {
|
|
532
|
+
const hosts = new Set(up.endpoints.map((endpoint) => endpoint.host));
|
|
533
|
+
for (const entry of this.entries.values()) {
|
|
534
|
+
if (isFinal(entry.run.state) || !entry.run.up) {
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
const taken = entry.run.up.endpoints.find((endpoint) => hosts.has(endpoint.host));
|
|
538
|
+
if (taken && !(this.owns(owner, entry) && entry.run.up.name === up.name)) {
|
|
539
|
+
const error = new RunError(`the address ${taken.host} is another service run's, pick another name`);
|
|
540
|
+
error.status = 409;
|
|
541
|
+
throw error;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
418
545
|
runningOf(owner) {
|
|
419
546
|
return [...this.entries.values()].filter((entry) => entry.run.state === 'running' && this.owns(owner, entry)).length;
|
|
420
547
|
}
|
|
@@ -444,7 +571,10 @@ class RunRegistry {
|
|
|
444
571
|
const [entry] = this.queue.splice(index, 1);
|
|
445
572
|
this.running++;
|
|
446
573
|
this.execute(entry).finally(() => {
|
|
447
|
-
|
|
574
|
+
// a service run that is up gave its place back already
|
|
575
|
+
if (!entry.released) {
|
|
576
|
+
this.running--;
|
|
577
|
+
}
|
|
448
578
|
this.dispatch();
|
|
449
579
|
});
|
|
450
580
|
}
|
|
@@ -453,27 +583,188 @@ class RunRegistry {
|
|
|
453
583
|
entry.run.state = 'running';
|
|
454
584
|
entry.run.startedAt = this.timestamp();
|
|
455
585
|
this.record(entry, { type: 'state', state: 'running' });
|
|
586
|
+
// a service run gets this long to be up, then its ttl counts
|
|
456
587
|
const limit = this.options.maxDuration;
|
|
457
|
-
|
|
588
|
+
entry.durationTimer = limit
|
|
458
589
|
? setTimeout(() => {
|
|
459
590
|
entry.timedOut = true;
|
|
460
591
|
entry.abort.abort();
|
|
461
592
|
}, limit)
|
|
462
593
|
: undefined;
|
|
463
|
-
|
|
594
|
+
entry.durationTimer?.unref();
|
|
464
595
|
try {
|
|
465
596
|
await this.options.executor.execute(this.copy(entry.run), {
|
|
466
597
|
emit: (event) => this.record(entry, event),
|
|
467
598
|
signal: entry.abort.signal,
|
|
599
|
+
...(entry.run.up ? { up: (targets) => this.markUp(entry, targets) } : {}),
|
|
468
600
|
});
|
|
469
|
-
|
|
601
|
+
if (entry.sleeping) {
|
|
602
|
+
this.sleep(entry);
|
|
603
|
+
}
|
|
604
|
+
else if (entry.run.upAt) {
|
|
605
|
+
// services stay until they are stopped: ones that stopped on their own failed
|
|
606
|
+
this.endServices(entry, 'the services stopped on their own');
|
|
607
|
+
}
|
|
608
|
+
else {
|
|
609
|
+
this.finish(entry, entry.timedOut ? 'failed' : entry.abort.signal.aborted ? 'cancelled' : 'succeeded', entry.timedOut ? this.tooLong() : undefined);
|
|
610
|
+
}
|
|
470
611
|
}
|
|
471
612
|
catch (e) {
|
|
472
|
-
const
|
|
473
|
-
|
|
613
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
614
|
+
if (entry.sleeping) {
|
|
615
|
+
this.sleep(entry);
|
|
616
|
+
}
|
|
617
|
+
else if (entry.run.upAt) {
|
|
618
|
+
this.endServices(entry, message);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
const cancelled = entry.abort.signal.aborted && !entry.timedOut;
|
|
622
|
+
this.finish(entry, cancelled ? 'cancelled' : 'failed', entry.timedOut ? this.tooLong() : cancelled ? undefined : message);
|
|
623
|
+
}
|
|
474
624
|
}
|
|
475
625
|
finally {
|
|
476
|
-
clearTimeout(
|
|
626
|
+
clearTimeout(entry.durationTimer);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// The services of a service run are ready: its ttl counts from now, its place of
|
|
630
|
+
// maxConcurrent goes to the next run, and an older run of its owner with the same name
|
|
631
|
+
// stops, so its hosts are this run's.
|
|
632
|
+
markUp(entry, targets) {
|
|
633
|
+
const spec = entry.run.up;
|
|
634
|
+
if (!spec || entry.run.state !== 'running' || entry.abort.signal.aborted) {
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
clearTimeout(entry.durationTimer);
|
|
638
|
+
entry.durationTimer = undefined;
|
|
639
|
+
entry.targets = targets.map((target) => ({ ...target, address: { ...target.address } }));
|
|
640
|
+
entry.run.state = 'up';
|
|
641
|
+
// the ttl counts from the first time, not from a wake
|
|
642
|
+
if (!entry.run.upAt) {
|
|
643
|
+
entry.run.upAt = this.timestamp();
|
|
644
|
+
entry.run.expiresAt = new Date(this.now() + spec.ttl).toISOString();
|
|
645
|
+
}
|
|
646
|
+
if (spec.sleepTtl) {
|
|
647
|
+
entry.run.sleepsAt = new Date(this.now() + spec.sleepTtl).toISOString();
|
|
648
|
+
}
|
|
649
|
+
this.record(entry, { type: 'state', state: 'up' });
|
|
650
|
+
this.expireLater(entry);
|
|
651
|
+
entry.released = true;
|
|
652
|
+
this.running--;
|
|
653
|
+
const entries = [...this.entries.values()];
|
|
654
|
+
for (const other of entries.slice(0, entries.indexOf(entry))) {
|
|
655
|
+
if (other.run.up?.name === spec.name &&
|
|
656
|
+
this.owns(entry.run.owner, other) &&
|
|
657
|
+
!isFinal(other.run.state) &&
|
|
658
|
+
other.run.state !== 'awaiting-approval') {
|
|
659
|
+
other.run.stoppedBy = 'replaced';
|
|
660
|
+
this.stop(other, `replaced by ${entry.run.id}`);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
this.dispatch();
|
|
664
|
+
}
|
|
665
|
+
// Stops a service run at its expiry, and puts one that is up to sleep at its sleepsAt. A
|
|
666
|
+
// timer can't wait longer than about 24 days, so a longer one looks again then.
|
|
667
|
+
expireLater(entry) {
|
|
668
|
+
this.clearTimer(entry);
|
|
669
|
+
const expiresAt = Date.parse(entry.run.expiresAt ?? '');
|
|
670
|
+
const sleepsAt = entry.run.state === 'up' ? Date.parse(entry.run.sleepsAt ?? '') : NaN;
|
|
671
|
+
const next = Number.isFinite(sleepsAt) ? Math.min(expiresAt, sleepsAt) : expiresAt;
|
|
672
|
+
entry.timer = setTimeout(() => {
|
|
673
|
+
var _a;
|
|
674
|
+
if (isFinal(entry.run.state)) {
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (Date.parse(entry.run.expiresAt ?? '') <= this.now()) {
|
|
678
|
+
(_a = entry.run).stoppedBy ?? (_a.stoppedBy = 'ttl');
|
|
679
|
+
entry.sleeping = false;
|
|
680
|
+
if (entry.run.state === 'asleep') {
|
|
681
|
+
this.finish(entry, 'succeeded');
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
this.stop(entry);
|
|
685
|
+
}
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (entry.run.state === 'up' && Date.parse(entry.run.sleepsAt ?? '') <= this.now()) {
|
|
689
|
+
entry.sleeping = true;
|
|
690
|
+
entry.abort.abort();
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.expireLater(entry);
|
|
694
|
+
}, Math.max(0, Math.min(next - this.now(), 2 ** 31 - 1)));
|
|
695
|
+
entry.timer.unref();
|
|
696
|
+
}
|
|
697
|
+
// Whether a service run sleeps instead of ending: it has a sleepTtl, its ttl has not
|
|
698
|
+
// ended, and it starts from a commit (the files of a sandbox change under it).
|
|
699
|
+
maySleep(entry) {
|
|
700
|
+
return (!!entry.run.up?.sleepTtl &&
|
|
701
|
+
!!entry.run.upAt &&
|
|
702
|
+
entry.run.source.type === 'ref' &&
|
|
703
|
+
Date.parse(entry.run.expiresAt ?? '') > this.now());
|
|
704
|
+
}
|
|
705
|
+
// The services stopped, the run waits for a request to wake it, or for its end.
|
|
706
|
+
sleep(entry) {
|
|
707
|
+
entry.sleeping = false;
|
|
708
|
+
entry.targets = undefined;
|
|
709
|
+
entry.run.state = 'asleep';
|
|
710
|
+
delete entry.run.sleepsAt;
|
|
711
|
+
this.record(entry, { type: 'state', state: 'asleep' });
|
|
712
|
+
this.expireLater(entry);
|
|
713
|
+
}
|
|
714
|
+
/**
|
|
715
|
+
* A request to an endpoint of a service run: one that is up stays up for its sleepTtl from
|
|
716
|
+
* now, one that is asleep is started again (it holds its place of runs.up.limits while it
|
|
717
|
+
* sleeps), if `mayWake` finds its owner may still run it: else it ends. Answers why it was
|
|
718
|
+
* not woken, or undefined.
|
|
719
|
+
*/
|
|
720
|
+
visit(id, mayWake) {
|
|
721
|
+
const entry = this.entries.get(id);
|
|
722
|
+
if (!entry?.run.up) {
|
|
723
|
+
return undefined;
|
|
724
|
+
}
|
|
725
|
+
const sleepTtl = entry.run.up.sleepTtl;
|
|
726
|
+
if (entry.run.state === 'up' && sleepTtl) {
|
|
727
|
+
// only kept in memory: a server that stops puts the run to sleep anyway
|
|
728
|
+
entry.run.sleepsAt = new Date(this.now() + sleepTtl).toISOString();
|
|
729
|
+
return undefined;
|
|
730
|
+
}
|
|
731
|
+
if (entry.run.state !== 'asleep' || this.closed) {
|
|
732
|
+
return undefined;
|
|
733
|
+
}
|
|
734
|
+
if (!this.maySleep(entry)) {
|
|
735
|
+
this.expireLater(entry);
|
|
736
|
+
return 'its time ran out';
|
|
737
|
+
}
|
|
738
|
+
const refused = mayWake?.(this.copy(entry.run));
|
|
739
|
+
if (refused) {
|
|
740
|
+
this.finish(entry, 'failed', refused);
|
|
741
|
+
return refused;
|
|
742
|
+
}
|
|
743
|
+
this.clearTimer(entry);
|
|
744
|
+
entry.abort = new AbortController();
|
|
745
|
+
entry.released = false;
|
|
746
|
+
entry.timedOut = false;
|
|
747
|
+
entry.run.wakes = (entry.run.wakes ?? 0) + 1;
|
|
748
|
+
entry.run.state = 'queued';
|
|
749
|
+
this.record(entry, { type: 'state', state: 'queued' });
|
|
750
|
+
this.queue.push(entry);
|
|
751
|
+
this.dispatch();
|
|
752
|
+
return undefined;
|
|
753
|
+
}
|
|
754
|
+
// The end of a run that was up: stopped by somebody, its ttl or a newer run it succeeded,
|
|
755
|
+
// stopped by the server going away it failed, as its record says after a restart.
|
|
756
|
+
endServices(entry, failure) {
|
|
757
|
+
if (entry.timedOut) {
|
|
758
|
+
this.finish(entry, 'failed', this.tooLong());
|
|
759
|
+
}
|
|
760
|
+
else if (!entry.abort.signal.aborted) {
|
|
761
|
+
this.finish(entry, 'failed', failure);
|
|
762
|
+
}
|
|
763
|
+
else if (entry.run.stoppedBy) {
|
|
764
|
+
this.finish(entry, 'succeeded');
|
|
765
|
+
}
|
|
766
|
+
else {
|
|
767
|
+
this.finish(entry, 'failed', 'the server stopped while the services were up, start them again');
|
|
477
768
|
}
|
|
478
769
|
}
|
|
479
770
|
tooLong() {
|
|
@@ -481,6 +772,8 @@ class RunRegistry {
|
|
|
481
772
|
}
|
|
482
773
|
finish(entry, state, error) {
|
|
483
774
|
this.clearTimer(entry);
|
|
775
|
+
clearTimeout(entry.durationTimer);
|
|
776
|
+
entry.targets = undefined;
|
|
484
777
|
entry.run.state = state;
|
|
485
778
|
entry.run.finishedAt = this.timestamp();
|
|
486
779
|
if (error) {
|
|
@@ -579,7 +872,7 @@ class RunRegistry {
|
|
|
579
872
|
}
|
|
580
873
|
// what a restart must not lose: the runs that wait, and how a run ended. A running
|
|
581
874
|
// run is not kept up to date, it fails with the server.
|
|
582
|
-
if (event.type === 'state' || event.type === 'approval') {
|
|
875
|
+
if (event.type === 'state' || event.type === 'approval' || event.type === 'extended') {
|
|
583
876
|
this.persist(entry);
|
|
584
877
|
}
|
|
585
878
|
}
|