@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {eq} from 'drizzle-orm';
|
|
2
2
|
import {
|
|
3
3
|
ReservationExpiredError,
|
|
4
|
+
ReservationNotFoundError,
|
|
4
5
|
RunnerInstanceAlreadyAssignedError,
|
|
5
6
|
RunnerInstanceNotAssignableError,
|
|
6
7
|
} from '#core/errors.js';
|
|
@@ -41,6 +42,50 @@ describe('assignRunnerInstances', () => {
|
|
|
41
42
|
});
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
it('repairs a provider-reported reservation before assignment commit', async () => {
|
|
46
|
+
const reservation = await createReservation();
|
|
47
|
+
const [runner] = await db()
|
|
48
|
+
.insert(providerRunners)
|
|
49
|
+
.values({
|
|
50
|
+
provisionerId,
|
|
51
|
+
intendedReservationId: reservation.id,
|
|
52
|
+
reservationId: reservation.id,
|
|
53
|
+
providerRunnerId: crypto.randomUUID(),
|
|
54
|
+
labels: ['linux'],
|
|
55
|
+
state: 'running',
|
|
56
|
+
reportedAt: new Date(),
|
|
57
|
+
})
|
|
58
|
+
.returning();
|
|
59
|
+
if (!runner) throw new Error('Runner instance insert returned no row');
|
|
60
|
+
await db()
|
|
61
|
+
.insert(runnerControlSessions)
|
|
62
|
+
.values({
|
|
63
|
+
runnerInstanceId: runner.id,
|
|
64
|
+
provisionerId,
|
|
65
|
+
hashedToken: crypto.randomUUID(),
|
|
66
|
+
prefix: 'test',
|
|
67
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const assigned = await assignRunnerInstances({
|
|
71
|
+
provisionerId,
|
|
72
|
+
reservationId: reservation.id,
|
|
73
|
+
runnerInstanceIds: [runner.id],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(assigned).toEqual([runner.id]);
|
|
77
|
+
const [stored] = await db()
|
|
78
|
+
.select()
|
|
79
|
+
.from(providerRunners)
|
|
80
|
+
.where(eq(providerRunners.id, runner.id));
|
|
81
|
+
expect(stored).toMatchObject({
|
|
82
|
+
workspaceId,
|
|
83
|
+
reservationId: reservation.id,
|
|
84
|
+
intendedReservationId: null,
|
|
85
|
+
assignedAt: expect.any(Date),
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
44
89
|
it('is idempotent for concurrent retries of the same assignment', async () => {
|
|
45
90
|
const reservation = await createReservation();
|
|
46
91
|
const runner = await createEnrolledRunner();
|
|
@@ -80,6 +125,32 @@ describe('assignRunnerInstances', () => {
|
|
|
80
125
|
expect(assigned).toEqual([runner.id]);
|
|
81
126
|
});
|
|
82
127
|
|
|
128
|
+
it('is idempotent for a committed bound reservation assignment', async () => {
|
|
129
|
+
const reservation = await createReservation({kind: 'bound'});
|
|
130
|
+
const runner = await createEnrolledRunner();
|
|
131
|
+
await db()
|
|
132
|
+
.update(providerRunners)
|
|
133
|
+
.set({
|
|
134
|
+
workspaceId,
|
|
135
|
+
reservationId: reservation.id,
|
|
136
|
+
assignedAt: new Date(),
|
|
137
|
+
})
|
|
138
|
+
.where(eq(providerRunners.id, runner.id));
|
|
139
|
+
|
|
140
|
+
const assigned = await assignRunnerInstances({
|
|
141
|
+
provisionerId,
|
|
142
|
+
reservationId: reservation.id,
|
|
143
|
+
runnerInstanceIds: [runner.id],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(assigned).toEqual([runner.id]);
|
|
147
|
+
const [stored] = await db()
|
|
148
|
+
.select()
|
|
149
|
+
.from(providerRunners)
|
|
150
|
+
.where(eq(providerRunners.id, runner.id));
|
|
151
|
+
expect(stored?.intendedReservationId).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
83
154
|
it('rejects expired reservations', async () => {
|
|
84
155
|
const reservation = await createReservation({expiresAt: new Date(Date.now() - 1_000)});
|
|
85
156
|
const runner = await createEnrolledRunner();
|
|
@@ -93,6 +164,19 @@ describe('assignRunnerInstances', () => {
|
|
|
93
164
|
await expect(assignment).rejects.toThrow(ReservationExpiredError);
|
|
94
165
|
});
|
|
95
166
|
|
|
167
|
+
it('rejects bound reservations', async () => {
|
|
168
|
+
const reservation = await createReservation({kind: 'bound'});
|
|
169
|
+
const runner = await createEnrolledRunner();
|
|
170
|
+
|
|
171
|
+
const assignment = assignRunnerInstances({
|
|
172
|
+
provisionerId,
|
|
173
|
+
reservationId: reservation.id,
|
|
174
|
+
runnerInstanceIds: [runner.id],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await expect(assignment).rejects.toThrow(ReservationNotFoundError);
|
|
178
|
+
});
|
|
179
|
+
|
|
96
180
|
it('rejects unenrolled or incompatible runners', async () => {
|
|
97
181
|
const reservation = await createReservation({requiredLabels: ['linux', 'gpu']});
|
|
98
182
|
const runner = await createEnrolledRunner({labels: ['linux']});
|
|
@@ -139,6 +223,25 @@ describe('assignRunnerInstances', () => {
|
|
|
139
223
|
await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
|
|
140
224
|
});
|
|
141
225
|
|
|
226
|
+
it('charges capacity for a committed runner replayed alongside a new one', async () => {
|
|
227
|
+
const reservation = await createReservation();
|
|
228
|
+
const firstRunner = await createEnrolledRunner();
|
|
229
|
+
const secondRunner = await createEnrolledRunner();
|
|
230
|
+
await assignRunnerInstances({
|
|
231
|
+
provisionerId,
|
|
232
|
+
reservationId: reservation.id,
|
|
233
|
+
runnerInstanceIds: [firstRunner.id],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const assignment = assignRunnerInstances({
|
|
237
|
+
provisionerId,
|
|
238
|
+
reservationId: reservation.id,
|
|
239
|
+
runnerInstanceIds: [firstRunner.id, secondRunner.id],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
|
|
243
|
+
});
|
|
244
|
+
|
|
142
245
|
it('does not charge capacity for an already released reservation intent', async () => {
|
|
143
246
|
const reservation = await createReservation();
|
|
144
247
|
await db().insert(providerRunners).values({
|
|
@@ -202,7 +305,7 @@ describe('assignRunnerInstances', () => {
|
|
|
202
305
|
});
|
|
203
306
|
|
|
204
307
|
async function createReservation(
|
|
205
|
-
overrides: Partial<{expiresAt: Date; requiredLabels: string[]}> = {},
|
|
308
|
+
overrides: Partial<{expiresAt: Date; requiredLabels: string[]; kind: 'bound' | 'launch'}> = {},
|
|
206
309
|
) {
|
|
207
310
|
const [reservation] = await db()
|
|
208
311
|
.insert(reservations)
|
|
@@ -211,6 +314,7 @@ describe('assignRunnerInstances', () => {
|
|
|
211
314
|
provisionerId,
|
|
212
315
|
requiredLabels: overrides.requiredLabels ?? ['linux'],
|
|
213
316
|
count: 1,
|
|
317
|
+
kind: overrides.kind ?? 'launch',
|
|
214
318
|
expiresAt: overrides.expiresAt ?? new Date(Date.now() + 60_000),
|
|
215
319
|
})
|
|
216
320
|
.returning();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {and, eq, inArray, isNull, notInArray, or, sql} from 'drizzle-orm';
|
|
1
|
+
import {and, eq, inArray, isNull, ne, notInArray, or, sql} from 'drizzle-orm';
|
|
2
2
|
import {
|
|
3
3
|
ReservationExpiredError,
|
|
4
4
|
ReservationNotFoundError,
|
|
@@ -7,7 +7,8 @@ import {
|
|
|
7
7
|
} from '#core/errors.js';
|
|
8
8
|
import type {Tx} from './db.js';
|
|
9
9
|
import {db} from './db.js';
|
|
10
|
-
import {
|
|
10
|
+
import {lockRunnerReservationAdvisoryKeysTx} from './reservation-locks.js';
|
|
11
|
+
import {terminalStates} from './runner-states.js';
|
|
11
12
|
import {reservations} from './schema/reservations.js';
|
|
12
13
|
import {runnerControlSessions} from './schema/runner-control-sessions.js';
|
|
13
14
|
import {providerRunners} from './schema/runner-instances.js';
|
|
@@ -30,15 +31,17 @@ export async function assignRunnerInstancesTx(
|
|
|
30
31
|
},
|
|
31
32
|
): Promise<string[]> {
|
|
32
33
|
const runnerInstanceIds = [...params.runnerInstanceIds].sort();
|
|
33
|
-
await tx
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
35
|
+
provisionerId: params.provisionerId,
|
|
36
|
+
reservationIds: [params.reservationId],
|
|
37
|
+
});
|
|
36
38
|
const runnerRows = await tx
|
|
37
39
|
.select({
|
|
38
40
|
id: providerRunners.id,
|
|
39
41
|
reservationId: providerRunners.reservationId,
|
|
40
42
|
intendedReservationId: providerRunners.intendedReservationId,
|
|
41
43
|
workspaceId: providerRunners.workspaceId,
|
|
44
|
+
assignedAt: providerRunners.assignedAt,
|
|
42
45
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
43
46
|
labels: providerRunners.labels,
|
|
44
47
|
state: providerRunners.state,
|
|
@@ -53,11 +56,16 @@ export async function assignRunnerInstancesTx(
|
|
|
53
56
|
.for('update');
|
|
54
57
|
|
|
55
58
|
// The assignment outlives its short reservation row. A retry after maintenance
|
|
56
|
-
// cleanup is complete when every owned runner already carries the
|
|
59
|
+
// cleanup is complete when every owned runner already carries the committed assignment.
|
|
57
60
|
if (
|
|
58
61
|
runnerInstanceIds.length > 0 &&
|
|
59
62
|
runnerRows.length === runnerInstanceIds.length &&
|
|
60
|
-
runnerRows.every(
|
|
63
|
+
runnerRows.every(
|
|
64
|
+
(runner) =>
|
|
65
|
+
runner.reservationId === params.reservationId &&
|
|
66
|
+
runner.assignedAt !== null &&
|
|
67
|
+
runner.workspaceId !== null,
|
|
68
|
+
)
|
|
61
69
|
) {
|
|
62
70
|
await tx
|
|
63
71
|
.update(providerRunners)
|
|
@@ -78,6 +86,7 @@ export async function assignRunnerInstancesTx(
|
|
|
78
86
|
and(
|
|
79
87
|
eq(reservations.id, params.reservationId),
|
|
80
88
|
eq(reservations.provisionerId, params.provisionerId),
|
|
89
|
+
eq(reservations.kind, 'launch'),
|
|
81
90
|
),
|
|
82
91
|
)
|
|
83
92
|
.limit(1)
|
|
@@ -110,13 +119,20 @@ export async function assignRunnerInstancesTx(
|
|
|
110
119
|
const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);
|
|
111
120
|
if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))
|
|
112
121
|
throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');
|
|
113
|
-
const newRunners = runners.filter(
|
|
122
|
+
const newRunners = runners.filter(
|
|
123
|
+
(runner) =>
|
|
124
|
+
runner.reservationId === null ||
|
|
125
|
+
(runner.reservationId === reservation.id &&
|
|
126
|
+
(runner.assignedAt === null || runner.workspaceId === null)),
|
|
127
|
+
);
|
|
128
|
+
const newRunnerIds = newRunners.map((runner) => runner.id);
|
|
114
129
|
const assignedCount = await tx
|
|
115
130
|
.select({count: sql<number>`count(*)::int`})
|
|
116
131
|
.from(providerRunners)
|
|
117
132
|
.where(
|
|
118
133
|
and(
|
|
119
134
|
eq(providerRunners.provisionerId, params.provisionerId),
|
|
135
|
+
notInArray(providerRunners.id, newRunnerIds),
|
|
120
136
|
or(
|
|
121
137
|
and(
|
|
122
138
|
eq(providerRunners.reservationId, reservation.id),
|
|
@@ -124,7 +140,6 @@ export async function assignRunnerInstancesTx(
|
|
|
124
140
|
),
|
|
125
141
|
and(
|
|
126
142
|
eq(providerRunners.intendedReservationId, reservation.id),
|
|
127
|
-
notInArray(providerRunners.id, runnerInstanceIds),
|
|
128
143
|
isNull(providerRunners.reservationReleasedAt),
|
|
129
144
|
notInArray(providerRunners.state, [...terminalStates]),
|
|
130
145
|
),
|
|
@@ -161,3 +176,157 @@ export async function assignRunnerInstancesTx(
|
|
|
161
176
|
}
|
|
162
177
|
return params.runnerInstanceIds;
|
|
163
178
|
}
|
|
179
|
+
|
|
180
|
+
export type RunnerReservationCapacityFailureReason =
|
|
181
|
+
| 'reservation-not-found'
|
|
182
|
+
| 'reservation-kind-mismatch'
|
|
183
|
+
| 'reservation-expired'
|
|
184
|
+
| 'capacity-exhausted';
|
|
185
|
+
|
|
186
|
+
export interface RunnerReservationCapacityValidation {
|
|
187
|
+
acceptedByReservation: Map<string, number>;
|
|
188
|
+
unavailableByReservation: Map<
|
|
189
|
+
string,
|
|
190
|
+
{reason: RunnerReservationCapacityFailureReason; count: number}
|
|
191
|
+
>;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export async function validateRunnerReservationCapacityTx(
|
|
195
|
+
tx: Tx,
|
|
196
|
+
params: {
|
|
197
|
+
provisionerId: string;
|
|
198
|
+
requests: readonly {reservationId: string; count: number}[];
|
|
199
|
+
},
|
|
200
|
+
options: {advisoryLocksHeld?: boolean} = {},
|
|
201
|
+
): Promise<RunnerReservationCapacityValidation> {
|
|
202
|
+
const requestedByReservation = new Map<string, number>();
|
|
203
|
+
for (const request of params.requests) {
|
|
204
|
+
if (request.count <= 0) continue;
|
|
205
|
+
requestedByReservation.set(
|
|
206
|
+
request.reservationId,
|
|
207
|
+
(requestedByReservation.get(request.reservationId) ?? 0) + request.count,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const reservationIds = [...requestedByReservation.keys()].sort();
|
|
211
|
+
if (reservationIds.length === 0)
|
|
212
|
+
return {acceptedByReservation: new Map(), unavailableByReservation: new Map()};
|
|
213
|
+
|
|
214
|
+
if (!options.advisoryLocksHeld)
|
|
215
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
216
|
+
provisionerId: params.provisionerId,
|
|
217
|
+
reservationIds,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const reservationRows = await tx
|
|
221
|
+
.select({
|
|
222
|
+
id: reservations.id,
|
|
223
|
+
count: reservations.count,
|
|
224
|
+
expiresAt: reservations.expiresAt,
|
|
225
|
+
isExpired: sql<boolean>`${reservations.expiresAt} <= now()`,
|
|
226
|
+
})
|
|
227
|
+
.from(reservations)
|
|
228
|
+
.where(
|
|
229
|
+
and(
|
|
230
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
231
|
+
inArray(reservations.id, reservationIds),
|
|
232
|
+
eq(reservations.kind, 'launch'),
|
|
233
|
+
),
|
|
234
|
+
)
|
|
235
|
+
.for('update');
|
|
236
|
+
const nonLaunchReservationRows = await tx
|
|
237
|
+
.select({id: reservations.id})
|
|
238
|
+
.from(reservations)
|
|
239
|
+
.where(
|
|
240
|
+
and(
|
|
241
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
242
|
+
inArray(reservations.id, reservationIds),
|
|
243
|
+
ne(reservations.kind, 'launch'),
|
|
244
|
+
),
|
|
245
|
+
)
|
|
246
|
+
.for('update');
|
|
247
|
+
const reservationsById = new Map(
|
|
248
|
+
reservationRows.map((reservation) => [reservation.id, reservation]),
|
|
249
|
+
);
|
|
250
|
+
const nonLaunchReservationIds = new Set(
|
|
251
|
+
nonLaunchReservationRows.map((reservation) => reservation.id),
|
|
252
|
+
);
|
|
253
|
+
const requestedReservationIds = new Set(reservationIds);
|
|
254
|
+
const usedRunnerIdsByReservation = new Map<string, Set<string>>();
|
|
255
|
+
const usedRunnerRows = await tx
|
|
256
|
+
.select({
|
|
257
|
+
id: providerRunners.id,
|
|
258
|
+
reservationId: providerRunners.reservationId,
|
|
259
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
260
|
+
state: providerRunners.state,
|
|
261
|
+
})
|
|
262
|
+
.from(providerRunners)
|
|
263
|
+
.where(
|
|
264
|
+
and(
|
|
265
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
266
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
267
|
+
or(
|
|
268
|
+
inArray(providerRunners.reservationId, reservationIds),
|
|
269
|
+
and(
|
|
270
|
+
inArray(providerRunners.intendedReservationId, reservationIds),
|
|
271
|
+
notInArray(providerRunners.state, [...terminalStates]),
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
);
|
|
276
|
+
for (const runner of usedRunnerRows) {
|
|
277
|
+
if (runner.reservationId && requestedReservationIds.has(runner.reservationId)) {
|
|
278
|
+
const usedRunnerIds =
|
|
279
|
+
usedRunnerIdsByReservation.get(runner.reservationId) ?? new Set<string>();
|
|
280
|
+
usedRunnerIds.add(runner.id);
|
|
281
|
+
usedRunnerIdsByReservation.set(runner.reservationId, usedRunnerIds);
|
|
282
|
+
}
|
|
283
|
+
if (
|
|
284
|
+
runner.intendedReservationId &&
|
|
285
|
+
requestedReservationIds.has(runner.intendedReservationId) &&
|
|
286
|
+
!terminalStates.includes(runner.state as (typeof terminalStates)[number])
|
|
287
|
+
) {
|
|
288
|
+
const usedRunnerIds =
|
|
289
|
+
usedRunnerIdsByReservation.get(runner.intendedReservationId) ?? new Set<string>();
|
|
290
|
+
usedRunnerIds.add(runner.id);
|
|
291
|
+
usedRunnerIdsByReservation.set(runner.intendedReservationId, usedRunnerIds);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const acceptedByReservation = new Map<string, number>();
|
|
296
|
+
const unavailableByReservation = new Map<
|
|
297
|
+
string,
|
|
298
|
+
{reason: RunnerReservationCapacityFailureReason; count: number}
|
|
299
|
+
>();
|
|
300
|
+
for (const reservationId of reservationIds) {
|
|
301
|
+
const requested = requestedByReservation.get(reservationId) ?? 0;
|
|
302
|
+
const reservation = reservationsById.get(reservationId);
|
|
303
|
+
if (!reservation) {
|
|
304
|
+
unavailableByReservation.set(reservationId, {
|
|
305
|
+
reason: nonLaunchReservationIds.has(reservationId)
|
|
306
|
+
? 'reservation-kind-mismatch'
|
|
307
|
+
: 'reservation-not-found',
|
|
308
|
+
count: requested,
|
|
309
|
+
});
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (reservation.isExpired) {
|
|
313
|
+
unavailableByReservation.set(reservationId, {
|
|
314
|
+
reason: 'reservation-expired',
|
|
315
|
+
count: requested,
|
|
316
|
+
});
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const used = usedRunnerIdsByReservation.get(reservationId)?.size ?? 0;
|
|
321
|
+
const accepted = Math.min(requested, Math.max(0, reservation.count - used));
|
|
322
|
+
acceptedByReservation.set(reservationId, accepted);
|
|
323
|
+
if (accepted < requested) {
|
|
324
|
+
unavailableByReservation.set(reservationId, {
|
|
325
|
+
reason: 'capacity-exhausted',
|
|
326
|
+
count: requested - accepted,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {acceptedByReservation, unavailableByReservation};
|
|
332
|
+
}
|