@shipfox/api-runners 12.2.0 → 12.4.0
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/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +34 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -3
- package/dist/config.js.map +1 -1
- package/dist/core/demand.d.ts +4 -2
- package/dist/core/demand.d.ts.map +1 -1
- package/dist/core/demand.js +22 -6
- package/dist/core/demand.js.map +1 -1
- package/dist/core/entities/reservation.d.ts +1 -0
- package/dist/core/entities/reservation.d.ts.map +1 -1
- package/dist/core/entities/reservation.js.map +1 -1
- package/dist/core/entities/runner-instance.d.ts +3 -0
- package/dist/core/entities/runner-instance.d.ts.map +1 -1
- package/dist/core/entities/runner-instance.js.map +1 -1
- package/dist/core/runner-activation.d.ts +4 -1
- package/dist/core/runner-activation.d.ts.map +1 -1
- package/dist/core/runner-activation.js +32 -3
- package/dist/core/runner-activation.js.map +1 -1
- package/dist/core/runner-control-sessions.d.ts +8 -4
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +69 -11
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/core/runner-instances.d.ts +1 -0
- package/dist/core/runner-instances.d.ts.map +1 -1
- package/dist/core/runner-instances.js +1 -0
- package/dist/core/runner-instances.js.map +1 -1
- package/dist/db/db.d.ts +68 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.js +1 -1
- package/dist/db/ephemeral-registration-tokens.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/reservation-locks.d.ts +6 -0
- package/dist/db/reservation-locks.d.ts.map +1 -0
- package/dist/db/reservation-locks.js +15 -0
- package/dist/db/reservation-locks.js.map +1 -0
- package/dist/db/reservations.d.ts +17 -3
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +256 -45
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +17 -0
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +103 -8
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +5 -2
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +134 -25
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-sessions.d.ts.map +1 -1
- package/dist/db/runner-sessions.js +22 -15
- package/dist/db/runner-sessions.js.map +1 -1
- package/dist/db/runner-states.d.ts +2 -0
- package/dist/db/runner-states.d.ts.map +1 -0
- package/dist/db/runner-states.js +7 -0
- package/dist/db/runner-states.js.map +1 -0
- package/dist/db/schema/reservations.d.ts +18 -0
- package/dist/db/schema/reservations.d.ts.map +1 -1
- package/dist/db/schema/reservations.js +7 -1
- package/dist/db/schema/reservations.js.map +1 -1
- package/dist/db/schema/runner-instances.d.ts +18 -0
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +8 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/metrics/index.d.ts +2 -2
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +24 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +23 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +20 -4
- package/dist/metrics/service.js.map +1 -1
- package/dist/presentation/dto/poll-demand.d.ts.map +1 -1
- package/dist/presentation/dto/poll-demand.js +3 -0
- package/dist/presentation/dto/poll-demand.js.map +1 -1
- package/dist/presentation/dto/runner-instances.d.ts +4 -1
- package/dist/presentation/dto/runner-instances.d.ts.map +1 -1
- package/dist/presentation/dto/runner-instances.js +4 -1
- package/dist/presentation/dto/runner-instances.js.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.js +3 -3
- package/dist/presentation/routes/assign-runner-instances.js.map +1 -1
- package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.js +1 -0
- package/dist/presentation/routes/poll-demand.js.map +1 -1
- package/dist/presentation/routes/reconcile-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/reconcile-runner-instances.js +4 -2
- package/dist/presentation/routes/reconcile-runner-instances.js.map +1 -1
- package/dist/presentation/routes/runner-enrollment.d.ts +4 -1
- package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
- package/dist/presentation/routes/runner-enrollment.js +11 -6
- package/dist/presentation/routes/runner-enrollment.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0005_previous_lenny_balinger.sql +5 -0
- package/drizzle/meta/0005_snapshot.json +2220 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/src/config.test.ts +24 -2
- package/src/config.ts +16 -3
- package/src/core/demand.test.ts +192 -3
- package/src/core/demand.ts +44 -4
- package/src/core/entities/reservation.ts +1 -0
- package/src/core/entities/runner-instance.ts +4 -0
- package/src/core/runner-activation.ts +48 -2
- package/src/core/runner-control-sessions.test.ts +230 -4
- package/src/core/runner-control-sessions.ts +84 -10
- package/src/core/runner-instances.test.ts +19 -0
- package/src/core/runner-instances.ts +2 -0
- package/src/core/runner-sessions.test.ts +121 -1
- package/src/db/ephemeral-registration-tokens.test.ts +20 -1
- package/src/db/ephemeral-registration-tokens.ts +1 -0
- package/src/db/index.ts +1 -0
- package/src/db/reservation-locks.ts +22 -0
- package/src/db/reservations.test.ts +1110 -31
- package/src/db/reservations.ts +434 -105
- package/src/db/runner-assignments.test.ts +105 -1
- package/src/db/runner-assignments.ts +178 -9
- package/src/db/runner-instances.test.ts +496 -2
- package/src/db/runner-instances.ts +247 -56
- package/src/db/runner-sessions.ts +26 -15
- package/src/db/runner-states.ts +7 -0
- package/src/db/schema/reservations.ts +5 -1
- package/src/db/schema/runner-instances.ts +9 -0
- package/src/metrics/index.ts +10 -1
- package/src/metrics/instance.ts +59 -2
- package/src/metrics/service.test.ts +106 -0
- package/src/metrics/service.ts +24 -4
- package/src/presentation/dto/poll-demand.ts +3 -0
- package/src/presentation/dto/runner-instances.ts +8 -1
- package/src/presentation/routes/assign-runner-instances.ts +6 -2
- package/src/presentation/routes/late-runner-enrollment.test.ts +447 -0
- package/src/presentation/routes/poll-demand.test.ts +79 -6
- package/src/presentation/routes/poll-demand.ts +1 -0
- package/src/presentation/routes/reconcile-runner-instances.test.ts +34 -2
- package/src/presentation/routes/reconcile-runner-instances.ts +7 -1
- package/src/presentation/routes/runner-enrollment.test.ts +316 -3
- package/src/presentation/routes/runner-enrollment.ts +8 -5
- package/test/factories/reservation.ts +2 -0
- package/test/factories/runner-instance.ts +6 -0
- package/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -4,6 +4,7 @@ import {db} from '#db/db.js';
|
|
|
4
4
|
import {createRunnerSessionConsumingEphemeralToken} from '#db/ephemeral-registration-tokens.js';
|
|
5
5
|
import {
|
|
6
6
|
attachRunnerInstanceProviderId,
|
|
7
|
+
countStaleEnrolledRunnerInstances,
|
|
7
8
|
listActiveRunnerInstanceCountsByTemplateTx,
|
|
8
9
|
listActiveRunnerInstances,
|
|
9
10
|
listProvisionerTerminateIntentRowsTx,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
} from '#db/runner-instances.js';
|
|
15
16
|
import {provisionerTokens} from '#db/schema/provisioner-tokens.js';
|
|
16
17
|
import {reservations} from '#db/schema/reservations.js';
|
|
18
|
+
import {runnerControlSessions} from '#db/schema/runner-control-sessions.js';
|
|
17
19
|
import {providerRunners} from '#db/schema/runner-instances.js';
|
|
18
20
|
import {runnerSessions} from '#db/schema/runner-sessions.js';
|
|
19
21
|
import {runningJobExecutions} from '#db/schema/running-job-executions.js';
|
|
@@ -144,6 +146,28 @@ describe('reportRunnerInstances', () => {
|
|
|
144
146
|
expect(rows[0]?.state).toBe('running');
|
|
145
147
|
});
|
|
146
148
|
|
|
149
|
+
it('preserves launch kind when a provider report updates the runner row', async () => {
|
|
150
|
+
await providerRunnerFactory.create({
|
|
151
|
+
workspaceId,
|
|
152
|
+
provisionerId,
|
|
153
|
+
providerRunnerId: 'demand-runner',
|
|
154
|
+
launchKind: 'demand',
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
await reportRunnerInstances({
|
|
158
|
+
scope: 'workspace',
|
|
159
|
+
workspaceId,
|
|
160
|
+
provisionerId,
|
|
161
|
+
events: [event({providerRunnerId: 'demand-runner', state: 'running'})],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const [row] = await db()
|
|
165
|
+
.select({launchKind: providerRunners.launchKind})
|
|
166
|
+
.from(providerRunners)
|
|
167
|
+
.where(eq(providerRunners.providerRunnerId, 'demand-runner'));
|
|
168
|
+
expect(row?.launchKind).toBe('demand');
|
|
169
|
+
});
|
|
170
|
+
|
|
147
171
|
it('uses state progression to dedupe equal-timestamp provisioned runner reports', async () => {
|
|
148
172
|
const reportedAt = new Date();
|
|
149
173
|
|
|
@@ -231,6 +255,191 @@ describe('reportRunnerInstances', () => {
|
|
|
231
255
|
expect(rows[0]?.reason).toBe('fresh');
|
|
232
256
|
});
|
|
233
257
|
|
|
258
|
+
it('preserves a rebound reservation through stale reports and releases it on termination', async () => {
|
|
259
|
+
const staleReservationId = await createReservation(1);
|
|
260
|
+
await db()
|
|
261
|
+
.update(reservations)
|
|
262
|
+
.set({expiresAt: new Date(Date.now() - 60_000)})
|
|
263
|
+
.where(eq(reservations.id, staleReservationId));
|
|
264
|
+
const reboundReservationId = await createReservation(2);
|
|
265
|
+
const initialReportedAt = new Date(Date.now() - 120_000);
|
|
266
|
+
const runningReportedAt = new Date(Date.now() - 60_000);
|
|
267
|
+
await providerRunnerFactory.create({
|
|
268
|
+
workspaceId,
|
|
269
|
+
provisionerId,
|
|
270
|
+
providerRunnerId: 'rebound-runner',
|
|
271
|
+
reservationId: reboundReservationId,
|
|
272
|
+
state: 'running',
|
|
273
|
+
reportedAt: initialReportedAt,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
const runningReport = await reportRunnerInstances({
|
|
277
|
+
scope: 'workspace',
|
|
278
|
+
workspaceId,
|
|
279
|
+
provisionerId,
|
|
280
|
+
events: [
|
|
281
|
+
event({
|
|
282
|
+
providerRunnerId: 'rebound-runner',
|
|
283
|
+
reservationId: staleReservationId,
|
|
284
|
+
reportedAt: runningReportedAt,
|
|
285
|
+
}),
|
|
286
|
+
],
|
|
287
|
+
});
|
|
288
|
+
const terminalReport = await reportRunnerInstances({
|
|
289
|
+
scope: 'workspace',
|
|
290
|
+
workspaceId,
|
|
291
|
+
provisionerId,
|
|
292
|
+
events: [
|
|
293
|
+
event({
|
|
294
|
+
providerRunnerId: 'rebound-runner',
|
|
295
|
+
reservationId: staleReservationId,
|
|
296
|
+
state: 'failed',
|
|
297
|
+
reportedAt: new Date(runningReportedAt.getTime() + 1_000),
|
|
298
|
+
}),
|
|
299
|
+
],
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
const [runner] = await db()
|
|
303
|
+
.select()
|
|
304
|
+
.from(providerRunners)
|
|
305
|
+
.where(eq(providerRunners.providerRunnerId, 'rebound-runner'));
|
|
306
|
+
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
307
|
+
expect(runningReport).toEqual({
|
|
308
|
+
accepted: 1,
|
|
309
|
+
reservationsReleased: 0,
|
|
310
|
+
terminateIntentsHonored: [],
|
|
311
|
+
});
|
|
312
|
+
expect(terminalReport).toEqual({
|
|
313
|
+
accepted: 1,
|
|
314
|
+
reservationsReleased: 1,
|
|
315
|
+
terminateIntentsHonored: [],
|
|
316
|
+
});
|
|
317
|
+
expect(runner).toMatchObject({
|
|
318
|
+
reservationId: reboundReservationId,
|
|
319
|
+
reservationReleasedAt: expect.any(Date),
|
|
320
|
+
});
|
|
321
|
+
expect(
|
|
322
|
+
reservationRows.find((reservation) => reservation.id === reboundReservationId)?.count,
|
|
323
|
+
).toBe(1);
|
|
324
|
+
expect(
|
|
325
|
+
reservationRows.find((reservation) => reservation.id === staleReservationId)?.count,
|
|
326
|
+
).toBe(1);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('keeps the stored reservation when a report carries a stale reservation id', async () => {
|
|
330
|
+
const staleReservationId = await createReservation(1);
|
|
331
|
+
const boundReservationId = await createReservation(1);
|
|
332
|
+
await db()
|
|
333
|
+
.insert(providerRunners)
|
|
334
|
+
.values({
|
|
335
|
+
workspaceId,
|
|
336
|
+
provisionerId,
|
|
337
|
+
reservationId: boundReservationId,
|
|
338
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
339
|
+
state: 'running',
|
|
340
|
+
reportedAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
await reportRunnerInstances({
|
|
344
|
+
scope: 'workspace',
|
|
345
|
+
workspaceId,
|
|
346
|
+
provisionerId,
|
|
347
|
+
events: [
|
|
348
|
+
event({
|
|
349
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
350
|
+
reservationId: staleReservationId,
|
|
351
|
+
state: 'running',
|
|
352
|
+
reportedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
353
|
+
}),
|
|
354
|
+
],
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
358
|
+
expect(rows[0]?.reservationId).toBe(boundReservationId);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('adopts a reported reservation id when the runner carries none', async () => {
|
|
362
|
+
const reservationId = await createReservation(1);
|
|
363
|
+
await db()
|
|
364
|
+
.insert(providerRunners)
|
|
365
|
+
.values({
|
|
366
|
+
workspaceId,
|
|
367
|
+
provisionerId,
|
|
368
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
369
|
+
state: 'starting',
|
|
370
|
+
reportedAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
await reportRunnerInstances({
|
|
374
|
+
scope: 'workspace',
|
|
375
|
+
workspaceId,
|
|
376
|
+
provisionerId,
|
|
377
|
+
events: [
|
|
378
|
+
event({
|
|
379
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
380
|
+
reservationId,
|
|
381
|
+
state: 'running',
|
|
382
|
+
reportedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
383
|
+
}),
|
|
384
|
+
],
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
388
|
+
expect(rows[0]?.reservationId).toBe(reservationId);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
it('does not adopt a bound reservation id from an unclaimed report', async () => {
|
|
392
|
+
const reservationId = await createReservation(1, {kind: 'bound'});
|
|
393
|
+
await db()
|
|
394
|
+
.insert(providerRunners)
|
|
395
|
+
.values({
|
|
396
|
+
workspaceId,
|
|
397
|
+
provisionerId,
|
|
398
|
+
providerRunnerId: 'bound-reservation-runner',
|
|
399
|
+
state: 'starting',
|
|
400
|
+
reportedAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
await reportRunnerInstances({
|
|
404
|
+
scope: 'workspace',
|
|
405
|
+
workspaceId,
|
|
406
|
+
provisionerId,
|
|
407
|
+
events: [
|
|
408
|
+
event({
|
|
409
|
+
providerRunnerId: 'bound-reservation-runner',
|
|
410
|
+
reservationId,
|
|
411
|
+
state: 'running',
|
|
412
|
+
reportedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
413
|
+
}),
|
|
414
|
+
],
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
418
|
+
expect(rows[0]?.reservationId).toBeNull();
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('does not let reports exceed reservation capacity when creating projection rows', async () => {
|
|
422
|
+
const reservationId = await createReservation(1);
|
|
423
|
+
|
|
424
|
+
await reportRunnerInstances({
|
|
425
|
+
scope: 'workspace',
|
|
426
|
+
workspaceId,
|
|
427
|
+
provisionerId,
|
|
428
|
+
events: [
|
|
429
|
+
event({providerRunnerId: 'provisioned-runner-1', reservationId}),
|
|
430
|
+
event({providerRunnerId: 'provisioned-runner-2', reservationId}),
|
|
431
|
+
],
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
435
|
+
expect(rows.find((row) => row.providerRunnerId === 'provisioned-runner-1')?.reservationId).toBe(
|
|
436
|
+
reservationId,
|
|
437
|
+
);
|
|
438
|
+
expect(
|
|
439
|
+
rows.find((row) => row.providerRunnerId === 'provisioned-runner-2')?.reservationId,
|
|
440
|
+
).toBeNull();
|
|
441
|
+
});
|
|
442
|
+
|
|
234
443
|
it('does not let equal-timestamp lower-priority reports flip terminal state', async () => {
|
|
235
444
|
const reservationId = await createReservation(1);
|
|
236
445
|
const reportedAt = new Date('2025-01-01T00:00:00.000Z');
|
|
@@ -419,8 +628,15 @@ describe('reportRunnerInstances', () => {
|
|
|
419
628
|
expect(active).toEqual([]);
|
|
420
629
|
});
|
|
421
630
|
|
|
422
|
-
it('releases one reservation unit for a terminal
|
|
631
|
+
it('releases one reservation unit for a terminal runner with an intended reservation', async () => {
|
|
423
632
|
const reservationId = await createReservation(2);
|
|
633
|
+
await providerRunnerFactory.create({
|
|
634
|
+
workspaceId,
|
|
635
|
+
provisionerId,
|
|
636
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
637
|
+
intendedReservationId: reservationId,
|
|
638
|
+
state: 'running',
|
|
639
|
+
});
|
|
424
640
|
|
|
425
641
|
const result = await reportRunnerInstances({
|
|
426
642
|
scope: 'workspace',
|
|
@@ -436,6 +652,30 @@ describe('reportRunnerInstances', () => {
|
|
|
436
652
|
expect(providerRunnerRows[0]?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
437
653
|
});
|
|
438
654
|
|
|
655
|
+
it('does not let an unrecognized terminal report consume reservation capacity', async () => {
|
|
656
|
+
const reservationId = await createReservation(1);
|
|
657
|
+
|
|
658
|
+
const result = await reportRunnerInstances({
|
|
659
|
+
scope: 'workspace',
|
|
660
|
+
workspaceId,
|
|
661
|
+
provisionerId,
|
|
662
|
+
events: [event({providerRunnerId: 'unrecognized-runner', reservationId, state: 'failed'})],
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
666
|
+
const providerRunnerRows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
667
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 0, terminateIntentsHonored: []});
|
|
668
|
+
expect(reservationRows[0]?.count).toBe(1);
|
|
669
|
+
expect(providerRunnerRows).toHaveLength(1);
|
|
670
|
+
expect(providerRunnerRows[0]).toMatchObject({
|
|
671
|
+
providerRunnerId: 'unrecognized-runner',
|
|
672
|
+
reservationId: null,
|
|
673
|
+
intendedReservationId: null,
|
|
674
|
+
reservationReleasedAt: null,
|
|
675
|
+
state: 'failed',
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
439
679
|
it('releases an intended reservation for a runner that dies before enrollment', async () => {
|
|
440
680
|
const reservationId = await createReservation(2);
|
|
441
681
|
const [runner] = await db()
|
|
@@ -606,6 +846,13 @@ describe('reportRunnerInstances', () => {
|
|
|
606
846
|
|
|
607
847
|
it('releases a reservation only once across repeated terminal reports', async () => {
|
|
608
848
|
const reservationId = await createReservation(2);
|
|
849
|
+
await providerRunnerFactory.create({
|
|
850
|
+
workspaceId,
|
|
851
|
+
provisionerId,
|
|
852
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
853
|
+
intendedReservationId: reservationId,
|
|
854
|
+
state: 'running',
|
|
855
|
+
});
|
|
609
856
|
|
|
610
857
|
await reportRunnerInstances({
|
|
611
858
|
scope: 'workspace',
|
|
@@ -627,6 +874,13 @@ describe('reportRunnerInstances', () => {
|
|
|
627
874
|
|
|
628
875
|
it('does not let a newer running report revive a terminal provisioned runner', async () => {
|
|
629
876
|
const reservationId = await createReservation(2);
|
|
877
|
+
await providerRunnerFactory.create({
|
|
878
|
+
workspaceId,
|
|
879
|
+
provisionerId,
|
|
880
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
881
|
+
intendedReservationId: reservationId,
|
|
882
|
+
state: 'running',
|
|
883
|
+
});
|
|
630
884
|
const failedAt = new Date();
|
|
631
885
|
await reportRunnerInstances({
|
|
632
886
|
scope: 'workspace',
|
|
@@ -665,6 +919,13 @@ describe('reportRunnerInstances', () => {
|
|
|
665
919
|
|
|
666
920
|
it('tracks provider cleanup as terminated', async () => {
|
|
667
921
|
const reservationId = await createReservation(2);
|
|
922
|
+
await providerRunnerFactory.create({
|
|
923
|
+
workspaceId,
|
|
924
|
+
provisionerId,
|
|
925
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
926
|
+
intendedReservationId: reservationId,
|
|
927
|
+
state: 'running',
|
|
928
|
+
});
|
|
668
929
|
|
|
669
930
|
const result = await reportRunnerInstances({
|
|
670
931
|
scope: 'workspace',
|
|
@@ -686,6 +947,20 @@ describe('reportRunnerInstances', () => {
|
|
|
686
947
|
|
|
687
948
|
it('releases multiple units from the same reservation in one batch', async () => {
|
|
688
949
|
const reservationId = await createReservation(3);
|
|
950
|
+
await providerRunnerFactory.create({
|
|
951
|
+
workspaceId,
|
|
952
|
+
provisionerId,
|
|
953
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
954
|
+
intendedReservationId: reservationId,
|
|
955
|
+
state: 'running',
|
|
956
|
+
});
|
|
957
|
+
await providerRunnerFactory.create({
|
|
958
|
+
workspaceId,
|
|
959
|
+
provisionerId,
|
|
960
|
+
providerRunnerId: 'provisioned-runner-2',
|
|
961
|
+
intendedReservationId: reservationId,
|
|
962
|
+
state: 'running',
|
|
963
|
+
});
|
|
689
964
|
|
|
690
965
|
const result = await reportRunnerInstances({
|
|
691
966
|
scope: 'workspace',
|
|
@@ -704,6 +979,13 @@ describe('reportRunnerInstances', () => {
|
|
|
704
979
|
|
|
705
980
|
it('deletes a one-unit reservation instead of violating the positive count check', async () => {
|
|
706
981
|
const reservationId = await createReservation(1);
|
|
982
|
+
await providerRunnerFactory.create({
|
|
983
|
+
workspaceId,
|
|
984
|
+
provisionerId,
|
|
985
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
986
|
+
intendedReservationId: reservationId,
|
|
987
|
+
state: 'running',
|
|
988
|
+
});
|
|
707
989
|
|
|
708
990
|
const result = await reportRunnerInstances({
|
|
709
991
|
scope: 'workspace',
|
|
@@ -727,6 +1009,13 @@ describe('reportRunnerInstances', () => {
|
|
|
727
1009
|
});
|
|
728
1010
|
const [reservation] = await reservationRowsFor({workspaceId, provisionerId});
|
|
729
1011
|
if (!reservation) throw new Error('Expected reservation');
|
|
1012
|
+
await providerRunnerFactory.create({
|
|
1013
|
+
workspaceId,
|
|
1014
|
+
provisionerId,
|
|
1015
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
1016
|
+
intendedReservationId: reservation.id,
|
|
1017
|
+
state: 'running',
|
|
1018
|
+
});
|
|
730
1019
|
|
|
731
1020
|
const result = await reportRunnerInstances({
|
|
732
1021
|
scope: 'workspace',
|
|
@@ -877,13 +1166,14 @@ describe('reportRunnerInstances', () => {
|
|
|
877
1166
|
|
|
878
1167
|
async function createReservation(
|
|
879
1168
|
count: number,
|
|
880
|
-
overrides?: {workspaceId?: string; provisionerId?: string},
|
|
1169
|
+
overrides?: {kind?: 'bound' | 'launch'; workspaceId?: string; provisionerId?: string},
|
|
881
1170
|
): Promise<string> {
|
|
882
1171
|
await reservationFactory.create({
|
|
883
1172
|
workspaceId: overrides?.workspaceId ?? workspaceId,
|
|
884
1173
|
provisionerId: overrides?.provisionerId ?? provisionerId,
|
|
885
1174
|
requiredLabels: ['linux'],
|
|
886
1175
|
count,
|
|
1176
|
+
kind: overrides?.kind ?? 'launch',
|
|
887
1177
|
expiresAt: new Date(Date.now() + 60_000),
|
|
888
1178
|
});
|
|
889
1179
|
const [reservation] = await db()
|
|
@@ -1022,6 +1312,106 @@ describe('listProvisionerTerminateIntents', () => {
|
|
|
1022
1312
|
expect(result).toEqual([{providerRunnerId: 'provisioned-runner-1', reason: 'job-cancelled'}]);
|
|
1023
1313
|
});
|
|
1024
1314
|
|
|
1315
|
+
it('returns an activation-timeout intent for a stale demand-backed runner', async () => {
|
|
1316
|
+
await createRunnerInstance({
|
|
1317
|
+
providerRunnerId: 'stale-demand-runner',
|
|
1318
|
+
launchKind: 'demand',
|
|
1319
|
+
createdAt: new Date(Date.now() - 301_000),
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
const result = await db().transaction((tx) =>
|
|
1323
|
+
listProvisionerTerminateIntentRowsTx(tx, {workspaceId, provisionerId, limit: 1000}),
|
|
1324
|
+
);
|
|
1325
|
+
const [runner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1326
|
+
|
|
1327
|
+
expect(result).toEqual([
|
|
1328
|
+
{providerRunnerId: 'stale-demand-runner', reason: 'activation-timeout'},
|
|
1329
|
+
]);
|
|
1330
|
+
expect(runner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1331
|
+
});
|
|
1332
|
+
|
|
1333
|
+
it('does not reclaim warm, manual, active, or already-activated runners', async () => {
|
|
1334
|
+
const [liveReservation] = await db()
|
|
1335
|
+
.insert(reservations)
|
|
1336
|
+
.values({
|
|
1337
|
+
workspaceId,
|
|
1338
|
+
provisionerId,
|
|
1339
|
+
requiredLabels: ['linux'],
|
|
1340
|
+
count: 1,
|
|
1341
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
1342
|
+
})
|
|
1343
|
+
.returning({id: reservations.id});
|
|
1344
|
+
if (!liveReservation) throw new Error('Expected live reservation');
|
|
1345
|
+
|
|
1346
|
+
const staleCreatedAt = new Date(Date.now() - 301_000);
|
|
1347
|
+
await createRunnerInstance({
|
|
1348
|
+
providerRunnerId: 'stale-warm-runner',
|
|
1349
|
+
launchKind: 'warm',
|
|
1350
|
+
createdAt: staleCreatedAt,
|
|
1351
|
+
});
|
|
1352
|
+
await createRunnerInstance({
|
|
1353
|
+
providerRunnerId: 'stale-manual-runner',
|
|
1354
|
+
launchKind: 'manual',
|
|
1355
|
+
createdAt: staleCreatedAt,
|
|
1356
|
+
});
|
|
1357
|
+
await createRunnerInstance({
|
|
1358
|
+
providerRunnerId: 'live-demand-runner',
|
|
1359
|
+
launchKind: 'demand',
|
|
1360
|
+
reservationId: liveReservation.id,
|
|
1361
|
+
createdAt: staleCreatedAt,
|
|
1362
|
+
});
|
|
1363
|
+
await db()
|
|
1364
|
+
.insert(providerRunners)
|
|
1365
|
+
.values({
|
|
1366
|
+
workspaceId,
|
|
1367
|
+
provisionerId,
|
|
1368
|
+
intendedReservationId: liveReservation.id,
|
|
1369
|
+
providerRunnerId: 'live-intended-demand-runner',
|
|
1370
|
+
launchKind: 'demand',
|
|
1371
|
+
state: 'running',
|
|
1372
|
+
labels: ['linux'],
|
|
1373
|
+
reportedAt: staleCreatedAt,
|
|
1374
|
+
createdAt: staleCreatedAt,
|
|
1375
|
+
updatedAt: staleCreatedAt,
|
|
1376
|
+
});
|
|
1377
|
+
await createRunnerInstance({
|
|
1378
|
+
providerRunnerId: 'activated-demand-runner',
|
|
1379
|
+
launchKind: 'demand',
|
|
1380
|
+
runnerSessionId: crypto.randomUUID(),
|
|
1381
|
+
createdAt: staleCreatedAt,
|
|
1382
|
+
});
|
|
1383
|
+
|
|
1384
|
+
const result = await listProvisionerTerminateIntents({workspaceId, provisionerId, limit: 1000});
|
|
1385
|
+
|
|
1386
|
+
expect(result).toEqual([]);
|
|
1387
|
+
});
|
|
1388
|
+
|
|
1389
|
+
it('marks activation-timeout retries after releasing the runner reservation', async () => {
|
|
1390
|
+
await createRunnerInstance({
|
|
1391
|
+
providerRunnerId: 'retryable-demand-runner',
|
|
1392
|
+
launchKind: 'demand',
|
|
1393
|
+
createdAt: new Date(Date.now() - 301_000),
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
const first = await db().transaction((tx) =>
|
|
1397
|
+
listProvisionerTerminateIntentRowsTx(tx, {workspaceId, provisionerId, limit: 1000}),
|
|
1398
|
+
);
|
|
1399
|
+
const second = await db().transaction((tx) =>
|
|
1400
|
+
listProvisionerTerminateIntentRowsTx(tx, {workspaceId, provisionerId, limit: 1000}),
|
|
1401
|
+
);
|
|
1402
|
+
|
|
1403
|
+
expect(first).toEqual([
|
|
1404
|
+
{providerRunnerId: 'retryable-demand-runner', reason: 'activation-timeout'},
|
|
1405
|
+
]);
|
|
1406
|
+
expect(second).toEqual([
|
|
1407
|
+
{
|
|
1408
|
+
providerRunnerId: 'retryable-demand-runner',
|
|
1409
|
+
reason: 'activation-timeout',
|
|
1410
|
+
activationTimeoutRetry: true,
|
|
1411
|
+
},
|
|
1412
|
+
]);
|
|
1413
|
+
});
|
|
1414
|
+
|
|
1025
1415
|
it('excludes active provisioned runners whose latest bound job is healthy', async () => {
|
|
1026
1416
|
await createRunnerInstance({providerRunnerId: 'provisioned-runner-1'});
|
|
1027
1417
|
await insertRunningJobRow({
|
|
@@ -1137,12 +1527,20 @@ describe('listProvisionerTerminateIntents', () => {
|
|
|
1137
1527
|
providerRunnerId: string;
|
|
1138
1528
|
provisionerId?: string;
|
|
1139
1529
|
state?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'terminated';
|
|
1530
|
+
launchKind?: 'demand' | 'warm' | 'manual';
|
|
1531
|
+
createdAt?: Date;
|
|
1532
|
+
reservationId?: string | null;
|
|
1533
|
+
runnerSessionId?: string | null;
|
|
1140
1534
|
}) {
|
|
1141
1535
|
return await providerRunnerFactory.create({
|
|
1142
1536
|
workspaceId,
|
|
1143
1537
|
provisionerId: params.provisionerId ?? provisionerId,
|
|
1144
1538
|
providerRunnerId: params.providerRunnerId,
|
|
1145
1539
|
state: params.state ?? 'running',
|
|
1540
|
+
launchKind: params.launchKind ?? 'manual',
|
|
1541
|
+
createdAt: params.createdAt ?? new Date(),
|
|
1542
|
+
reservationId: params.reservationId ?? null,
|
|
1543
|
+
runnerSessionId: params.runnerSessionId ?? null,
|
|
1146
1544
|
});
|
|
1147
1545
|
}
|
|
1148
1546
|
});
|
|
@@ -2046,6 +2444,102 @@ describe('reconcileRunnerInstances', () => {
|
|
|
2046
2444
|
}
|
|
2047
2445
|
});
|
|
2048
2446
|
|
|
2447
|
+
describe('countStaleEnrolledRunnerInstances', () => {
|
|
2448
|
+
let provisionerId: string;
|
|
2449
|
+
|
|
2450
|
+
beforeEach(() => {
|
|
2451
|
+
provisionerId = crypto.randomUUID();
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2454
|
+
function staleAt(): Date {
|
|
2455
|
+
return new Date(Date.now() - 120_000);
|
|
2456
|
+
}
|
|
2457
|
+
|
|
2458
|
+
it('counts only stale reported runners with a live control session and no assignment', async () => {
|
|
2459
|
+
const baseline = await countStaleEnrolledRunnerInstances({graceSeconds: 60});
|
|
2460
|
+
const stale = await createRunner({updatedAt: staleAt()});
|
|
2461
|
+
await createControlSession(stale);
|
|
2462
|
+
|
|
2463
|
+
const fresh = await createRunner({updatedAt: new Date()});
|
|
2464
|
+
await createControlSession(fresh);
|
|
2465
|
+
|
|
2466
|
+
const recentlyMutated = await createRunner({reportedAt: staleAt(), updatedAt: new Date()});
|
|
2467
|
+
await createControlSession(recentlyMutated);
|
|
2468
|
+
|
|
2469
|
+
const freshReport = await createRunner({reportedAt: new Date(), updatedAt: staleAt()});
|
|
2470
|
+
await createControlSession(freshReport);
|
|
2471
|
+
|
|
2472
|
+
const assigned = await createRunner({workspaceId: crypto.randomUUID(), updatedAt: staleAt()});
|
|
2473
|
+
await createControlSession(assigned);
|
|
2474
|
+
|
|
2475
|
+
const activated = await createRunner({
|
|
2476
|
+
runnerSessionId: crypto.randomUUID(),
|
|
2477
|
+
updatedAt: staleAt(),
|
|
2478
|
+
});
|
|
2479
|
+
await createControlSession(activated);
|
|
2480
|
+
|
|
2481
|
+
await createRunner({updatedAt: staleAt()});
|
|
2482
|
+
const expiredControlSession = await createRunner({updatedAt: staleAt()});
|
|
2483
|
+
await createControlSession(expiredControlSession, {
|
|
2484
|
+
expiresAt: new Date(Date.now() - 1_000),
|
|
2485
|
+
});
|
|
2486
|
+
const closedControlSession = await createRunner({updatedAt: staleAt()});
|
|
2487
|
+
await createControlSession(closedControlSession, {
|
|
2488
|
+
closedAt: new Date(),
|
|
2489
|
+
});
|
|
2490
|
+
const stopped = await createRunner({state: 'stopped', updatedAt: staleAt()});
|
|
2491
|
+
await createControlSession(stopped);
|
|
2492
|
+
|
|
2493
|
+
const count = await countStaleEnrolledRunnerInstances({graceSeconds: 60});
|
|
2494
|
+
|
|
2495
|
+
expect(count - baseline).toBe(2);
|
|
2496
|
+
});
|
|
2497
|
+
|
|
2498
|
+
async function createRunner(params: {
|
|
2499
|
+
state?: 'running' | 'stopped';
|
|
2500
|
+
workspaceId?: string | null;
|
|
2501
|
+
runnerSessionId?: string | null;
|
|
2502
|
+
reportedAt?: Date;
|
|
2503
|
+
updatedAt?: Date;
|
|
2504
|
+
}) {
|
|
2505
|
+
const reportedAt = params.reportedAt ?? params.updatedAt ?? new Date();
|
|
2506
|
+
const [runner] = await db()
|
|
2507
|
+
.insert(providerRunners)
|
|
2508
|
+
.values({
|
|
2509
|
+
workspaceId: params.workspaceId ?? null,
|
|
2510
|
+
provisionerId,
|
|
2511
|
+
providerRunnerId: crypto.randomUUID(),
|
|
2512
|
+
templateKey: 'linux',
|
|
2513
|
+
labels: ['linux'],
|
|
2514
|
+
state: params.state ?? 'running',
|
|
2515
|
+
runnerSessionId: params.runnerSessionId ?? null,
|
|
2516
|
+
providerKind: 'docker',
|
|
2517
|
+
reportedAt,
|
|
2518
|
+
updatedAt: params.updatedAt ?? reportedAt,
|
|
2519
|
+
})
|
|
2520
|
+
.returning({id: providerRunners.id});
|
|
2521
|
+
if (!runner) throw new Error('Expected runner instance');
|
|
2522
|
+
return runner.id;
|
|
2523
|
+
}
|
|
2524
|
+
|
|
2525
|
+
async function createControlSession(
|
|
2526
|
+
runnerInstanceId: string,
|
|
2527
|
+
params: {expiresAt?: Date; closedAt?: Date} = {},
|
|
2528
|
+
) {
|
|
2529
|
+
await db()
|
|
2530
|
+
.insert(runnerControlSessions)
|
|
2531
|
+
.values({
|
|
2532
|
+
runnerInstanceId,
|
|
2533
|
+
provisionerId,
|
|
2534
|
+
hashedToken: crypto.randomUUID(),
|
|
2535
|
+
prefix: 'test',
|
|
2536
|
+
expiresAt: params.expiresAt ?? new Date(Date.now() + 60_000),
|
|
2537
|
+
closedAt: params.closedAt,
|
|
2538
|
+
closeReason: params.closedAt ? 'test' : null,
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
});
|
|
2542
|
+
|
|
2049
2543
|
describe('runner instance provider attachment', () => {
|
|
2050
2544
|
it('updates the pre-created runner instance before its first lifecycle report', async () => {
|
|
2051
2545
|
const provisionerId = crypto.randomUUID();
|