@shipfox/api-runners 12.3.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 +22 -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/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 +55 -9
- 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/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 +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 +2 -2
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +124 -24
- package/dist/db/runner-instances.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/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 +2 -2
- 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/runner-activation.ts +48 -2
- package/src/core/runner-control-sessions.test.ts +64 -3
- package/src/core/runner-control-sessions.ts +62 -9
- 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/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 +178 -9
- package/src/db/runner-instances.test.ts +265 -2
- package/src/db/runner-instances.ts +215 -55
- 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 +8 -1
- 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 +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,9 +7,11 @@ import {
|
|
|
7
7
|
extractBearerToken,
|
|
8
8
|
} from '@shipfox/node-fastify';
|
|
9
9
|
import {vi} from '@shipfox/vitest/vi';
|
|
10
|
-
import {eq} from 'drizzle-orm';
|
|
10
|
+
import {and, eq} from 'drizzle-orm';
|
|
11
11
|
import type {FastifyInstance, FastifyRequest} from 'fastify';
|
|
12
12
|
import {db} from '#db/db.js';
|
|
13
|
+
import {reservations} from '#db/schema/reservations.js';
|
|
14
|
+
import {runnerControlSessions} from '#db/schema/runner-control-sessions.js';
|
|
13
15
|
import {providerRunners} from '#db/schema/runner-instances.js';
|
|
14
16
|
import {runnerSessions} from '#db/schema/runner-sessions.js';
|
|
15
17
|
import {runnerReservationPromotionFailureCount} from '#metrics/instance.js';
|
|
@@ -160,7 +162,22 @@ describe('late runner enrollment recovery', () => {
|
|
|
160
162
|
});
|
|
161
163
|
|
|
162
164
|
expect(demand.statusCode).toBe(200);
|
|
163
|
-
|
|
165
|
+
expect(demand.json().reservations).toEqual([]);
|
|
166
|
+
const reboundReservations = await db()
|
|
167
|
+
.select()
|
|
168
|
+
.from(reservations)
|
|
169
|
+
.where(
|
|
170
|
+
and(
|
|
171
|
+
eq(reservations.workspaceId, workspaceId),
|
|
172
|
+
eq(reservations.provisionerId, provisionerId),
|
|
173
|
+
eq(reservations.kind, 'bound'),
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
const reboundReservation = reboundReservations.find(
|
|
177
|
+
(reservation) => reservation.id !== arrangement.reservationId,
|
|
178
|
+
);
|
|
179
|
+
const reboundReservationId = reboundReservation?.id;
|
|
180
|
+
if (!reboundReservationId) throw new Error('Expected rebound reservation');
|
|
164
181
|
expect(reboundReservationId).toEqual(expect.any(String));
|
|
165
182
|
|
|
166
183
|
const [reboundRunner] = await db()
|
|
@@ -216,6 +233,206 @@ describe('late runner enrollment recovery', () => {
|
|
|
216
233
|
});
|
|
217
234
|
});
|
|
218
235
|
|
|
236
|
+
it('preserves a rebound assignment across stale provider reports and releases it once', async () => {
|
|
237
|
+
const providerRunnerId = `stale-assignment-${crypto.randomUUID()}`;
|
|
238
|
+
const firstReportAt = new Date(Date.now() - 2_000);
|
|
239
|
+
const [staleReservation] = await db()
|
|
240
|
+
.insert(reservations)
|
|
241
|
+
.values({
|
|
242
|
+
workspaceId,
|
|
243
|
+
provisionerId,
|
|
244
|
+
requiredLabels: ['linux'],
|
|
245
|
+
count: 1,
|
|
246
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
247
|
+
})
|
|
248
|
+
.returning({id: reservations.id});
|
|
249
|
+
if (!staleReservation) throw new Error('Expected stale reservation');
|
|
250
|
+
|
|
251
|
+
const initialReport = await app.inject({
|
|
252
|
+
method: 'POST',
|
|
253
|
+
url: '/provisioners/runner-instances/report',
|
|
254
|
+
headers: {authorization: `Bearer ${workspaceToken}`},
|
|
255
|
+
payload: {
|
|
256
|
+
events: [
|
|
257
|
+
{
|
|
258
|
+
provider_runner_id: providerRunnerId,
|
|
259
|
+
reservation_id: staleReservation.id,
|
|
260
|
+
template_key: 'linux',
|
|
261
|
+
labels: ['linux'],
|
|
262
|
+
state: 'running',
|
|
263
|
+
reported_at: firstReportAt.toISOString(),
|
|
264
|
+
provider_kind: 'docker',
|
|
265
|
+
},
|
|
266
|
+
],
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
expect(initialReport.statusCode).toBe(200);
|
|
271
|
+
expect(initialReport.json()).toEqual({accepted: 1, reservations_released: 0});
|
|
272
|
+
|
|
273
|
+
const [reportedRunner] = await db()
|
|
274
|
+
.select()
|
|
275
|
+
.from(providerRunners)
|
|
276
|
+
.where(
|
|
277
|
+
and(
|
|
278
|
+
eq(providerRunners.provisionerId, provisionerId),
|
|
279
|
+
eq(providerRunners.providerRunnerId, providerRunnerId),
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
if (!reportedRunner) throw new Error('Expected reported runner');
|
|
283
|
+
expect(reportedRunner).toMatchObject({
|
|
284
|
+
workspaceId,
|
|
285
|
+
reservationId: staleReservation.id,
|
|
286
|
+
state: 'running',
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
await db()
|
|
290
|
+
.insert(runnerControlSessions)
|
|
291
|
+
.values({
|
|
292
|
+
runnerInstanceId: reportedRunner.id,
|
|
293
|
+
provisionerId,
|
|
294
|
+
hashedToken: crypto.randomUUID(),
|
|
295
|
+
prefix: 'test',
|
|
296
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
297
|
+
});
|
|
298
|
+
await db()
|
|
299
|
+
.update(reservations)
|
|
300
|
+
.set({expiresAt: new Date(Date.now() - 1_000)})
|
|
301
|
+
.where(eq(reservations.id, staleReservation.id));
|
|
302
|
+
await pendingJobFactory.create({workspaceId, requiredLabels: ['linux']});
|
|
303
|
+
|
|
304
|
+
const demand = await app.inject({
|
|
305
|
+
method: 'POST',
|
|
306
|
+
url: '/provisioners/demand/poll',
|
|
307
|
+
headers: {authorization: `Bearer ${workspaceToken}`},
|
|
308
|
+
payload: {
|
|
309
|
+
wait_seconds: 0,
|
|
310
|
+
max_reservations: 1,
|
|
311
|
+
reservation_ttl_seconds: 60,
|
|
312
|
+
templates: [
|
|
313
|
+
{
|
|
314
|
+
template_key: 'linux',
|
|
315
|
+
labels: ['linux'],
|
|
316
|
+
available_slots: 1,
|
|
317
|
+
starting: 0,
|
|
318
|
+
running: 1,
|
|
319
|
+
},
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(demand.statusCode).toBe(200);
|
|
325
|
+
expect(demand.json().reservations).toEqual([]);
|
|
326
|
+
const reboundReservations = await db()
|
|
327
|
+
.select()
|
|
328
|
+
.from(reservations)
|
|
329
|
+
.where(
|
|
330
|
+
and(
|
|
331
|
+
eq(reservations.workspaceId, workspaceId),
|
|
332
|
+
eq(reservations.provisionerId, provisionerId),
|
|
333
|
+
eq(reservations.kind, 'bound'),
|
|
334
|
+
),
|
|
335
|
+
);
|
|
336
|
+
const reboundReservation = reboundReservations.find(
|
|
337
|
+
(reservation) => reservation.id !== staleReservation.id,
|
|
338
|
+
);
|
|
339
|
+
const reboundReservationId = reboundReservation?.id;
|
|
340
|
+
if (!reboundReservationId) throw new Error('Expected rebound reservation');
|
|
341
|
+
expect(reboundReservationId).toEqual(expect.any(String));
|
|
342
|
+
expect(reboundReservationId).not.toBe(staleReservation.id);
|
|
343
|
+
|
|
344
|
+
const staleReport = await app.inject({
|
|
345
|
+
method: 'POST',
|
|
346
|
+
url: '/provisioners/runner-instances/report',
|
|
347
|
+
headers: {authorization: `Bearer ${workspaceToken}`},
|
|
348
|
+
payload: {
|
|
349
|
+
events: [
|
|
350
|
+
{
|
|
351
|
+
provider_runner_id: providerRunnerId,
|
|
352
|
+
reservation_id: staleReservation.id,
|
|
353
|
+
template_key: 'linux',
|
|
354
|
+
labels: ['linux'],
|
|
355
|
+
state: 'running',
|
|
356
|
+
reported_at: new Date(firstReportAt.getTime() + 1_000).toISOString(),
|
|
357
|
+
provider_kind: 'docker',
|
|
358
|
+
},
|
|
359
|
+
],
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
expect(staleReport.statusCode).toBe(200);
|
|
364
|
+
expect(staleReport.json()).toEqual({accepted: 1, reservations_released: 0});
|
|
365
|
+
|
|
366
|
+
const [reboundRunner] = await db()
|
|
367
|
+
.select()
|
|
368
|
+
.from(providerRunners)
|
|
369
|
+
.where(eq(providerRunners.id, reportedRunner.id));
|
|
370
|
+
expect(reboundRunner).toMatchObject({
|
|
371
|
+
workspaceId,
|
|
372
|
+
reservationId: reboundReservationId,
|
|
373
|
+
state: 'running',
|
|
374
|
+
intendedReservationId: null,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
const terminalReport = await app.inject({
|
|
378
|
+
method: 'POST',
|
|
379
|
+
url: '/provisioners/runner-instances/report',
|
|
380
|
+
headers: {authorization: `Bearer ${workspaceToken}`},
|
|
381
|
+
payload: {
|
|
382
|
+
events: [
|
|
383
|
+
{
|
|
384
|
+
provider_runner_id: providerRunnerId,
|
|
385
|
+
reservation_id: staleReservation.id,
|
|
386
|
+
template_key: 'linux',
|
|
387
|
+
labels: ['linux'],
|
|
388
|
+
state: 'terminated',
|
|
389
|
+
reported_at: new Date(firstReportAt.getTime() + 2_000).toISOString(),
|
|
390
|
+
provider_kind: 'docker',
|
|
391
|
+
},
|
|
392
|
+
],
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
expect(terminalReport.statusCode).toBe(200);
|
|
397
|
+
expect(terminalReport.json()).toEqual({accepted: 1, reservations_released: 1});
|
|
398
|
+
|
|
399
|
+
const [staleReservationAfterTerminal] = await db()
|
|
400
|
+
.select()
|
|
401
|
+
.from(reservations)
|
|
402
|
+
.where(eq(reservations.id, staleReservation.id));
|
|
403
|
+
const [reboundReservationAfterTerminal] = await db()
|
|
404
|
+
.select()
|
|
405
|
+
.from(reservations)
|
|
406
|
+
.where(eq(reservations.id, reboundReservationId));
|
|
407
|
+
expect(staleReservationAfterTerminal).toMatchObject({
|
|
408
|
+
id: staleReservation.id,
|
|
409
|
+
count: 1,
|
|
410
|
+
});
|
|
411
|
+
expect(reboundReservationAfterTerminal).toBeUndefined();
|
|
412
|
+
|
|
413
|
+
const repeatedTerminalReport = await app.inject({
|
|
414
|
+
method: 'POST',
|
|
415
|
+
url: '/provisioners/runner-instances/report',
|
|
416
|
+
headers: {authorization: `Bearer ${workspaceToken}`},
|
|
417
|
+
payload: {
|
|
418
|
+
events: [
|
|
419
|
+
{
|
|
420
|
+
provider_runner_id: providerRunnerId,
|
|
421
|
+
reservation_id: staleReservation.id,
|
|
422
|
+
template_key: 'linux',
|
|
423
|
+
labels: ['linux'],
|
|
424
|
+
state: 'terminated',
|
|
425
|
+
reported_at: new Date(firstReportAt.getTime() + 3_000).toISOString(),
|
|
426
|
+
provider_kind: 'docker',
|
|
427
|
+
},
|
|
428
|
+
],
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
expect(repeatedTerminalReport.statusCode).toBe(200);
|
|
433
|
+
expect(repeatedTerminalReport.json()).toEqual({accepted: 1, reservations_released: 0});
|
|
434
|
+
});
|
|
435
|
+
|
|
219
436
|
async function enrollRunner(controlSessionToken: string): Promise<void> {
|
|
220
437
|
const enrolled = await app.inject({
|
|
221
438
|
method: 'POST',
|
|
@@ -20,6 +20,7 @@ import {db} from '#db/db.js';
|
|
|
20
20
|
import {provisionerCapabilitySnapshots} from '#db/schema/provisioner-capability-snapshots.js';
|
|
21
21
|
import {runningJobExecutions} from '#db/schema/running-job-executions.js';
|
|
22
22
|
import {
|
|
23
|
+
providerRunnerActivationOutcomeCount,
|
|
23
24
|
providerRunnerCountDivergenceCount,
|
|
24
25
|
providerRunnerTerminateIntentIssuedCount,
|
|
25
26
|
} from '#metrics/instance.js';
|
|
@@ -99,6 +100,7 @@ describe('POST /provisioners/demand/poll', () => {
|
|
|
99
100
|
expect(res.json()).toMatchObject({
|
|
100
101
|
stats: [{labels: ['linux'], queued: 1, reserved: 1}],
|
|
101
102
|
reservations: [{labels: ['linux'], count: 1}],
|
|
103
|
+
newly_reserved_count: 1,
|
|
102
104
|
terminate_provider_runner_ids: [],
|
|
103
105
|
});
|
|
104
106
|
expect(res.json().reservations[0].reservation_id).toEqual(expect.any(String));
|
|
@@ -367,6 +369,60 @@ describe('POST /provisioners/demand/poll', () => {
|
|
|
367
369
|
expect(intentCalls).toHaveLength(1);
|
|
368
370
|
});
|
|
369
371
|
|
|
372
|
+
it('counts activation-timeout reaps once across terminate-intent retries', async () => {
|
|
373
|
+
const intentSpy = vi.spyOn(providerRunnerTerminateIntentIssuedCount, 'add');
|
|
374
|
+
const outcomeSpy = vi.spyOn(providerRunnerActivationOutcomeCount, 'add');
|
|
375
|
+
await providerRunnerFactory.create({
|
|
376
|
+
workspaceId,
|
|
377
|
+
provisionerId: provisionerTokenId,
|
|
378
|
+
providerRunnerId: 'stale-demand-runner',
|
|
379
|
+
launchKind: 'demand',
|
|
380
|
+
createdAt: new Date(Date.now() - 301_000),
|
|
381
|
+
templateKey: 'linux',
|
|
382
|
+
state: 'running',
|
|
383
|
+
});
|
|
384
|
+
const intentCallsBefore = intentSpy.mock.calls.length;
|
|
385
|
+
const outcomeCallsBefore = outcomeSpy.mock.calls.length;
|
|
386
|
+
|
|
387
|
+
const poll = () =>
|
|
388
|
+
app.inject({
|
|
389
|
+
method: 'POST',
|
|
390
|
+
url: '/provisioners/demand/poll',
|
|
391
|
+
headers: {authorization: `Bearer ${VALID_PROVISIONER_TOKEN}`},
|
|
392
|
+
payload: body({max_reservations: 0}),
|
|
393
|
+
});
|
|
394
|
+
const firstResponse = await poll();
|
|
395
|
+
const retryResponse = await poll();
|
|
396
|
+
|
|
397
|
+
expect(firstResponse.statusCode).toBe(200);
|
|
398
|
+
expect(retryResponse.statusCode).toBe(200);
|
|
399
|
+
expect(firstResponse.json()).toMatchObject({
|
|
400
|
+
reservations: [],
|
|
401
|
+
terminate_provider_runner_ids: ['stale-demand-runner'],
|
|
402
|
+
});
|
|
403
|
+
expect(retryResponse.json()).toMatchObject({
|
|
404
|
+
reservations: [],
|
|
405
|
+
terminate_provider_runner_ids: ['stale-demand-runner'],
|
|
406
|
+
});
|
|
407
|
+
const intentCalls = intentSpy.mock.calls
|
|
408
|
+
.slice(intentCallsBefore)
|
|
409
|
+
.filter(
|
|
410
|
+
([value, attributes]) =>
|
|
411
|
+
value === 1 &&
|
|
412
|
+
JSON.stringify(attributes) ===
|
|
413
|
+
JSON.stringify({surface: 'poll-demand', reason: 'activation-timeout'}),
|
|
414
|
+
);
|
|
415
|
+
expect(intentCalls).toHaveLength(2);
|
|
416
|
+
expect(
|
|
417
|
+
outcomeSpy.mock.calls
|
|
418
|
+
.slice(outcomeCallsBefore)
|
|
419
|
+
.filter(
|
|
420
|
+
([value, attributes]) =>
|
|
421
|
+
value === 1 && JSON.stringify(attributes) === JSON.stringify({outcome: 'reaped'}),
|
|
422
|
+
),
|
|
423
|
+
).toHaveLength(1);
|
|
424
|
+
});
|
|
425
|
+
|
|
370
426
|
it('returns 400 for max reservations above the request bound', async () => {
|
|
371
427
|
const res = await app.inject({
|
|
372
428
|
method: 'POST',
|
|
@@ -98,6 +98,7 @@ export function createPollDemandRoute(options: CreateRunnersModuleOptions = {})
|
|
|
98
98
|
provisionerId: provisionerContext.provisionerTokenId,
|
|
99
99
|
maxReservations: request.body.max_reservations,
|
|
100
100
|
ttlSeconds,
|
|
101
|
+
activationGraceSeconds: config.RESERVATION_TTL_SECONDS,
|
|
101
102
|
templates,
|
|
102
103
|
capabilityWindowSeconds: config.PROVISIONER_ACTIVE_WINDOW_SECONDS,
|
|
103
104
|
eligibleWorkspaceIds,
|
|
@@ -6,6 +6,10 @@ import {
|
|
|
6
6
|
AUTH_USER,
|
|
7
7
|
setProvisionerContext,
|
|
8
8
|
} from '@shipfox/api-auth-context';
|
|
9
|
+
import {
|
|
10
|
+
RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER,
|
|
11
|
+
RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER_VALUE,
|
|
12
|
+
} from '@shipfox/api-runners-dto';
|
|
9
13
|
import {
|
|
10
14
|
type AuthMethod,
|
|
11
15
|
ClientError,
|
|
@@ -86,7 +90,11 @@ describe('POST /provisioners/runner-instances/reconcile', () => {
|
|
|
86
90
|
const jobId = crypto.randomUUID();
|
|
87
91
|
const workflowRunId = crypto.randomUUID();
|
|
88
92
|
const workflowRunAttemptId = crypto.randomUUID();
|
|
89
|
-
|
|
93
|
+
const intendedReservationId = crypto.randomUUID();
|
|
94
|
+
await createRunnerInstance({
|
|
95
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
96
|
+
intendedReservationId,
|
|
97
|
+
});
|
|
90
98
|
await insertRunningJob({
|
|
91
99
|
jobId,
|
|
92
100
|
workflowRunId,
|
|
@@ -98,7 +106,11 @@ describe('POST /provisioners/runner-instances/reconcile', () => {
|
|
|
98
106
|
const res = await app.inject({
|
|
99
107
|
method: 'POST',
|
|
100
108
|
url: '/provisioners/runner-instances/reconcile',
|
|
101
|
-
headers: {
|
|
109
|
+
headers: {
|
|
110
|
+
authorization: `Bearer ${VALID_PROVISIONER_TOKEN}`,
|
|
111
|
+
[RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER]:
|
|
112
|
+
RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER_VALUE,
|
|
113
|
+
},
|
|
102
114
|
payload: {observed_provider_runner_ids: ['provisioned-runner-1']},
|
|
103
115
|
});
|
|
104
116
|
|
|
@@ -108,6 +120,7 @@ describe('POST /provisioners/runner-instances/reconcile', () => {
|
|
|
108
120
|
{
|
|
109
121
|
provider_runner_id: 'provisioned-runner-1',
|
|
110
122
|
state: 'running',
|
|
123
|
+
intended_reservation_id: intendedReservationId,
|
|
111
124
|
reservation_id: null,
|
|
112
125
|
runner_session_id: null,
|
|
113
126
|
bound_job: {
|
|
@@ -123,6 +136,23 @@ describe('POST /provisioners/runner-instances/reconcile', () => {
|
|
|
123
136
|
});
|
|
124
137
|
});
|
|
125
138
|
|
|
139
|
+
it('omits the additive reservation field for legacy provisioners', async () => {
|
|
140
|
+
await createRunnerInstance({
|
|
141
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
142
|
+
intendedReservationId: crypto.randomUUID(),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const res = await app.inject({
|
|
146
|
+
method: 'POST',
|
|
147
|
+
url: '/provisioners/runner-instances/reconcile',
|
|
148
|
+
headers: {authorization: `Bearer ${VALID_PROVISIONER_TOKEN}`},
|
|
149
|
+
payload: {observed_provider_runner_ids: ['provisioned-runner-1']},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
expect(res.statusCode).toBe(200);
|
|
153
|
+
expect(Object.hasOwn(res.json().runners[0], 'intended_reservation_id')).toBe(false);
|
|
154
|
+
});
|
|
155
|
+
|
|
126
156
|
it('returns terminate for an observed terminal runner', async () => {
|
|
127
157
|
await createRunnerInstance({
|
|
128
158
|
providerRunnerId: 'provisioned-runner-1',
|
|
@@ -340,6 +370,7 @@ describe('POST /provisioners/runner-instances/reconcile', () => {
|
|
|
340
370
|
async function createRunnerInstance(params: {
|
|
341
371
|
providerRunnerId: string;
|
|
342
372
|
state?: 'starting' | 'running' | 'stopping' | 'stopped' | 'failed' | 'terminated';
|
|
373
|
+
intendedReservationId?: string | null;
|
|
343
374
|
reservationId?: string | null;
|
|
344
375
|
reportedAt?: Date;
|
|
345
376
|
}) {
|
|
@@ -347,6 +378,7 @@ describe('POST /provisioners/runner-instances/reconcile', () => {
|
|
|
347
378
|
workspaceId,
|
|
348
379
|
provisionerId: provisionerTokenId,
|
|
349
380
|
providerRunnerId: params.providerRunnerId,
|
|
381
|
+
intendedReservationId: params.intendedReservationId ?? null,
|
|
350
382
|
reservationId: params.reservationId ?? null,
|
|
351
383
|
state: params.state ?? 'running',
|
|
352
384
|
reportedAt: params.reportedAt ?? new Date(),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import {requireProvisionerContext} from '@shipfox/api-auth-context';
|
|
2
2
|
import {
|
|
3
|
+
RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER,
|
|
4
|
+
RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER_VALUE,
|
|
3
5
|
reconcileRunnerInstancesBodySchema,
|
|
4
6
|
reconcileRunnerInstancesResponseSchema,
|
|
5
7
|
} from '@shipfox/api-runners-dto';
|
|
@@ -25,6 +27,10 @@ export const reconcileRunnerInstancesRoute = defineRoute({
|
|
|
25
27
|
observedRunnerInstanceIds: request.body.observed_provider_runner_ids,
|
|
26
28
|
});
|
|
27
29
|
|
|
28
|
-
return toReconcileRunnerInstancesResponseDto(result
|
|
30
|
+
return toReconcileRunnerInstancesResponseDto(result, {
|
|
31
|
+
includeIntendedReservationId:
|
|
32
|
+
request.headers[RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER] ===
|
|
33
|
+
RECONCILE_RUNNER_INSTANCES_INTENDED_RESERVATION_HEADER_VALUE,
|
|
34
|
+
});
|
|
29
35
|
},
|
|
30
36
|
});
|