@shipfox/api-runners 12.2.0 → 12.3.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 +12 -0
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +14 -2
- package/dist/core/runner-control-sessions.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/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +22 -6
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-instances.d.ts +3 -0
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +10 -1
- 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/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +20 -4
- package/dist/metrics/service.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/config.test.ts +2 -2
- package/src/config.ts +3 -3
- package/src/core/runner-control-sessions.test.ts +169 -4
- package/src/core/runner-control-sessions.ts +22 -1
- package/src/db/index.ts +1 -0
- package/src/db/reservations.test.ts +369 -1
- package/src/db/reservations.ts +74 -9
- package/src/db/runner-instances.test.ts +231 -0
- package/src/db/runner-instances.ts +32 -1
- package/src/db/runner-sessions.ts +26 -15
- package/src/metrics/service.test.ts +99 -0
- package/src/metrics/service.ts +24 -4
- package/src/presentation/routes/late-runner-enrollment.test.ts +230 -0
- package/src/presentation/routes/poll-demand.test.ts +23 -6
- package/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -86,6 +86,350 @@ describe('pollDemandAndReserve', () => {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
it('binds a runner whose intended reservation has passed its activation grace period', async () => {
|
|
90
|
+
const intendedReservation = await createIntendedReservation({
|
|
91
|
+
workspaceId,
|
|
92
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
93
|
+
});
|
|
94
|
+
const runner = await createIdleRunner({
|
|
95
|
+
labels: ['linux'],
|
|
96
|
+
intendedReservationId: intendedReservation.id,
|
|
97
|
+
});
|
|
98
|
+
await createPendingJobs(1, ['linux']);
|
|
99
|
+
|
|
100
|
+
const result = await pollDemandAndReserve({
|
|
101
|
+
workspaceId,
|
|
102
|
+
provisionerId,
|
|
103
|
+
maxReservations: 1,
|
|
104
|
+
ttlSeconds: 60,
|
|
105
|
+
templates: [template('linux', ['linux'], 1)],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const [storedRunner] = await db()
|
|
109
|
+
.select()
|
|
110
|
+
.from(providerRunners)
|
|
111
|
+
.where(eq(providerRunners.id, runner.id));
|
|
112
|
+
expect(storedRunner).toMatchObject({
|
|
113
|
+
workspaceId,
|
|
114
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
115
|
+
intendedReservationId: null,
|
|
116
|
+
assignedAt: expect.any(Date),
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('binds a runner whose intended reservation was deleted after its grace period', async () => {
|
|
121
|
+
const intendedReservation = await createIntendedReservation({
|
|
122
|
+
workspaceId,
|
|
123
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
124
|
+
});
|
|
125
|
+
await db().delete(reservations).where(eq(reservations.id, intendedReservation.id));
|
|
126
|
+
const runner = await createIdleRunner({
|
|
127
|
+
labels: ['linux'],
|
|
128
|
+
intendedReservationId: intendedReservation.id,
|
|
129
|
+
});
|
|
130
|
+
await createPendingJobs(1, ['linux']);
|
|
131
|
+
|
|
132
|
+
const result = await pollDemandAndReserve({
|
|
133
|
+
workspaceId,
|
|
134
|
+
provisionerId,
|
|
135
|
+
maxReservations: 1,
|
|
136
|
+
ttlSeconds: 60,
|
|
137
|
+
templates: [template('linux', ['linux'], 1)],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const [storedRunner] = await db()
|
|
141
|
+
.select()
|
|
142
|
+
.from(providerRunners)
|
|
143
|
+
.where(eq(providerRunners.id, runner.id));
|
|
144
|
+
expect(storedRunner).toMatchObject({
|
|
145
|
+
workspaceId,
|
|
146
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
147
|
+
intendedReservationId: null,
|
|
148
|
+
assignedAt: expect.any(Date),
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('binds an assigned runner after its reservation grace period expires', async () => {
|
|
153
|
+
const staleReservation = await createIntendedReservation({
|
|
154
|
+
workspaceId,
|
|
155
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
156
|
+
});
|
|
157
|
+
const runner = await createIdleRunner({
|
|
158
|
+
labels: ['linux'],
|
|
159
|
+
workspaceId,
|
|
160
|
+
reservationId: staleReservation.id,
|
|
161
|
+
intendedReservationId: staleReservation.id,
|
|
162
|
+
});
|
|
163
|
+
const [activationToken] = await db()
|
|
164
|
+
.insert(runnerActivationTokens)
|
|
165
|
+
.values({
|
|
166
|
+
runnerInstanceId: runner.id,
|
|
167
|
+
hashedToken: crypto.randomUUID(),
|
|
168
|
+
prefix: 'test',
|
|
169
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
170
|
+
})
|
|
171
|
+
.returning();
|
|
172
|
+
if (!activationToken) throw new Error('Expected activation token');
|
|
173
|
+
await createPendingJobs(1, ['linux']);
|
|
174
|
+
|
|
175
|
+
const result = await pollDemandAndReserve({
|
|
176
|
+
workspaceId,
|
|
177
|
+
provisionerId,
|
|
178
|
+
maxReservations: 1,
|
|
179
|
+
ttlSeconds: 60,
|
|
180
|
+
templates: [template('linux', ['linux'], 1)],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const [storedRunner] = await db()
|
|
184
|
+
.select()
|
|
185
|
+
.from(providerRunners)
|
|
186
|
+
.where(eq(providerRunners.id, runner.id));
|
|
187
|
+
const [storedToken] = await db()
|
|
188
|
+
.select()
|
|
189
|
+
.from(runnerActivationTokens)
|
|
190
|
+
.where(eq(runnerActivationTokens.id, activationToken.id));
|
|
191
|
+
expect(storedRunner).toMatchObject({
|
|
192
|
+
workspaceId,
|
|
193
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
194
|
+
intendedReservationId: null,
|
|
195
|
+
assignedAt: expect.any(Date),
|
|
196
|
+
});
|
|
197
|
+
expect(storedToken?.revokedAt).toBeInstanceOf(Date);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('binds an assigned runner whose reservation was deleted', async () => {
|
|
201
|
+
const staleReservation = await createIntendedReservation({
|
|
202
|
+
workspaceId,
|
|
203
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
204
|
+
});
|
|
205
|
+
await db().delete(reservations).where(eq(reservations.id, staleReservation.id));
|
|
206
|
+
const runner = await createIdleRunner({
|
|
207
|
+
labels: ['linux'],
|
|
208
|
+
workspaceId,
|
|
209
|
+
reservationId: staleReservation.id,
|
|
210
|
+
intendedReservationId: staleReservation.id,
|
|
211
|
+
});
|
|
212
|
+
await createPendingJobs(1, ['linux']);
|
|
213
|
+
|
|
214
|
+
const result = await pollDemandAndReserve({
|
|
215
|
+
workspaceId,
|
|
216
|
+
provisionerId,
|
|
217
|
+
maxReservations: 1,
|
|
218
|
+
ttlSeconds: 60,
|
|
219
|
+
templates: [template('linux', ['linux'], 1)],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const [storedRunner] = await db()
|
|
223
|
+
.select()
|
|
224
|
+
.from(providerRunners)
|
|
225
|
+
.where(eq(providerRunners.id, runner.id));
|
|
226
|
+
expect(storedRunner).toMatchObject({
|
|
227
|
+
workspaceId,
|
|
228
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
229
|
+
intendedReservationId: null,
|
|
230
|
+
assignedAt: expect.any(Date),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('does not bind an assigned runner within its activation grace period', async () => {
|
|
235
|
+
const liveReservation = await createIntendedReservation({
|
|
236
|
+
workspaceId,
|
|
237
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
238
|
+
});
|
|
239
|
+
const runner = await createIdleRunner({
|
|
240
|
+
labels: ['linux'],
|
|
241
|
+
workspaceId,
|
|
242
|
+
reservationId: liveReservation.id,
|
|
243
|
+
});
|
|
244
|
+
await createPendingJobs(2, ['linux']);
|
|
245
|
+
|
|
246
|
+
const result = await pollDemandAndReserve({
|
|
247
|
+
workspaceId,
|
|
248
|
+
provisionerId,
|
|
249
|
+
maxReservations: 2,
|
|
250
|
+
ttlSeconds: 60,
|
|
251
|
+
templates: [template('linux', ['linux'], 2)],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const [storedRunner] = await db()
|
|
255
|
+
.select()
|
|
256
|
+
.from(providerRunners)
|
|
257
|
+
.where(eq(providerRunners.id, runner.id));
|
|
258
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
259
|
+
expect(storedRunner).toMatchObject({
|
|
260
|
+
workspaceId,
|
|
261
|
+
reservationId: liveReservation.id,
|
|
262
|
+
intendedReservationId: null,
|
|
263
|
+
assignedAt: null,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('does not bind a runner whose intended reservation is within its activation grace period', async () => {
|
|
268
|
+
const intendedReservation = await createIntendedReservation({
|
|
269
|
+
workspaceId: crypto.randomUUID(),
|
|
270
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
271
|
+
});
|
|
272
|
+
const runner = await createIdleRunner({
|
|
273
|
+
labels: ['linux'],
|
|
274
|
+
intendedReservationId: intendedReservation.id,
|
|
275
|
+
});
|
|
276
|
+
await createPendingJobs(1, ['linux']);
|
|
277
|
+
|
|
278
|
+
const result = await pollDemandAndReserve({
|
|
279
|
+
workspaceId,
|
|
280
|
+
provisionerId,
|
|
281
|
+
maxReservations: 1,
|
|
282
|
+
ttlSeconds: 60,
|
|
283
|
+
templates: [template('linux', ['linux'], 1)],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const [storedRunner] = await db()
|
|
287
|
+
.select()
|
|
288
|
+
.from(providerRunners)
|
|
289
|
+
.where(eq(providerRunners.id, runner.id));
|
|
290
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
291
|
+
expect(storedRunner).toMatchObject({
|
|
292
|
+
workspaceId: null,
|
|
293
|
+
reservationId: null,
|
|
294
|
+
intendedReservationId: intendedReservation.id,
|
|
295
|
+
assignedAt: null,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('does not rebind a stale runner from another workspace in a workspace-scoped poll', async () => {
|
|
300
|
+
const previousWorkspaceId = crypto.randomUUID();
|
|
301
|
+
const staleReservation = await createIntendedReservation({
|
|
302
|
+
workspaceId: previousWorkspaceId,
|
|
303
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
304
|
+
});
|
|
305
|
+
const runner = await createIdleRunner({
|
|
306
|
+
labels: ['linux'],
|
|
307
|
+
workspaceId: previousWorkspaceId,
|
|
308
|
+
reservationId: staleReservation.id,
|
|
309
|
+
intendedReservationId: staleReservation.id,
|
|
310
|
+
});
|
|
311
|
+
await createPendingJobs(1, ['linux']);
|
|
312
|
+
|
|
313
|
+
const result = await pollDemandAndReserve({
|
|
314
|
+
workspaceId,
|
|
315
|
+
provisionerId,
|
|
316
|
+
maxReservations: 1,
|
|
317
|
+
ttlSeconds: 60,
|
|
318
|
+
templates: [template('linux', ['linux'], 1)],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const [storedRunner] = await db()
|
|
322
|
+
.select()
|
|
323
|
+
.from(providerRunners)
|
|
324
|
+
.where(eq(providerRunners.id, runner.id));
|
|
325
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
326
|
+
expect(storedRunner).toMatchObject({
|
|
327
|
+
workspaceId: previousWorkspaceId,
|
|
328
|
+
reservationId: staleReservation.id,
|
|
329
|
+
intendedReservationId: staleReservation.id,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('rebinds an unowned runner whose reservation was deleted', async () => {
|
|
334
|
+
const runner = await createIdleRunner({
|
|
335
|
+
labels: ['linux'],
|
|
336
|
+
reservationId: crypto.randomUUID(),
|
|
337
|
+
});
|
|
338
|
+
await createPendingJobs(1, ['linux']);
|
|
339
|
+
|
|
340
|
+
const result = await pollDemandAndReserve({
|
|
341
|
+
workspaceId,
|
|
342
|
+
provisionerId,
|
|
343
|
+
maxReservations: 1,
|
|
344
|
+
ttlSeconds: 60,
|
|
345
|
+
templates: [template('linux', ['linux'], 1)],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const [storedRunner] = await db()
|
|
349
|
+
.select()
|
|
350
|
+
.from(providerRunners)
|
|
351
|
+
.where(eq(providerRunners.id, runner.id));
|
|
352
|
+
expect(storedRunner).toMatchObject({
|
|
353
|
+
workspaceId,
|
|
354
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
355
|
+
intendedReservationId: null,
|
|
356
|
+
assignedAt: expect.any(Date),
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('rebinds a stale runner across workspaces for an installation-scoped poll', async () => {
|
|
361
|
+
const previousWorkspaceId = crypto.randomUUID();
|
|
362
|
+
const targetWorkspaceId = crypto.randomUUID();
|
|
363
|
+
const staleReservation = await createIntendedReservation({
|
|
364
|
+
workspaceId: previousWorkspaceId,
|
|
365
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
366
|
+
});
|
|
367
|
+
const runner = await createIdleRunner({
|
|
368
|
+
labels: ['linux'],
|
|
369
|
+
workspaceId: previousWorkspaceId,
|
|
370
|
+
reservationId: staleReservation.id,
|
|
371
|
+
intendedReservationId: staleReservation.id,
|
|
372
|
+
});
|
|
373
|
+
await pendingJobFactory.create({workspaceId: targetWorkspaceId, requiredLabels: ['linux']});
|
|
374
|
+
|
|
375
|
+
const result = await pollInstallationDemandAndReserve({
|
|
376
|
+
provisionerId,
|
|
377
|
+
maxReservations: 1,
|
|
378
|
+
ttlSeconds: 60,
|
|
379
|
+
templates: [template('linux', ['linux'], 1)],
|
|
380
|
+
capabilityWindowSeconds: 60,
|
|
381
|
+
eligibleWorkspaceIds: new Set([targetWorkspaceId]),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const [storedRunner] = await db()
|
|
385
|
+
.select()
|
|
386
|
+
.from(providerRunners)
|
|
387
|
+
.where(eq(providerRunners.id, runner.id));
|
|
388
|
+
expect(result.reservations[0]).toMatchObject({workspaceId: targetWorkspaceId, count: 1});
|
|
389
|
+
expect(storedRunner).toMatchObject({
|
|
390
|
+
workspaceId: targetWorkspaceId,
|
|
391
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
392
|
+
intendedReservationId: null,
|
|
393
|
+
assignedAt: expect.any(Date),
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('does not rebind a released runner after its reservation expires', async () => {
|
|
398
|
+
const staleReservation = await createIntendedReservation({
|
|
399
|
+
workspaceId,
|
|
400
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
401
|
+
});
|
|
402
|
+
const releasedAt = new Date(Date.now() - 30_000);
|
|
403
|
+
const runner = await createIdleRunner({
|
|
404
|
+
labels: ['linux'],
|
|
405
|
+
workspaceId,
|
|
406
|
+
reservationId: staleReservation.id,
|
|
407
|
+
intendedReservationId: staleReservation.id,
|
|
408
|
+
reservationReleasedAt: releasedAt,
|
|
409
|
+
});
|
|
410
|
+
await createPendingJobs(1, ['linux']);
|
|
411
|
+
|
|
412
|
+
const result = await pollDemandAndReserve({
|
|
413
|
+
workspaceId,
|
|
414
|
+
provisionerId,
|
|
415
|
+
maxReservations: 1,
|
|
416
|
+
ttlSeconds: 60,
|
|
417
|
+
templates: [template('linux', ['linux'], 1)],
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const [storedRunner] = await db()
|
|
421
|
+
.select()
|
|
422
|
+
.from(providerRunners)
|
|
423
|
+
.where(eq(providerRunners.id, runner.id));
|
|
424
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
425
|
+
expect(storedRunner).toMatchObject({
|
|
426
|
+
workspaceId,
|
|
427
|
+
reservationId: staleReservation.id,
|
|
428
|
+
intendedReservationId: staleReservation.id,
|
|
429
|
+
reservationReleasedAt: releasedAt,
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
89
433
|
it('does not bind a running runner without an active control session', async () => {
|
|
90
434
|
const [runner] = await db()
|
|
91
435
|
.insert(providerRunners)
|
|
@@ -808,19 +1152,43 @@ describe('pollDemandAndReserve', () => {
|
|
|
808
1152
|
}
|
|
809
1153
|
}
|
|
810
1154
|
|
|
1155
|
+
async function createIntendedReservation(params: {
|
|
1156
|
+
workspaceId: string;
|
|
1157
|
+
expiresAt: Date;
|
|
1158
|
+
}): Promise<{id: string}> {
|
|
1159
|
+
const [reservation] = await db()
|
|
1160
|
+
.insert(reservations)
|
|
1161
|
+
.values({
|
|
1162
|
+
workspaceId: params.workspaceId,
|
|
1163
|
+
provisionerId,
|
|
1164
|
+
requiredLabels: ['linux'],
|
|
1165
|
+
count: 1,
|
|
1166
|
+
expiresAt: params.expiresAt,
|
|
1167
|
+
})
|
|
1168
|
+
.returning({id: reservations.id});
|
|
1169
|
+
if (!reservation) throw new Error('Expected reservation');
|
|
1170
|
+
return reservation;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
811
1173
|
async function createIdleRunner(params: {
|
|
812
1174
|
labels: string[];
|
|
813
1175
|
createdAt?: Date;
|
|
814
1176
|
controlSessionExpiresAt?: Date;
|
|
815
|
-
|
|
1177
|
+
workspaceId?: string | null;
|
|
1178
|
+
reservationId?: string | null;
|
|
1179
|
+
intendedReservationId?: string | null;
|
|
1180
|
+
reservationReleasedAt?: Date | null;
|
|
816
1181
|
}) {
|
|
817
1182
|
const createdAt = params.createdAt ?? new Date();
|
|
818
1183
|
const [runner] = await db()
|
|
819
1184
|
.insert(providerRunners)
|
|
820
1185
|
.values({
|
|
821
1186
|
provisionerId,
|
|
1187
|
+
workspaceId: params.workspaceId,
|
|
1188
|
+
reservationId: params.reservationId,
|
|
822
1189
|
providerRunnerId: crypto.randomUUID(),
|
|
823
1190
|
intendedReservationId: params.intendedReservationId,
|
|
1191
|
+
reservationReleasedAt: params.reservationReleasedAt,
|
|
824
1192
|
labels: params.labels,
|
|
825
1193
|
state: 'running',
|
|
826
1194
|
reportedAt: createdAt,
|
package/src/db/reservations.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
isNotNull,
|
|
11
11
|
isNull,
|
|
12
12
|
lt,
|
|
13
|
+
notExists,
|
|
13
14
|
or,
|
|
14
15
|
sql,
|
|
15
16
|
} from 'drizzle-orm';
|
|
@@ -67,6 +68,12 @@ export interface InstallationPollDemandAndReserveParams {
|
|
|
67
68
|
onReservations?: (reservations: ReservationGrant[]) => void;
|
|
68
69
|
}
|
|
69
70
|
|
|
71
|
+
type DemandScope = 'installation' | 'workspace';
|
|
72
|
+
|
|
73
|
+
type PollDemandAndReserveLockedParams = PollDemandAndReserveParams & {
|
|
74
|
+
scope: DemandScope;
|
|
75
|
+
};
|
|
76
|
+
|
|
70
77
|
interface NormalizedTemplate {
|
|
71
78
|
templateKey: string;
|
|
72
79
|
labels: string[];
|
|
@@ -92,7 +99,7 @@ export async function pollDemandAndReserveTx(
|
|
|
92
99
|
params: PollDemandAndReserveParams,
|
|
93
100
|
): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
|
|
94
101
|
await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId}))`);
|
|
95
|
-
return await pollDemandAndReserveLockedTx(tx, params);
|
|
102
|
+
return await pollDemandAndReserveLockedTx(tx, {...params, scope: 'workspace'});
|
|
96
103
|
}
|
|
97
104
|
|
|
98
105
|
export async function pollInstallationDemandAndReserve(
|
|
@@ -126,6 +133,7 @@ export async function pollInstallationDemandAndReserve(
|
|
|
126
133
|
availableSlots: template.remainingSlots,
|
|
127
134
|
})),
|
|
128
135
|
capabilityWindowSeconds: params.capabilityWindowSeconds,
|
|
136
|
+
scope: 'installation',
|
|
129
137
|
});
|
|
130
138
|
});
|
|
131
139
|
results.push(result);
|
|
@@ -158,7 +166,7 @@ function consumeInstallationTemplateSlots(
|
|
|
158
166
|
|
|
159
167
|
async function pollDemandAndReserveLockedTx(
|
|
160
168
|
tx: Tx,
|
|
161
|
-
params:
|
|
169
|
+
params: PollDemandAndReserveLockedParams,
|
|
162
170
|
): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
|
|
163
171
|
let demandRows = (
|
|
164
172
|
await tx
|
|
@@ -265,6 +273,7 @@ async function pollDemandAndReserveLockedTx(
|
|
|
265
273
|
requiredLabels: demand.requiredLabels,
|
|
266
274
|
count: grant,
|
|
267
275
|
workspaceId: params.workspaceId,
|
|
276
|
+
scope: params.scope,
|
|
268
277
|
});
|
|
269
278
|
reservedAfterGrant += grant;
|
|
270
279
|
grants.push({
|
|
@@ -296,18 +305,61 @@ async function bindIdleRunnerInstancesTx(
|
|
|
296
305
|
workspaceId: string;
|
|
297
306
|
requiredLabels: string[];
|
|
298
307
|
count: number;
|
|
308
|
+
scope: DemandScope;
|
|
299
309
|
},
|
|
300
310
|
): Promise<void> {
|
|
311
|
+
// Reservation expiry is the activation grace deadline. A live reservation
|
|
312
|
+
// protects a runner that is still booting; an expired or missing row is stale.
|
|
313
|
+
const canBindRunner = () =>
|
|
314
|
+
and(
|
|
315
|
+
or(
|
|
316
|
+
and(isNull(providerRunners.workspaceId), isNull(providerRunners.reservationId)),
|
|
317
|
+
and(
|
|
318
|
+
isNotNull(providerRunners.reservationId),
|
|
319
|
+
params.scope === 'installation'
|
|
320
|
+
? undefined
|
|
321
|
+
: or(
|
|
322
|
+
isNull(providerRunners.workspaceId),
|
|
323
|
+
eq(providerRunners.workspaceId, params.workspaceId),
|
|
324
|
+
),
|
|
325
|
+
notExists(
|
|
326
|
+
tx
|
|
327
|
+
.select({id: reservations.id})
|
|
328
|
+
.from(reservations)
|
|
329
|
+
.where(
|
|
330
|
+
and(
|
|
331
|
+
eq(reservations.id, providerRunners.reservationId),
|
|
332
|
+
gt(reservations.expiresAt, sql`now()`),
|
|
333
|
+
),
|
|
334
|
+
),
|
|
335
|
+
),
|
|
336
|
+
),
|
|
337
|
+
),
|
|
338
|
+
or(
|
|
339
|
+
isNull(providerRunners.intendedReservationId),
|
|
340
|
+
notExists(
|
|
341
|
+
tx
|
|
342
|
+
.select({id: reservations.id})
|
|
343
|
+
.from(reservations)
|
|
344
|
+
.where(
|
|
345
|
+
and(
|
|
346
|
+
eq(reservations.id, providerRunners.intendedReservationId),
|
|
347
|
+
gt(reservations.expiresAt, sql`now()`),
|
|
348
|
+
),
|
|
349
|
+
),
|
|
350
|
+
),
|
|
351
|
+
),
|
|
352
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
353
|
+
isNull(providerRunners.runnerSessionId),
|
|
354
|
+
);
|
|
355
|
+
|
|
301
356
|
const idleRunners = await tx
|
|
302
357
|
.select({id: providerRunners.id})
|
|
303
358
|
.from(providerRunners)
|
|
304
359
|
.where(
|
|
305
360
|
and(
|
|
306
361
|
eq(providerRunners.provisionerId, params.provisionerId),
|
|
307
|
-
|
|
308
|
-
isNull(providerRunners.reservationId),
|
|
309
|
-
isNull(providerRunners.intendedReservationId),
|
|
310
|
-
isNull(providerRunners.runnerSessionId),
|
|
362
|
+
canBindRunner(),
|
|
311
363
|
isNotNull(providerRunners.providerRunnerId),
|
|
312
364
|
eq(providerRunners.state, 'running'),
|
|
313
365
|
arrayContains(providerRunners.labels, params.requiredLabels),
|
|
@@ -332,11 +384,26 @@ async function bindIdleRunnerInstancesTx(
|
|
|
332
384
|
|
|
333
385
|
if (idleRunners.length === 0) return;
|
|
334
386
|
|
|
387
|
+
await tx
|
|
388
|
+
.update(runnerActivationTokens)
|
|
389
|
+
.set({revokedAt: sql`now()`})
|
|
390
|
+
.where(
|
|
391
|
+
and(
|
|
392
|
+
inArray(
|
|
393
|
+
runnerActivationTokens.runnerInstanceId,
|
|
394
|
+
idleRunners.map((runner) => runner.id),
|
|
395
|
+
),
|
|
396
|
+
isNull(runnerActivationTokens.consumedAt),
|
|
397
|
+
isNull(runnerActivationTokens.revokedAt),
|
|
398
|
+
),
|
|
399
|
+
);
|
|
400
|
+
|
|
335
401
|
await tx
|
|
336
402
|
.update(providerRunners)
|
|
337
403
|
.set({
|
|
338
404
|
workspaceId: params.workspaceId,
|
|
339
405
|
reservationId: params.reservationId,
|
|
406
|
+
intendedReservationId: null,
|
|
340
407
|
assignedAt: sql`now()`,
|
|
341
408
|
updatedAt: sql`now()`,
|
|
342
409
|
})
|
|
@@ -346,9 +413,7 @@ async function bindIdleRunnerInstancesTx(
|
|
|
346
413
|
providerRunners.id,
|
|
347
414
|
idleRunners.map((runner) => runner.id),
|
|
348
415
|
),
|
|
349
|
-
|
|
350
|
-
isNull(providerRunners.reservationId),
|
|
351
|
-
isNull(providerRunners.runnerSessionId),
|
|
416
|
+
canBindRunner(),
|
|
352
417
|
),
|
|
353
418
|
);
|
|
354
419
|
}
|