@shipfox/api-runners 9.3.0 → 10.1.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 +46 -0
- package/dist/config.js +1 -1
- package/dist/config.js.map +1 -1
- package/dist/core/runner-activation.d.ts +6 -0
- package/dist/core/runner-activation.d.ts.map +1 -1
- package/dist/core/runner-activation.js +20 -19
- package/dist/core/runner-activation.js.map +1 -1
- package/dist/core/runner-control-sessions.d.ts +2 -1
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +65 -19
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/db/db.d.ts +34 -0
- package/dist/db/db.d.ts.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/job-executions.d.ts +7 -0
- package/dist/db/job-executions.d.ts.map +1 -1
- package/dist/db/job-executions.js +25 -0
- package/dist/db/job-executions.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +6 -0
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +57 -42
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +64 -19
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/schema/runner-instances.d.ts +17 -0
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +3 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +7 -2
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/routes/runner-enrollment.d.ts +9 -0
- package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
- package/dist/presentation/routes/runner-enrollment.js +36 -23
- package/dist/presentation/routes/runner-enrollment.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0001_intended_reservation.sql +3 -0
- package/drizzle/meta/0001_snapshot.json +2093 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +7 -7
- package/src/config.test.ts +11 -0
- package/src/config.ts +1 -1
- package/src/core/runner-activation.ts +48 -38
- package/src/core/runner-control-sessions.ts +115 -33
- package/src/db/index.ts +1 -0
- package/src/db/job-executions.ts +28 -0
- package/src/db/runner-assignments.test.ts +81 -0
- package/src/db/runner-assignments.ts +134 -86
- package/src/db/runner-instances.test.ts +182 -0
- package/src/db/runner-instances.ts +123 -28
- package/src/db/schema/runner-instances.ts +9 -0
- package/src/index.ts +1 -0
- package/src/presentation/inter-module.ts +4 -0
- package/src/presentation/routes/poll-demand.test.ts +2 -2
- package/src/presentation/routes/runner-enrollment.test.ts +144 -1
- package/src/presentation/routes/runner-enrollment.ts +40 -20
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {and, eq, inArray, isNull, sql} from 'drizzle-orm';
|
|
1
|
+
import {and, eq, inArray, isNull, notInArray, or, sql} from 'drizzle-orm';
|
|
2
2
|
import {
|
|
3
3
|
ReservationExpiredError,
|
|
4
4
|
ReservationNotFoundError,
|
|
5
5
|
RunnerInstanceAlreadyAssignedError,
|
|
6
6
|
RunnerInstanceNotAssignableError,
|
|
7
7
|
} from '#core/errors.js';
|
|
8
|
+
import type {Tx} from './db.js';
|
|
8
9
|
import {db} from './db.js';
|
|
10
|
+
import {terminalStates} from './runner-instances.js';
|
|
9
11
|
import {reservations} from './schema/reservations.js';
|
|
10
12
|
import {runnerControlSessions} from './schema/runner-control-sessions.js';
|
|
11
13
|
import {providerRunners} from './schema/runner-instances.js';
|
|
@@ -16,100 +18,146 @@ export async function assignRunnerInstances(params: {
|
|
|
16
18
|
reservationId: string;
|
|
17
19
|
runnerInstanceIds: string[];
|
|
18
20
|
}): Promise<string[]> {
|
|
21
|
+
return await db().transaction(async (tx) => assignRunnerInstancesTx(tx, params));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function assignRunnerInstancesTx(
|
|
25
|
+
tx: Tx,
|
|
26
|
+
params: {
|
|
27
|
+
provisionerId: string;
|
|
28
|
+
reservationId: string;
|
|
29
|
+
runnerInstanceIds: string[];
|
|
30
|
+
},
|
|
31
|
+
): Promise<string[]> {
|
|
19
32
|
const runnerInstanceIds = [...params.runnerInstanceIds].sort();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
.
|
|
26
|
-
.
|
|
27
|
-
.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
await tx.execute(
|
|
34
|
+
sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${params.reservationId}`}))`,
|
|
35
|
+
);
|
|
36
|
+
const runnerRows = await tx
|
|
37
|
+
.select({
|
|
38
|
+
id: providerRunners.id,
|
|
39
|
+
reservationId: providerRunners.reservationId,
|
|
40
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
41
|
+
workspaceId: providerRunners.workspaceId,
|
|
42
|
+
providerRunnerId: providerRunners.providerRunnerId,
|
|
43
|
+
labels: providerRunners.labels,
|
|
44
|
+
state: providerRunners.state,
|
|
45
|
+
})
|
|
46
|
+
.from(providerRunners)
|
|
47
|
+
.where(
|
|
48
|
+
and(
|
|
49
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
50
|
+
inArray(providerRunners.id, runnerInstanceIds),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
.for('update');
|
|
38
54
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
.
|
|
55
|
+
// The assignment outlives its short reservation row. A retry after maintenance
|
|
56
|
+
// cleanup is complete when every owned runner already carries the requested assignment.
|
|
57
|
+
if (
|
|
58
|
+
runnerInstanceIds.length > 0 &&
|
|
59
|
+
runnerRows.length === runnerInstanceIds.length &&
|
|
60
|
+
runnerRows.every((runner) => runner.reservationId === params.reservationId)
|
|
61
|
+
) {
|
|
62
|
+
await tx
|
|
63
|
+
.update(providerRunners)
|
|
64
|
+
.set({intendedReservationId: null, updatedAt: sql`now()`})
|
|
49
65
|
.where(
|
|
50
66
|
and(
|
|
51
67
|
eq(providerRunners.provisionerId, params.provisionerId),
|
|
52
68
|
inArray(providerRunners.id, runnerInstanceIds),
|
|
53
69
|
),
|
|
54
|
-
)
|
|
55
|
-
.for('update');
|
|
56
|
-
const activeControlSessions = await tx
|
|
57
|
-
.select({runnerInstanceId: runnerControlSessions.runnerInstanceId})
|
|
58
|
-
.from(runnerControlSessions)
|
|
59
|
-
.where(
|
|
60
|
-
and(
|
|
61
|
-
inArray(
|
|
62
|
-
runnerControlSessions.runnerInstanceId,
|
|
63
|
-
runnerRows.map((runner) => runner.id),
|
|
64
|
-
),
|
|
65
|
-
isNull(runnerControlSessions.closedAt),
|
|
66
|
-
),
|
|
67
70
|
);
|
|
68
|
-
|
|
69
|
-
|
|
71
|
+
return params.runnerInstanceIds;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const [reservation] = await tx
|
|
75
|
+
.select()
|
|
76
|
+
.from(reservations)
|
|
77
|
+
.where(
|
|
78
|
+
and(
|
|
79
|
+
eq(reservations.id, params.reservationId),
|
|
80
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
81
|
+
),
|
|
82
|
+
)
|
|
83
|
+
.limit(1)
|
|
84
|
+
.for('update');
|
|
85
|
+
if (!reservation) throw new ReservationNotFoundError(params.reservationId);
|
|
86
|
+
if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);
|
|
87
|
+
if (runnerRows.length !== runnerInstanceIds.length)
|
|
88
|
+
throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');
|
|
89
|
+
|
|
90
|
+
const activeControlSessions = await tx
|
|
91
|
+
.select({runnerInstanceId: runnerControlSessions.runnerInstanceId})
|
|
92
|
+
.from(runnerControlSessions)
|
|
93
|
+
.where(
|
|
94
|
+
and(
|
|
95
|
+
inArray(
|
|
96
|
+
runnerControlSessions.runnerInstanceId,
|
|
97
|
+
runnerRows.map((runner) => runner.id),
|
|
98
|
+
),
|
|
99
|
+
isNull(runnerControlSessions.closedAt),
|
|
100
|
+
),
|
|
70
101
|
);
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
102
|
+
const runnerInstanceIdsWithControlSession = new Set(
|
|
103
|
+
activeControlSessions.map((session) => session.runnerInstanceId),
|
|
104
|
+
);
|
|
105
|
+
const runners = runnerRows.map((runner) => ({
|
|
106
|
+
...runner,
|
|
107
|
+
controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,
|
|
108
|
+
}));
|
|
77
109
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
!runner.controlSessionId ||
|
|
93
|
-
!reservation.requiredLabels.every((label) => runner.labels.includes(label))
|
|
94
|
-
)
|
|
95
|
-
throw new RunnerInstanceNotAssignableError(runner.id);
|
|
96
|
-
}
|
|
97
|
-
if (newRunners.length > 0) {
|
|
98
|
-
await tx
|
|
99
|
-
.update(providerRunners)
|
|
100
|
-
.set({
|
|
101
|
-
workspaceId: reservation.workspaceId,
|
|
102
|
-
reservationId: reservation.id,
|
|
103
|
-
assignedAt: sql`now()`,
|
|
104
|
-
updatedAt: sql`now()`,
|
|
105
|
-
})
|
|
106
|
-
.where(
|
|
107
|
-
inArray(
|
|
108
|
-
providerRunners.id,
|
|
109
|
-
newRunners.map((runner) => runner.id),
|
|
110
|
+
const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);
|
|
111
|
+
if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))
|
|
112
|
+
throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');
|
|
113
|
+
const newRunners = runners.filter((runner) => runner.reservationId === null);
|
|
114
|
+
const assignedCount = await tx
|
|
115
|
+
.select({count: sql<number>`count(*)::int`})
|
|
116
|
+
.from(providerRunners)
|
|
117
|
+
.where(
|
|
118
|
+
and(
|
|
119
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
120
|
+
or(
|
|
121
|
+
and(
|
|
122
|
+
eq(providerRunners.reservationId, reservation.id),
|
|
123
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
110
124
|
),
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
and(
|
|
126
|
+
eq(providerRunners.intendedReservationId, reservation.id),
|
|
127
|
+
notInArray(providerRunners.id, runnerInstanceIds),
|
|
128
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
129
|
+
notInArray(providerRunners.state, [...terminalStates]),
|
|
130
|
+
),
|
|
131
|
+
),
|
|
132
|
+
),
|
|
133
|
+
);
|
|
134
|
+
if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)
|
|
135
|
+
throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');
|
|
136
|
+
for (const runner of newRunners) {
|
|
137
|
+
if (
|
|
138
|
+
runner.state !== 'running' ||
|
|
139
|
+
!runner.providerRunnerId ||
|
|
140
|
+
!runner.controlSessionId ||
|
|
141
|
+
!reservation.requiredLabels.every((label) => runner.labels.includes(label))
|
|
142
|
+
)
|
|
143
|
+
throw new RunnerInstanceNotAssignableError(runner.id);
|
|
144
|
+
}
|
|
145
|
+
if (newRunners.length > 0) {
|
|
146
|
+
await tx
|
|
147
|
+
.update(providerRunners)
|
|
148
|
+
.set({
|
|
149
|
+
workspaceId: reservation.workspaceId,
|
|
150
|
+
reservationId: reservation.id,
|
|
151
|
+
intendedReservationId: null,
|
|
152
|
+
assignedAt: sql`now()`,
|
|
153
|
+
updatedAt: sql`now()`,
|
|
154
|
+
})
|
|
155
|
+
.where(
|
|
156
|
+
inArray(
|
|
157
|
+
providerRunners.id,
|
|
158
|
+
newRunners.map((runner) => runner.id),
|
|
159
|
+
),
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return params.runnerInstanceIds;
|
|
115
163
|
}
|
|
@@ -413,6 +413,134 @@ describe('reportRunnerInstances', () => {
|
|
|
413
413
|
expect(providerRunnerRows[0]?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
414
414
|
});
|
|
415
415
|
|
|
416
|
+
it('releases an intended reservation for a runner that dies before enrollment', async () => {
|
|
417
|
+
const reservationId = await createReservation(2);
|
|
418
|
+
const [runner] = await db()
|
|
419
|
+
.insert(providerRunners)
|
|
420
|
+
.values({
|
|
421
|
+
provisionerId,
|
|
422
|
+
intendedReservationId: reservationId,
|
|
423
|
+
providerRunnerId: 'pre-enrollment-runner',
|
|
424
|
+
state: 'starting',
|
|
425
|
+
reportedAt: new Date(),
|
|
426
|
+
})
|
|
427
|
+
.returning();
|
|
428
|
+
if (!runner) throw new Error('Runner instance insert returned no row');
|
|
429
|
+
if (!runner.providerRunnerId) throw new Error('Runner instance provider id missing');
|
|
430
|
+
|
|
431
|
+
const result = await reportRunnerInstances({
|
|
432
|
+
workspaceId,
|
|
433
|
+
provisionerId,
|
|
434
|
+
events: [event({providerRunnerId: runner.providerRunnerId, state: 'failed'})],
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
const [storedRunner] = await db()
|
|
438
|
+
.select()
|
|
439
|
+
.from(providerRunners)
|
|
440
|
+
.where(eq(providerRunners.id, runner.id));
|
|
441
|
+
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
442
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 1, terminateIntentsHonored: []});
|
|
443
|
+
expect(reservationRows[0]?.count).toBe(1);
|
|
444
|
+
expect(storedRunner?.intendedReservationId).toBeNull();
|
|
445
|
+
expect(storedRunner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
it('releases an intended reservation from an installation-scoped terminal report', async () => {
|
|
449
|
+
const installationProvisioner = await provisionerTokenFactory.create({
|
|
450
|
+
scope: 'installation',
|
|
451
|
+
workspaceId: null,
|
|
452
|
+
});
|
|
453
|
+
const reservationWorkspaceId = crypto.randomUUID();
|
|
454
|
+
await reservationFactory.create({
|
|
455
|
+
workspaceId: reservationWorkspaceId,
|
|
456
|
+
provisionerId: installationProvisioner.id,
|
|
457
|
+
requiredLabels: ['linux'],
|
|
458
|
+
count: 2,
|
|
459
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
460
|
+
});
|
|
461
|
+
const [reservation] = await db()
|
|
462
|
+
.select()
|
|
463
|
+
.from(reservations)
|
|
464
|
+
.where(
|
|
465
|
+
and(
|
|
466
|
+
eq(reservations.workspaceId, reservationWorkspaceId),
|
|
467
|
+
eq(reservations.provisionerId, installationProvisioner.id),
|
|
468
|
+
),
|
|
469
|
+
)
|
|
470
|
+
.orderBy(desc(reservations.id))
|
|
471
|
+
.limit(1);
|
|
472
|
+
if (!reservation) throw new Error('Expected reservation');
|
|
473
|
+
await db().insert(providerRunners).values({
|
|
474
|
+
provisionerId: installationProvisioner.id,
|
|
475
|
+
intendedReservationId: reservation.id,
|
|
476
|
+
providerRunnerId: 'installation-pre-enrollment-runner',
|
|
477
|
+
state: 'starting',
|
|
478
|
+
reportedAt: new Date(),
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const result = await reportRunnerInstances({
|
|
482
|
+
workspaceId: null,
|
|
483
|
+
provisionerId: installationProvisioner.id,
|
|
484
|
+
events: [event({providerRunnerId: 'installation-pre-enrollment-runner', state: 'failed'})],
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
const [storedReservation] = await db()
|
|
488
|
+
.select()
|
|
489
|
+
.from(reservations)
|
|
490
|
+
.where(eq(reservations.id, reservation.id));
|
|
491
|
+
const [storedRunner] = await db()
|
|
492
|
+
.select()
|
|
493
|
+
.from(providerRunners)
|
|
494
|
+
.where(eq(providerRunners.providerRunnerId, 'installation-pre-enrollment-runner'));
|
|
495
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 1, terminateIntentsHonored: []});
|
|
496
|
+
expect(storedReservation?.count).toBe(1);
|
|
497
|
+
expect(storedRunner?.intendedReservationId).toBeNull();
|
|
498
|
+
expect(storedRunner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('prefers the validated intended reservation when both reservation ids are set', async () => {
|
|
502
|
+
const installationProvisioner = await provisionerTokenFactory.create({
|
|
503
|
+
scope: 'installation',
|
|
504
|
+
workspaceId: null,
|
|
505
|
+
});
|
|
506
|
+
const staleReservationId = await createReservation(1, {
|
|
507
|
+
workspaceId: crypto.randomUUID(),
|
|
508
|
+
provisionerId: installationProvisioner.id,
|
|
509
|
+
});
|
|
510
|
+
const intendedReservationId = await createReservation(1, {
|
|
511
|
+
workspaceId: crypto.randomUUID(),
|
|
512
|
+
provisionerId: installationProvisioner.id,
|
|
513
|
+
});
|
|
514
|
+
await db().insert(providerRunners).values({
|
|
515
|
+
provisionerId: installationProvisioner.id,
|
|
516
|
+
reservationId: staleReservationId,
|
|
517
|
+
intendedReservationId,
|
|
518
|
+
providerRunnerId: 'installation-runner-with-stale-reservation',
|
|
519
|
+
state: 'starting',
|
|
520
|
+
reportedAt: new Date(),
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
const result = await reportRunnerInstances({
|
|
524
|
+
workspaceId: null,
|
|
525
|
+
provisionerId: installationProvisioner.id,
|
|
526
|
+
events: [
|
|
527
|
+
event({providerRunnerId: 'installation-runner-with-stale-reservation', state: 'failed'}),
|
|
528
|
+
],
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
const [staleReservation] = await db()
|
|
532
|
+
.select()
|
|
533
|
+
.from(reservations)
|
|
534
|
+
.where(eq(reservations.id, staleReservationId));
|
|
535
|
+
const [intendedReservation] = await db()
|
|
536
|
+
.select()
|
|
537
|
+
.from(reservations)
|
|
538
|
+
.where(eq(reservations.id, intendedReservationId));
|
|
539
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 1, terminateIntentsHonored: []});
|
|
540
|
+
expect(staleReservation?.count).toBe(1);
|
|
541
|
+
expect(intendedReservation).toBeUndefined();
|
|
542
|
+
});
|
|
543
|
+
|
|
416
544
|
it('does not release a reservation owned by another workspace or provisioner', async () => {
|
|
417
545
|
const otherWorkspaceReservationId = await createReservation(1, {
|
|
418
546
|
workspaceId: crypto.randomUUID(),
|
|
@@ -1045,6 +1173,60 @@ describe('reapStaleRunnerInstances', () => {
|
|
|
1045
1173
|
expect(row).toMatchObject({workspaceId: null, providerRunnerId: null, state: 'failed'});
|
|
1046
1174
|
});
|
|
1047
1175
|
|
|
1176
|
+
it('releases an intended reservation while reaping a stale installation runner', async () => {
|
|
1177
|
+
const installationProvisioner = await provisionerTokenFactory.create({
|
|
1178
|
+
scope: 'installation',
|
|
1179
|
+
workspaceId: null,
|
|
1180
|
+
});
|
|
1181
|
+
const reservationWorkspaceId = crypto.randomUUID();
|
|
1182
|
+
await reservationFactory.create({
|
|
1183
|
+
workspaceId: reservationWorkspaceId,
|
|
1184
|
+
provisionerId: installationProvisioner.id,
|
|
1185
|
+
requiredLabels: ['linux'],
|
|
1186
|
+
count: 1,
|
|
1187
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
1188
|
+
});
|
|
1189
|
+
const [reservation] = await db()
|
|
1190
|
+
.select()
|
|
1191
|
+
.from(reservations)
|
|
1192
|
+
.where(
|
|
1193
|
+
and(
|
|
1194
|
+
eq(reservations.workspaceId, reservationWorkspaceId),
|
|
1195
|
+
eq(reservations.provisionerId, installationProvisioner.id),
|
|
1196
|
+
),
|
|
1197
|
+
)
|
|
1198
|
+
.orderBy(desc(reservations.id))
|
|
1199
|
+
.limit(1);
|
|
1200
|
+
if (!reservation) throw new Error('Expected reservation');
|
|
1201
|
+
const [instance] = await db()
|
|
1202
|
+
.insert(providerRunners)
|
|
1203
|
+
.values({
|
|
1204
|
+
provisionerId: installationProvisioner.id,
|
|
1205
|
+
intendedReservationId: reservation.id,
|
|
1206
|
+
providerRunnerId: 'stale-installation-runner-with-reservation',
|
|
1207
|
+
state: 'starting',
|
|
1208
|
+
reportedAt: staleAt(),
|
|
1209
|
+
updatedAt: staleAt(),
|
|
1210
|
+
})
|
|
1211
|
+
.returning({id: providerRunners.id});
|
|
1212
|
+
if (!instance) throw new Error('Runner instance insert returned no row');
|
|
1213
|
+
|
|
1214
|
+
const result = await reapStaleRunnerInstances({thresholdSeconds: 60, limit: 100});
|
|
1215
|
+
const [row] = await db()
|
|
1216
|
+
.select()
|
|
1217
|
+
.from(providerRunners)
|
|
1218
|
+
.where(eq(providerRunners.id, instance.id));
|
|
1219
|
+
const remainingReservations = await db()
|
|
1220
|
+
.select()
|
|
1221
|
+
.from(reservations)
|
|
1222
|
+
.where(eq(reservations.id, reservation.id));
|
|
1223
|
+
|
|
1224
|
+
expect(result).toEqual({reaped: 1, reservationsReleased: 1});
|
|
1225
|
+
expect(row?.intendedReservationId).toBeNull();
|
|
1226
|
+
expect(row?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1227
|
+
expect(remainingReservations).toHaveLength(0);
|
|
1228
|
+
});
|
|
1229
|
+
|
|
1048
1230
|
it('skips fresh rows, live provisioners, terminal rows, running jobs, and fresh sessions', async () => {
|
|
1049
1231
|
await createRunnerInstance({
|
|
1050
1232
|
providerRunnerId: 'fresh-row',
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
import {releaseReservationUnits} from './reservations.js';
|
|
29
29
|
import {ephemeralRegistrationTokens} from './schema/ephemeral-registration-tokens.js';
|
|
30
30
|
import {provisionerTokens} from './schema/provisioner-tokens.js';
|
|
31
|
+
import {reservations} from './schema/reservations.js';
|
|
31
32
|
import {providerRunners, toRunnerInstance} from './schema/runner-instances.js';
|
|
32
33
|
import {runnerSessions} from './schema/runner-sessions.js';
|
|
33
34
|
import {runningJobExecutions} from './schema/running-job-executions.js';
|
|
@@ -563,7 +564,7 @@ async function releaseTerminalRunnerInstanceReservations(
|
|
|
563
564
|
events: RunnerInstanceReportEvent[],
|
|
564
565
|
): Promise<number> {
|
|
565
566
|
const terminalEvents = events.filter((event) => isTerminalState(event.state));
|
|
566
|
-
if (terminalEvents.length === 0
|
|
567
|
+
if (terminalEvents.length === 0) return 0;
|
|
567
568
|
|
|
568
569
|
return await releaseTerminalRunnerInstanceReservationsByIds(tx, {
|
|
569
570
|
workspaceId: params.workspaceId,
|
|
@@ -575,7 +576,7 @@ async function releaseTerminalRunnerInstanceReservations(
|
|
|
575
576
|
async function releaseTerminalRunnerInstanceReservationsByIds(
|
|
576
577
|
tx: Tx,
|
|
577
578
|
params: {
|
|
578
|
-
workspaceId: string;
|
|
579
|
+
workspaceId: string | null;
|
|
579
580
|
provisionerId: string;
|
|
580
581
|
providerRunnerIds: string[];
|
|
581
582
|
requireUnlinkedSession?: boolean;
|
|
@@ -583,31 +584,115 @@ async function releaseTerminalRunnerInstanceReservationsByIds(
|
|
|
583
584
|
): Promise<number> {
|
|
584
585
|
if (params.providerRunnerIds.length === 0) return 0;
|
|
585
586
|
|
|
587
|
+
const reservationWorkspacePredicate =
|
|
588
|
+
params.workspaceId === null
|
|
589
|
+
? sql``
|
|
590
|
+
: sql`and ${eq(reservations.workspaceId, params.workspaceId)}`;
|
|
591
|
+
|
|
586
592
|
const rows = await tx
|
|
587
593
|
.select({
|
|
588
594
|
id: providerRunners.id,
|
|
589
|
-
|
|
595
|
+
releaseReservationId: sql<string | null>`coalesce(
|
|
596
|
+
(select ${reservations.id}
|
|
597
|
+
from ${reservations}
|
|
598
|
+
where ${reservations.id} = ${providerRunners.intendedReservationId}
|
|
599
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
600
|
+
${reservationWorkspacePredicate}),
|
|
601
|
+
(select ${reservations.id}
|
|
602
|
+
from ${reservations}
|
|
603
|
+
where ${reservations.id} = ${providerRunners.reservationId}
|
|
604
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
605
|
+
${reservationWorkspacePredicate})
|
|
606
|
+
)`,
|
|
607
|
+
releaseReservationWorkspaceId: sql<string | null>`coalesce(
|
|
608
|
+
(select ${reservations.workspaceId}
|
|
609
|
+
from ${reservations}
|
|
610
|
+
where ${reservations.id} = ${providerRunners.intendedReservationId}
|
|
611
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
612
|
+
${reservationWorkspacePredicate}),
|
|
613
|
+
(select ${reservations.workspaceId}
|
|
614
|
+
from ${reservations}
|
|
615
|
+
where ${reservations.id} = ${providerRunners.reservationId}
|
|
616
|
+
and ${reservations.provisionerId} = ${params.provisionerId}
|
|
617
|
+
${reservationWorkspacePredicate})
|
|
618
|
+
)`,
|
|
590
619
|
})
|
|
591
620
|
.from(providerRunners)
|
|
592
621
|
.where(
|
|
593
622
|
and(
|
|
594
|
-
eq(providerRunners.workspaceId, params.workspaceId),
|
|
595
623
|
eq(providerRunners.provisionerId, params.provisionerId),
|
|
596
624
|
inArray(providerRunners.providerRunnerId, params.providerRunnerIds),
|
|
597
625
|
inArray(providerRunners.state, terminalStates),
|
|
598
|
-
|
|
626
|
+
or(
|
|
627
|
+
isNotNull(providerRunners.reservationId),
|
|
628
|
+
isNotNull(providerRunners.intendedReservationId),
|
|
629
|
+
),
|
|
630
|
+
params.workspaceId === null
|
|
631
|
+
? undefined
|
|
632
|
+
: or(
|
|
633
|
+
and(
|
|
634
|
+
eq(providerRunners.workspaceId, params.workspaceId),
|
|
635
|
+
or(
|
|
636
|
+
exists(
|
|
637
|
+
tx
|
|
638
|
+
.select({id: reservations.id})
|
|
639
|
+
.from(reservations)
|
|
640
|
+
.where(
|
|
641
|
+
and(
|
|
642
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
643
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
644
|
+
eq(reservations.id, providerRunners.reservationId),
|
|
645
|
+
),
|
|
646
|
+
),
|
|
647
|
+
),
|
|
648
|
+
exists(
|
|
649
|
+
tx
|
|
650
|
+
.select({id: reservations.id})
|
|
651
|
+
.from(reservations)
|
|
652
|
+
.where(
|
|
653
|
+
and(
|
|
654
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
655
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
656
|
+
eq(reservations.id, providerRunners.intendedReservationId),
|
|
657
|
+
),
|
|
658
|
+
),
|
|
659
|
+
),
|
|
660
|
+
),
|
|
661
|
+
),
|
|
662
|
+
and(
|
|
663
|
+
isNull(providerRunners.workspaceId),
|
|
664
|
+
isNotNull(providerRunners.intendedReservationId),
|
|
665
|
+
exists(
|
|
666
|
+
tx
|
|
667
|
+
.select({id: reservations.id})
|
|
668
|
+
.from(reservations)
|
|
669
|
+
.where(
|
|
670
|
+
and(
|
|
671
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
672
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
673
|
+
eq(reservations.id, providerRunners.intendedReservationId),
|
|
674
|
+
),
|
|
675
|
+
),
|
|
676
|
+
),
|
|
677
|
+
),
|
|
678
|
+
),
|
|
599
679
|
params.requireUnlinkedSession === false
|
|
600
680
|
? undefined
|
|
601
681
|
: isNull(providerRunners.runnerSessionId),
|
|
602
682
|
isNull(providerRunners.reservationReleasedAt),
|
|
603
683
|
),
|
|
604
|
-
)
|
|
684
|
+
)
|
|
685
|
+
.for('update');
|
|
605
686
|
|
|
606
687
|
if (rows.length === 0) return 0;
|
|
607
688
|
|
|
608
689
|
const updated = await tx
|
|
609
690
|
.update(providerRunners)
|
|
610
|
-
.set({
|
|
691
|
+
.set({
|
|
692
|
+
intendedReservationId: null,
|
|
693
|
+
reservationReleasedAt: sql`now()`,
|
|
694
|
+
updatedAt: sql`now()`,
|
|
695
|
+
})
|
|
611
696
|
.where(
|
|
612
697
|
and(
|
|
613
698
|
inArray(
|
|
@@ -620,27 +705,37 @@ async function releaseTerminalRunnerInstanceReservationsByIds(
|
|
|
620
705
|
isNull(providerRunners.reservationReleasedAt),
|
|
621
706
|
),
|
|
622
707
|
)
|
|
623
|
-
.returning({
|
|
624
|
-
|
|
625
|
-
const releasesByReservationId = new Map<
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
);
|
|
708
|
+
.returning({id: providerRunners.id});
|
|
709
|
+
|
|
710
|
+
const releasesByReservationId = new Map<
|
|
711
|
+
string,
|
|
712
|
+
{workspaceId: string; reservationId: string; count: number}
|
|
713
|
+
>();
|
|
714
|
+
const updatedIds = new Set(updated.map((row) => row.id));
|
|
715
|
+
for (const row of rows) {
|
|
716
|
+
if (!updatedIds.has(row.id)) continue;
|
|
717
|
+
if (!row.releaseReservationId || !row.releaseReservationWorkspaceId) continue;
|
|
718
|
+
const key = `${row.releaseReservationWorkspaceId}:${row.releaseReservationId}`;
|
|
719
|
+
const release = releasesByReservationId.get(key) ?? {
|
|
720
|
+
workspaceId: row.releaseReservationWorkspaceId,
|
|
721
|
+
reservationId: row.releaseReservationId,
|
|
722
|
+
count: 0,
|
|
723
|
+
};
|
|
724
|
+
release.count += 1;
|
|
725
|
+
releasesByReservationId.set(key, release);
|
|
632
726
|
}
|
|
633
727
|
|
|
634
728
|
if (releasesByReservationId.size === 0) return 0;
|
|
635
729
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
count,
|
|
642
|
-
})
|
|
643
|
-
}
|
|
730
|
+
let released = 0;
|
|
731
|
+
for (const release of releasesByReservationId.values()) {
|
|
732
|
+
released += await releaseReservationUnits(tx, {
|
|
733
|
+
workspaceId: release.workspaceId,
|
|
734
|
+
provisionerId: params.provisionerId,
|
|
735
|
+
releases: [{reservationId: release.reservationId, count: release.count}],
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
return released;
|
|
644
739
|
}
|
|
645
740
|
|
|
646
741
|
function staleRunnerInstanceCutoff(thresholdSeconds: number): SQL {
|
|
@@ -697,14 +792,14 @@ function groupRunnerInstanceIds(
|
|
|
697
792
|
provisionerId: string;
|
|
698
793
|
providerRunnerId: string | null;
|
|
699
794
|
}>,
|
|
700
|
-
): Array<{workspaceId: string; provisionerId: string; providerRunnerIds: string[]}> {
|
|
795
|
+
): Array<{workspaceId: string | null; provisionerId: string; providerRunnerIds: string[]}> {
|
|
701
796
|
const groups = new Map<
|
|
702
797
|
string,
|
|
703
|
-
{workspaceId: string; provisionerId: string; providerRunnerIds: string[]}
|
|
798
|
+
{workspaceId: string | null; provisionerId: string; providerRunnerIds: string[]}
|
|
704
799
|
>();
|
|
705
800
|
for (const row of rows) {
|
|
706
|
-
if (!row.
|
|
707
|
-
const key = `${row.workspaceId}:${row.provisionerId}`;
|
|
801
|
+
if (!row.providerRunnerId) continue;
|
|
802
|
+
const key = `${row.workspaceId ?? ''}:${row.provisionerId}`;
|
|
708
803
|
const group = groups.get(key) ?? {
|
|
709
804
|
workspaceId: row.workspaceId,
|
|
710
805
|
provisionerId: row.provisionerId,
|
|
@@ -23,6 +23,7 @@ export const providerRunners = pgTable(
|
|
|
23
23
|
workspaceId: uuid('workspace_id'),
|
|
24
24
|
provisionerId: uuid('provisioner_id').notNull(),
|
|
25
25
|
providerRunnerId: text('provider_runner_id'),
|
|
26
|
+
intendedReservationId: uuid('intended_reservation_id'),
|
|
26
27
|
reservationId: uuid('reservation_id'),
|
|
27
28
|
assignedAt: timestamp('assigned_at', {withTimezone: true}),
|
|
28
29
|
templateKey: text('template_key'),
|
|
@@ -53,6 +54,14 @@ export const providerRunners = pgTable(
|
|
|
53
54
|
table.updatedAt,
|
|
54
55
|
table.reportedAt,
|
|
55
56
|
),
|
|
57
|
+
index('runners_runner_instances_provisioner_intended_reservation_idx')
|
|
58
|
+
.on(table.provisionerId, table.intendedReservationId)
|
|
59
|
+
.where(
|
|
60
|
+
sql`${table.intendedReservationId} is not null and ${table.reservationReleasedAt} is null`,
|
|
61
|
+
),
|
|
62
|
+
index('runners_runner_instances_provisioner_reservation_idx')
|
|
63
|
+
.on(table.provisionerId, table.reservationId)
|
|
64
|
+
.where(sql`${table.reservationId} is not null`),
|
|
56
65
|
index('runners_runner_instances_active_template_counts_idx')
|
|
57
66
|
.on(table.provisionerId, table.state, table.templateKey)
|
|
58
67
|
.where(sql`"state" in ('starting', 'running') and "template_key" is not null`),
|