@shipfox/api-runners 12.3.0 → 12.4.1
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 +36 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -2
- 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/errors.d.ts +5 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +9 -2
- package/dist/core/errors.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 +116 -20
- 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/job-executions.d.ts.map +1 -1
- package/dist/db/job-executions.js +35 -4
- package/dist/db/job-executions.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 +246 -51
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +25 -1
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +160 -18
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +11 -2
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +167 -24
- 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 +14 -1
- 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 +72 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +161 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +25 -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 +10 -4
- 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 +6 -6
- package/src/config.test.ts +22 -0
- package/src/config.ts +15 -2
- 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/errors.ts +30 -1
- package/src/core/runner-activation.ts +48 -2
- package/src/core/runner-control-sessions.test.ts +135 -3
- package/src/core/runner-control-sessions.ts +141 -19
- 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/job-executions.test.ts +98 -12
- package/src/db/job-executions.ts +52 -4
- package/src/db/reservation-locks.ts +22 -0
- package/src/db/reservations.test.ts +733 -22
- package/src/db/reservations.ts +408 -144
- package/src/db/runner-assignments.test.ts +105 -1
- package/src/db/runner-assignments.ts +245 -25
- package/src/db/runner-instances.test.ts +357 -2
- package/src/db/runner-instances.ts +289 -55
- package/src/db/runner-sessions.ts +19 -1
- 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 +24 -1
- package/src/metrics/instance.test.ts +118 -0
- package/src/metrics/instance.ts +247 -3
- package/src/metrics/service.test.ts +46 -1
- package/src/metrics/service.ts +55 -8
- 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 +14 -2
- package/src/presentation/routes/late-runner-enrollment.test.ts +219 -2
- package/src/presentation/routes/poll-demand.test.ts +56 -0
- 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/tsconfig.build.tsbuildinfo +1 -1
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
countStaleEnrolledRunnerInstances,
|
|
8
8
|
listActiveRunnerInstanceCountsByTemplateTx,
|
|
9
9
|
listActiveRunnerInstances,
|
|
10
|
+
listProviderRunnerByPhaseMetrics,
|
|
10
11
|
listProvisionerTerminateIntentRowsTx,
|
|
11
12
|
listProvisionerTerminateIntents,
|
|
12
13
|
reapStaleRunnerInstances,
|
|
@@ -146,6 +147,28 @@ describe('reportRunnerInstances', () => {
|
|
|
146
147
|
expect(rows[0]?.state).toBe('running');
|
|
147
148
|
});
|
|
148
149
|
|
|
150
|
+
it('preserves launch kind when a provider report updates the runner row', async () => {
|
|
151
|
+
await providerRunnerFactory.create({
|
|
152
|
+
workspaceId,
|
|
153
|
+
provisionerId,
|
|
154
|
+
providerRunnerId: 'demand-runner',
|
|
155
|
+
launchKind: 'demand',
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await reportRunnerInstances({
|
|
159
|
+
scope: 'workspace',
|
|
160
|
+
workspaceId,
|
|
161
|
+
provisionerId,
|
|
162
|
+
events: [event({providerRunnerId: 'demand-runner', state: 'running'})],
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const [row] = await db()
|
|
166
|
+
.select({launchKind: providerRunners.launchKind})
|
|
167
|
+
.from(providerRunners)
|
|
168
|
+
.where(eq(providerRunners.providerRunnerId, 'demand-runner'));
|
|
169
|
+
expect(row?.launchKind).toBe('demand');
|
|
170
|
+
});
|
|
171
|
+
|
|
149
172
|
it('uses state progression to dedupe equal-timestamp provisioned runner reports', async () => {
|
|
150
173
|
const reportedAt = new Date();
|
|
151
174
|
|
|
@@ -366,6 +389,58 @@ describe('reportRunnerInstances', () => {
|
|
|
366
389
|
expect(rows[0]?.reservationId).toBe(reservationId);
|
|
367
390
|
});
|
|
368
391
|
|
|
392
|
+
it('does not adopt a bound reservation id from an unclaimed report', async () => {
|
|
393
|
+
const reservationId = await createReservation(1, {kind: 'bound'});
|
|
394
|
+
await db()
|
|
395
|
+
.insert(providerRunners)
|
|
396
|
+
.values({
|
|
397
|
+
workspaceId,
|
|
398
|
+
provisionerId,
|
|
399
|
+
providerRunnerId: 'bound-reservation-runner',
|
|
400
|
+
state: 'starting',
|
|
401
|
+
reportedAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
await reportRunnerInstances({
|
|
405
|
+
scope: 'workspace',
|
|
406
|
+
workspaceId,
|
|
407
|
+
provisionerId,
|
|
408
|
+
events: [
|
|
409
|
+
event({
|
|
410
|
+
providerRunnerId: 'bound-reservation-runner',
|
|
411
|
+
reservationId,
|
|
412
|
+
state: 'running',
|
|
413
|
+
reportedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
414
|
+
}),
|
|
415
|
+
],
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
419
|
+
expect(rows[0]?.reservationId).toBeNull();
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
it('does not let reports exceed reservation capacity when creating projection rows', async () => {
|
|
423
|
+
const reservationId = await createReservation(1);
|
|
424
|
+
|
|
425
|
+
await reportRunnerInstances({
|
|
426
|
+
scope: 'workspace',
|
|
427
|
+
workspaceId,
|
|
428
|
+
provisionerId,
|
|
429
|
+
events: [
|
|
430
|
+
event({providerRunnerId: 'provisioned-runner-1', reservationId}),
|
|
431
|
+
event({providerRunnerId: 'provisioned-runner-2', reservationId}),
|
|
432
|
+
],
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
436
|
+
expect(rows.find((row) => row.providerRunnerId === 'provisioned-runner-1')?.reservationId).toBe(
|
|
437
|
+
reservationId,
|
|
438
|
+
);
|
|
439
|
+
expect(
|
|
440
|
+
rows.find((row) => row.providerRunnerId === 'provisioned-runner-2')?.reservationId,
|
|
441
|
+
).toBeNull();
|
|
442
|
+
});
|
|
443
|
+
|
|
369
444
|
it('does not let equal-timestamp lower-priority reports flip terminal state', async () => {
|
|
370
445
|
const reservationId = await createReservation(1);
|
|
371
446
|
const reportedAt = new Date('2025-01-01T00:00:00.000Z');
|
|
@@ -554,8 +629,15 @@ describe('reportRunnerInstances', () => {
|
|
|
554
629
|
expect(active).toEqual([]);
|
|
555
630
|
});
|
|
556
631
|
|
|
557
|
-
it('releases one reservation unit for a terminal
|
|
632
|
+
it('releases one reservation unit for a terminal runner with an intended reservation', async () => {
|
|
558
633
|
const reservationId = await createReservation(2);
|
|
634
|
+
await providerRunnerFactory.create({
|
|
635
|
+
workspaceId,
|
|
636
|
+
provisionerId,
|
|
637
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
638
|
+
intendedReservationId: reservationId,
|
|
639
|
+
state: 'running',
|
|
640
|
+
});
|
|
559
641
|
|
|
560
642
|
const result = await reportRunnerInstances({
|
|
561
643
|
scope: 'workspace',
|
|
@@ -571,6 +653,30 @@ describe('reportRunnerInstances', () => {
|
|
|
571
653
|
expect(providerRunnerRows[0]?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
572
654
|
});
|
|
573
655
|
|
|
656
|
+
it('does not let an unrecognized terminal report consume reservation capacity', async () => {
|
|
657
|
+
const reservationId = await createReservation(1);
|
|
658
|
+
|
|
659
|
+
const result = await reportRunnerInstances({
|
|
660
|
+
scope: 'workspace',
|
|
661
|
+
workspaceId,
|
|
662
|
+
provisionerId,
|
|
663
|
+
events: [event({providerRunnerId: 'unrecognized-runner', reservationId, state: 'failed'})],
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
667
|
+
const providerRunnerRows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
668
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 0, terminateIntentsHonored: []});
|
|
669
|
+
expect(reservationRows[0]?.count).toBe(1);
|
|
670
|
+
expect(providerRunnerRows).toHaveLength(1);
|
|
671
|
+
expect(providerRunnerRows[0]).toMatchObject({
|
|
672
|
+
providerRunnerId: 'unrecognized-runner',
|
|
673
|
+
reservationId: null,
|
|
674
|
+
intendedReservationId: null,
|
|
675
|
+
reservationReleasedAt: null,
|
|
676
|
+
state: 'failed',
|
|
677
|
+
});
|
|
678
|
+
});
|
|
679
|
+
|
|
574
680
|
it('releases an intended reservation for a runner that dies before enrollment', async () => {
|
|
575
681
|
const reservationId = await createReservation(2);
|
|
576
682
|
const [runner] = await db()
|
|
@@ -741,6 +847,13 @@ describe('reportRunnerInstances', () => {
|
|
|
741
847
|
|
|
742
848
|
it('releases a reservation only once across repeated terminal reports', async () => {
|
|
743
849
|
const reservationId = await createReservation(2);
|
|
850
|
+
await providerRunnerFactory.create({
|
|
851
|
+
workspaceId,
|
|
852
|
+
provisionerId,
|
|
853
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
854
|
+
intendedReservationId: reservationId,
|
|
855
|
+
state: 'running',
|
|
856
|
+
});
|
|
744
857
|
|
|
745
858
|
await reportRunnerInstances({
|
|
746
859
|
scope: 'workspace',
|
|
@@ -762,6 +875,13 @@ describe('reportRunnerInstances', () => {
|
|
|
762
875
|
|
|
763
876
|
it('does not let a newer running report revive a terminal provisioned runner', async () => {
|
|
764
877
|
const reservationId = await createReservation(2);
|
|
878
|
+
await providerRunnerFactory.create({
|
|
879
|
+
workspaceId,
|
|
880
|
+
provisionerId,
|
|
881
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
882
|
+
intendedReservationId: reservationId,
|
|
883
|
+
state: 'running',
|
|
884
|
+
});
|
|
765
885
|
const failedAt = new Date();
|
|
766
886
|
await reportRunnerInstances({
|
|
767
887
|
scope: 'workspace',
|
|
@@ -800,6 +920,13 @@ describe('reportRunnerInstances', () => {
|
|
|
800
920
|
|
|
801
921
|
it('tracks provider cleanup as terminated', async () => {
|
|
802
922
|
const reservationId = await createReservation(2);
|
|
923
|
+
await providerRunnerFactory.create({
|
|
924
|
+
workspaceId,
|
|
925
|
+
provisionerId,
|
|
926
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
927
|
+
intendedReservationId: reservationId,
|
|
928
|
+
state: 'running',
|
|
929
|
+
});
|
|
803
930
|
|
|
804
931
|
const result = await reportRunnerInstances({
|
|
805
932
|
scope: 'workspace',
|
|
@@ -821,6 +948,20 @@ describe('reportRunnerInstances', () => {
|
|
|
821
948
|
|
|
822
949
|
it('releases multiple units from the same reservation in one batch', async () => {
|
|
823
950
|
const reservationId = await createReservation(3);
|
|
951
|
+
await providerRunnerFactory.create({
|
|
952
|
+
workspaceId,
|
|
953
|
+
provisionerId,
|
|
954
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
955
|
+
intendedReservationId: reservationId,
|
|
956
|
+
state: 'running',
|
|
957
|
+
});
|
|
958
|
+
await providerRunnerFactory.create({
|
|
959
|
+
workspaceId,
|
|
960
|
+
provisionerId,
|
|
961
|
+
providerRunnerId: 'provisioned-runner-2',
|
|
962
|
+
intendedReservationId: reservationId,
|
|
963
|
+
state: 'running',
|
|
964
|
+
});
|
|
824
965
|
|
|
825
966
|
const result = await reportRunnerInstances({
|
|
826
967
|
scope: 'workspace',
|
|
@@ -839,6 +980,13 @@ describe('reportRunnerInstances', () => {
|
|
|
839
980
|
|
|
840
981
|
it('deletes a one-unit reservation instead of violating the positive count check', async () => {
|
|
841
982
|
const reservationId = await createReservation(1);
|
|
983
|
+
await providerRunnerFactory.create({
|
|
984
|
+
workspaceId,
|
|
985
|
+
provisionerId,
|
|
986
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
987
|
+
intendedReservationId: reservationId,
|
|
988
|
+
state: 'running',
|
|
989
|
+
});
|
|
842
990
|
|
|
843
991
|
const result = await reportRunnerInstances({
|
|
844
992
|
scope: 'workspace',
|
|
@@ -862,6 +1010,13 @@ describe('reportRunnerInstances', () => {
|
|
|
862
1010
|
});
|
|
863
1011
|
const [reservation] = await reservationRowsFor({workspaceId, provisionerId});
|
|
864
1012
|
if (!reservation) throw new Error('Expected reservation');
|
|
1013
|
+
await providerRunnerFactory.create({
|
|
1014
|
+
workspaceId,
|
|
1015
|
+
provisionerId,
|
|
1016
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
1017
|
+
intendedReservationId: reservation.id,
|
|
1018
|
+
state: 'running',
|
|
1019
|
+
});
|
|
865
1020
|
|
|
866
1021
|
const result = await reportRunnerInstances({
|
|
867
1022
|
scope: 'workspace',
|
|
@@ -1012,13 +1167,14 @@ describe('reportRunnerInstances', () => {
|
|
|
1012
1167
|
|
|
1013
1168
|
async function createReservation(
|
|
1014
1169
|
count: number,
|
|
1015
|
-
overrides?: {workspaceId?: string; provisionerId?: string},
|
|
1170
|
+
overrides?: {kind?: 'bound' | 'launch'; workspaceId?: string; provisionerId?: string},
|
|
1016
1171
|
): Promise<string> {
|
|
1017
1172
|
await reservationFactory.create({
|
|
1018
1173
|
workspaceId: overrides?.workspaceId ?? workspaceId,
|
|
1019
1174
|
provisionerId: overrides?.provisionerId ?? provisionerId,
|
|
1020
1175
|
requiredLabels: ['linux'],
|
|
1021
1176
|
count,
|
|
1177
|
+
kind: overrides?.kind ?? 'launch',
|
|
1022
1178
|
expiresAt: new Date(Date.now() + 60_000),
|
|
1023
1179
|
});
|
|
1024
1180
|
const [reservation] = await db()
|
|
@@ -1157,6 +1313,106 @@ describe('listProvisionerTerminateIntents', () => {
|
|
|
1157
1313
|
expect(result).toEqual([{providerRunnerId: 'provisioned-runner-1', reason: 'job-cancelled'}]);
|
|
1158
1314
|
});
|
|
1159
1315
|
|
|
1316
|
+
it('returns an activation-timeout intent for a stale demand-backed runner', async () => {
|
|
1317
|
+
await createRunnerInstance({
|
|
1318
|
+
providerRunnerId: 'stale-demand-runner',
|
|
1319
|
+
launchKind: 'demand',
|
|
1320
|
+
createdAt: new Date(Date.now() - 301_000),
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
const result = await db().transaction((tx) =>
|
|
1324
|
+
listProvisionerTerminateIntentRowsTx(tx, {workspaceId, provisionerId, limit: 1000}),
|
|
1325
|
+
);
|
|
1326
|
+
const [runner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1327
|
+
|
|
1328
|
+
expect(result).toEqual([
|
|
1329
|
+
{providerRunnerId: 'stale-demand-runner', reason: 'activation-timeout'},
|
|
1330
|
+
]);
|
|
1331
|
+
expect(runner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
it('does not reclaim warm, manual, active, or already-activated runners', async () => {
|
|
1335
|
+
const [liveReservation] = await db()
|
|
1336
|
+
.insert(reservations)
|
|
1337
|
+
.values({
|
|
1338
|
+
workspaceId,
|
|
1339
|
+
provisionerId,
|
|
1340
|
+
requiredLabels: ['linux'],
|
|
1341
|
+
count: 1,
|
|
1342
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
1343
|
+
})
|
|
1344
|
+
.returning({id: reservations.id});
|
|
1345
|
+
if (!liveReservation) throw new Error('Expected live reservation');
|
|
1346
|
+
|
|
1347
|
+
const staleCreatedAt = new Date(Date.now() - 301_000);
|
|
1348
|
+
await createRunnerInstance({
|
|
1349
|
+
providerRunnerId: 'stale-warm-runner',
|
|
1350
|
+
launchKind: 'warm',
|
|
1351
|
+
createdAt: staleCreatedAt,
|
|
1352
|
+
});
|
|
1353
|
+
await createRunnerInstance({
|
|
1354
|
+
providerRunnerId: 'stale-manual-runner',
|
|
1355
|
+
launchKind: 'manual',
|
|
1356
|
+
createdAt: staleCreatedAt,
|
|
1357
|
+
});
|
|
1358
|
+
await createRunnerInstance({
|
|
1359
|
+
providerRunnerId: 'live-demand-runner',
|
|
1360
|
+
launchKind: 'demand',
|
|
1361
|
+
reservationId: liveReservation.id,
|
|
1362
|
+
createdAt: staleCreatedAt,
|
|
1363
|
+
});
|
|
1364
|
+
await db()
|
|
1365
|
+
.insert(providerRunners)
|
|
1366
|
+
.values({
|
|
1367
|
+
workspaceId,
|
|
1368
|
+
provisionerId,
|
|
1369
|
+
intendedReservationId: liveReservation.id,
|
|
1370
|
+
providerRunnerId: 'live-intended-demand-runner',
|
|
1371
|
+
launchKind: 'demand',
|
|
1372
|
+
state: 'running',
|
|
1373
|
+
labels: ['linux'],
|
|
1374
|
+
reportedAt: staleCreatedAt,
|
|
1375
|
+
createdAt: staleCreatedAt,
|
|
1376
|
+
updatedAt: staleCreatedAt,
|
|
1377
|
+
});
|
|
1378
|
+
await createRunnerInstance({
|
|
1379
|
+
providerRunnerId: 'activated-demand-runner',
|
|
1380
|
+
launchKind: 'demand',
|
|
1381
|
+
runnerSessionId: crypto.randomUUID(),
|
|
1382
|
+
createdAt: staleCreatedAt,
|
|
1383
|
+
});
|
|
1384
|
+
|
|
1385
|
+
const result = await listProvisionerTerminateIntents({workspaceId, provisionerId, limit: 1000});
|
|
1386
|
+
|
|
1387
|
+
expect(result).toEqual([]);
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
it('marks activation-timeout retries after releasing the runner reservation', async () => {
|
|
1391
|
+
await createRunnerInstance({
|
|
1392
|
+
providerRunnerId: 'retryable-demand-runner',
|
|
1393
|
+
launchKind: 'demand',
|
|
1394
|
+
createdAt: new Date(Date.now() - 301_000),
|
|
1395
|
+
});
|
|
1396
|
+
|
|
1397
|
+
const first = await db().transaction((tx) =>
|
|
1398
|
+
listProvisionerTerminateIntentRowsTx(tx, {workspaceId, provisionerId, limit: 1000}),
|
|
1399
|
+
);
|
|
1400
|
+
const second = await db().transaction((tx) =>
|
|
1401
|
+
listProvisionerTerminateIntentRowsTx(tx, {workspaceId, provisionerId, limit: 1000}),
|
|
1402
|
+
);
|
|
1403
|
+
|
|
1404
|
+
expect(first).toEqual([
|
|
1405
|
+
{providerRunnerId: 'retryable-demand-runner', reason: 'activation-timeout'},
|
|
1406
|
+
]);
|
|
1407
|
+
expect(second).toEqual([
|
|
1408
|
+
{
|
|
1409
|
+
providerRunnerId: 'retryable-demand-runner',
|
|
1410
|
+
reason: 'activation-timeout',
|
|
1411
|
+
activationTimeoutRetry: true,
|
|
1412
|
+
},
|
|
1413
|
+
]);
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1160
1416
|
it('excludes active provisioned runners whose latest bound job is healthy', async () => {
|
|
1161
1417
|
await createRunnerInstance({providerRunnerId: 'provisioned-runner-1'});
|
|
1162
1418
|
await insertRunningJobRow({
|
|
@@ -1272,12 +1528,20 @@ describe('listProvisionerTerminateIntents', () => {
|
|
|
1272
1528
|
providerRunnerId: string;
|
|
1273
1529
|
provisionerId?: string;
|
|
1274
1530
|
state?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'terminated';
|
|
1531
|
+
launchKind?: 'demand' | 'warm' | 'manual';
|
|
1532
|
+
createdAt?: Date;
|
|
1533
|
+
reservationId?: string | null;
|
|
1534
|
+
runnerSessionId?: string | null;
|
|
1275
1535
|
}) {
|
|
1276
1536
|
return await providerRunnerFactory.create({
|
|
1277
1537
|
workspaceId,
|
|
1278
1538
|
provisionerId: params.provisionerId ?? provisionerId,
|
|
1279
1539
|
providerRunnerId: params.providerRunnerId,
|
|
1280
1540
|
state: params.state ?? 'running',
|
|
1541
|
+
launchKind: params.launchKind ?? 'manual',
|
|
1542
|
+
createdAt: params.createdAt ?? new Date(),
|
|
1543
|
+
reservationId: params.reservationId ?? null,
|
|
1544
|
+
runnerSessionId: params.runnerSessionId ?? null,
|
|
1281
1545
|
});
|
|
1282
1546
|
}
|
|
1283
1547
|
});
|
|
@@ -2277,6 +2541,97 @@ describe('countStaleEnrolledRunnerInstances', () => {
|
|
|
2277
2541
|
}
|
|
2278
2542
|
});
|
|
2279
2543
|
|
|
2544
|
+
describe('listProviderRunnerByPhaseMetrics', () => {
|
|
2545
|
+
it('groups runners by the lifecycle phase that is currently blocking activation', async () => {
|
|
2546
|
+
const provisionerId = crypto.randomUUID();
|
|
2547
|
+
const provider = `metrics-test-${crypto.randomUUID()}`;
|
|
2548
|
+
const reservationId = crypto.randomUUID();
|
|
2549
|
+
const workspaceId = crypto.randomUUID();
|
|
2550
|
+
const old = new Date(Date.now() - 120_000);
|
|
2551
|
+
|
|
2552
|
+
const createRunner = async (params: {
|
|
2553
|
+
state: 'starting' | 'running';
|
|
2554
|
+
launchKind: 'demand' | 'warm' | 'manual';
|
|
2555
|
+
intendedReservationId?: string | null;
|
|
2556
|
+
workspaceId?: string | null;
|
|
2557
|
+
reservationId?: string | null;
|
|
2558
|
+
assignedAt?: Date | null;
|
|
2559
|
+
createdAt?: Date;
|
|
2560
|
+
}) => {
|
|
2561
|
+
const [runner] = await db()
|
|
2562
|
+
.insert(providerRunners)
|
|
2563
|
+
.values({
|
|
2564
|
+
provisionerId,
|
|
2565
|
+
providerRunnerId: crypto.randomUUID(),
|
|
2566
|
+
providerKind: provider,
|
|
2567
|
+
launchKind: params.launchKind,
|
|
2568
|
+
intendedReservationId: params.intendedReservationId ?? null,
|
|
2569
|
+
workspaceId: params.workspaceId ?? null,
|
|
2570
|
+
reservationId: params.reservationId ?? null,
|
|
2571
|
+
assignedAt: params.assignedAt ?? null,
|
|
2572
|
+
state: params.state,
|
|
2573
|
+
labels: ['linux'],
|
|
2574
|
+
reportedAt: new Date(),
|
|
2575
|
+
createdAt: params.createdAt ?? old,
|
|
2576
|
+
})
|
|
2577
|
+
.returning({id: providerRunners.id});
|
|
2578
|
+
if (!runner) throw new Error('Expected runner instance');
|
|
2579
|
+
return runner.id;
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
await createRunner({state: 'starting', launchKind: 'demand'});
|
|
2583
|
+
const enrollmentRunnerId = await createRunner({state: 'starting', launchKind: 'warm'});
|
|
2584
|
+
const assignmentRunnerId = await createRunner({
|
|
2585
|
+
state: 'running',
|
|
2586
|
+
launchKind: 'demand',
|
|
2587
|
+
intendedReservationId: reservationId,
|
|
2588
|
+
});
|
|
2589
|
+
const activationRunnerId = await createRunner({
|
|
2590
|
+
state: 'running',
|
|
2591
|
+
launchKind: 'manual',
|
|
2592
|
+
workspaceId,
|
|
2593
|
+
reservationId,
|
|
2594
|
+
assignedAt: old,
|
|
2595
|
+
});
|
|
2596
|
+
const idleRunnerId = await createRunner({state: 'running', launchKind: 'warm'});
|
|
2597
|
+
const demandIdleRunnerId = await createRunner({state: 'running', launchKind: 'demand'});
|
|
2598
|
+
|
|
2599
|
+
await db()
|
|
2600
|
+
.insert(runnerControlSessions)
|
|
2601
|
+
.values(
|
|
2602
|
+
[
|
|
2603
|
+
enrollmentRunnerId,
|
|
2604
|
+
assignmentRunnerId,
|
|
2605
|
+
activationRunnerId,
|
|
2606
|
+
idleRunnerId,
|
|
2607
|
+
demandIdleRunnerId,
|
|
2608
|
+
].map((runnerInstanceId) => ({
|
|
2609
|
+
runnerInstanceId,
|
|
2610
|
+
provisionerId,
|
|
2611
|
+
hashedToken: crypto.randomUUID(),
|
|
2612
|
+
prefix: 'metrics-test',
|
|
2613
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
2614
|
+
createdAt: old,
|
|
2615
|
+
})),
|
|
2616
|
+
);
|
|
2617
|
+
|
|
2618
|
+
const metrics = await listProviderRunnerByPhaseMetrics();
|
|
2619
|
+
const ownMetrics = metrics.filter((metric) => metric.provider === provider);
|
|
2620
|
+
|
|
2621
|
+
expect(ownMetrics).toEqual(
|
|
2622
|
+
expect.arrayContaining([
|
|
2623
|
+
expect.objectContaining({phase: 'control_session', launchKind: 'demand', count: 1}),
|
|
2624
|
+
expect.objectContaining({phase: 'enrollment', launchKind: 'warm', count: 1}),
|
|
2625
|
+
expect.objectContaining({phase: 'assignment', launchKind: 'demand', count: 1}),
|
|
2626
|
+
expect.objectContaining({phase: 'activation', launchKind: 'manual', count: 1}),
|
|
2627
|
+
expect.objectContaining({phase: 'idle', launchKind: 'warm', count: 1}),
|
|
2628
|
+
expect.objectContaining({phase: 'idle', launchKind: 'demand', count: 1}),
|
|
2629
|
+
]),
|
|
2630
|
+
);
|
|
2631
|
+
expect(ownMetrics).toHaveLength(6);
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2280
2635
|
describe('runner instance provider attachment', () => {
|
|
2281
2636
|
it('updates the pre-created runner instance before its first lifecycle report', async () => {
|
|
2282
2637
|
const provisionerId = crypto.randomUUID();
|