@shipfox/api-runners 12.1.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.
Files changed (129) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +35 -0
  3. package/dist/config.d.ts +3 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +24 -2
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/errors.d.ts +4 -0
  8. package/dist/core/errors.d.ts.map +1 -1
  9. package/dist/core/errors.js +6 -0
  10. package/dist/core/errors.js.map +1 -1
  11. package/dist/core/index.d.ts +1 -1
  12. package/dist/core/index.d.ts.map +1 -1
  13. package/dist/core/index.js +1 -1
  14. package/dist/core/index.js.map +1 -1
  15. package/dist/core/rate-limit.d.ts.map +1 -1
  16. package/dist/core/runner-control-sessions.d.ts.map +1 -1
  17. package/dist/core/runner-control-sessions.js +37 -11
  18. package/dist/core/runner-control-sessions.js.map +1 -1
  19. package/dist/core/runner-instances.d.ts +1 -0
  20. package/dist/core/runner-instances.d.ts.map +1 -1
  21. package/dist/core/runner-instances.js.map +1 -1
  22. package/dist/core/runner-labels.d.ts +11 -0
  23. package/dist/core/runner-labels.d.ts.map +1 -0
  24. package/dist/core/runner-labels.js +41 -0
  25. package/dist/core/runner-labels.js.map +1 -0
  26. package/dist/core/runner-sessions.d.ts.map +1 -1
  27. package/dist/core/runner-sessions.js +5 -1
  28. package/dist/core/runner-sessions.js.map +1 -1
  29. package/dist/db/db.d.ts +64 -64
  30. package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
  31. package/dist/db/ephemeral-registration-tokens.js +12 -2
  32. package/dist/db/ephemeral-registration-tokens.js.map +1 -1
  33. package/dist/db/index.d.ts +1 -1
  34. package/dist/db/index.d.ts.map +1 -1
  35. package/dist/db/index.js +1 -1
  36. package/dist/db/index.js.map +1 -1
  37. package/dist/db/reservations.d.ts.map +1 -1
  38. package/dist/db/reservations.js +36 -10
  39. package/dist/db/reservations.js.map +1 -1
  40. package/dist/db/runner-instances.d.ts +4 -0
  41. package/dist/db/runner-instances.d.ts.map +1 -1
  42. package/dist/db/runner-instances.js +16 -5
  43. package/dist/db/runner-instances.js.map +1 -1
  44. package/dist/db/runner-sessions.d.ts.map +1 -1
  45. package/dist/db/runner-sessions.js +33 -15
  46. package/dist/db/runner-sessions.js.map +1 -1
  47. package/dist/db/schema/admin-command-results.d.ts +1 -1
  48. package/dist/db/schema/ephemeral-registration-tokens.d.ts +1 -1
  49. package/dist/db/schema/manual-registration-tokens.d.ts +1 -1
  50. package/dist/db/schema/outbox.d.ts +1 -1
  51. package/dist/db/schema/pending-job-executions.d.ts +3 -3
  52. package/dist/db/schema/provisioner-capability-snapshots.d.ts +3 -3
  53. package/dist/db/schema/provisioner-tokens.d.ts +2 -2
  54. package/dist/db/schema/rate-limits.d.ts +1 -1
  55. package/dist/db/schema/reservations.d.ts +3 -3
  56. package/dist/db/schema/runner-activation-tokens.d.ts +1 -1
  57. package/dist/db/schema/runner-control-sessions.d.ts +2 -2
  58. package/dist/db/schema/runner-instances.d.ts +4 -4
  59. package/dist/db/schema/runner-instances.d.ts.map +1 -1
  60. package/dist/db/schema/runner-instances.js +1 -0
  61. package/dist/db/schema/runner-instances.js.map +1 -1
  62. package/dist/db/schema/runner-sessions.d.ts +4 -4
  63. package/dist/db/schema/running-job-executions.d.ts +5 -5
  64. package/dist/metrics/index.d.ts +2 -1
  65. package/dist/metrics/index.d.ts.map +1 -1
  66. package/dist/metrics/index.js +1 -1
  67. package/dist/metrics/index.js.map +1 -1
  68. package/dist/metrics/instance.d.ts +13 -8
  69. package/dist/metrics/instance.d.ts.map +1 -1
  70. package/dist/metrics/instance.js +8 -0
  71. package/dist/metrics/instance.js.map +1 -1
  72. package/dist/metrics/service.d.ts.map +1 -1
  73. package/dist/metrics/service.js +20 -4
  74. package/dist/metrics/service.js.map +1 -1
  75. package/dist/presentation/dto/admin-provisioner-token.d.ts +1 -1
  76. package/dist/presentation/dto/admin-provisioner-token.d.ts.map +1 -1
  77. package/dist/presentation/dto/admin-runner-instances.d.ts +2 -2
  78. package/dist/presentation/dto/admin-runner-instances.d.ts.map +1 -1
  79. package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
  80. package/dist/presentation/routes/poll-demand.js +12 -6
  81. package/dist/presentation/routes/poll-demand.js.map +1 -1
  82. package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
  83. package/dist/presentation/routes/register.d.ts.map +1 -1
  84. package/dist/presentation/routes/register.js +9 -1
  85. package/dist/presentation/routes/register.js.map +1 -1
  86. package/dist/presentation/routes/report-runner-instances.d.ts.map +1 -1
  87. package/dist/presentation/routes/report-runner-instances.js +1 -0
  88. package/dist/presentation/routes/report-runner-instances.js.map +1 -1
  89. package/dist/tsconfig.test.tsbuildinfo +1 -1
  90. package/drizzle/0004_huge_skreet.sql +1 -0
  91. package/drizzle/meta/0004_snapshot.json +2194 -0
  92. package/drizzle/meta/_journal.json +7 -0
  93. package/package.json +9 -9
  94. package/src/config.test.ts +81 -0
  95. package/src/config.ts +39 -2
  96. package/src/core/errors.ts +9 -0
  97. package/src/core/index.ts +1 -0
  98. package/src/core/runner-control-sessions.test.ts +404 -0
  99. package/src/core/runner-control-sessions.ts +51 -11
  100. package/src/core/runner-instances.ts +1 -0
  101. package/src/core/runner-labels.test.ts +53 -0
  102. package/src/core/runner-labels.ts +51 -0
  103. package/src/core/runner-sessions.test.ts +34 -1
  104. package/src/core/runner-sessions.ts +8 -1
  105. package/src/db/ephemeral-registration-tokens.ts +15 -1
  106. package/src/db/index.ts +1 -0
  107. package/src/db/reservations.test.ts +537 -0
  108. package/src/db/reservations.ts +116 -17
  109. package/src/db/runner-instances.test.ts +276 -1
  110. package/src/db/runner-instances.ts +39 -3
  111. package/src/db/runner-sessions.ts +39 -15
  112. package/src/db/schema/runner-instances.ts +3 -0
  113. package/src/metrics/index.ts +2 -0
  114. package/src/metrics/instance.ts +18 -0
  115. package/src/metrics/service.test.ts +99 -0
  116. package/src/metrics/service.ts +24 -4
  117. package/src/presentation/routes/late-runner-enrollment.test.ts +230 -0
  118. package/src/presentation/routes/poll-demand.test.ts +116 -3
  119. package/src/presentation/routes/poll-demand.ts +18 -6
  120. package/src/presentation/routes/register.test.ts +116 -2
  121. package/src/presentation/routes/register.ts +7 -0
  122. package/src/presentation/routes/report-runner-instances.test.ts +33 -0
  123. package/src/presentation/routes/report-runner-instances.ts +1 -0
  124. package/src/presentation/routes/runner-enrollment.test.ts +65 -5
  125. package/src/temporal/activities/maintenance-activities.test.ts +24 -31
  126. package/test/env.ts +1 -0
  127. package/test/fixtures/late-runner-enrollment.ts +103 -0
  128. package/test/index.ts +5 -0
  129. package/tsconfig.build.tsbuildinfo +1 -1
@@ -10,6 +10,8 @@ import {
10
10
  isNotNull,
11
11
  isNull,
12
12
  lt,
13
+ notExists,
14
+ or,
13
15
  sql,
14
16
  } from 'drizzle-orm';
15
17
  import type {Tx} from './db.js';
@@ -66,6 +68,12 @@ export interface InstallationPollDemandAndReserveParams {
66
68
  onReservations?: (reservations: ReservationGrant[]) => void;
67
69
  }
68
70
 
71
+ type DemandScope = 'installation' | 'workspace';
72
+
73
+ type PollDemandAndReserveLockedParams = PollDemandAndReserveParams & {
74
+ scope: DemandScope;
75
+ };
76
+
69
77
  interface NormalizedTemplate {
70
78
  templateKey: string;
71
79
  labels: string[];
@@ -91,7 +99,7 @@ export async function pollDemandAndReserveTx(
91
99
  params: PollDemandAndReserveParams,
92
100
  ): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
93
101
  await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId}))`);
94
- return await pollDemandAndReserveLockedTx(tx, params);
102
+ return await pollDemandAndReserveLockedTx(tx, {...params, scope: 'workspace'});
95
103
  }
96
104
 
97
105
  export async function pollInstallationDemandAndReserve(
@@ -125,6 +133,7 @@ export async function pollInstallationDemandAndReserve(
125
133
  availableSlots: template.remainingSlots,
126
134
  })),
127
135
  capabilityWindowSeconds: params.capabilityWindowSeconds,
136
+ scope: 'installation',
128
137
  });
129
138
  });
130
139
  results.push(result);
@@ -157,7 +166,7 @@ function consumeInstallationTemplateSlots(
157
166
 
158
167
  async function pollDemandAndReserveLockedTx(
159
168
  tx: Tx,
160
- params: PollDemandAndReserveParams,
169
+ params: PollDemandAndReserveLockedParams,
161
170
  ): Promise<{stats: DemandStat[]; reservations: ReservationGrant[]}> {
162
171
  let demandRows = (
163
172
  await tx
@@ -264,6 +273,7 @@ async function pollDemandAndReserveLockedTx(
264
273
  requiredLabels: demand.requiredLabels,
265
274
  count: grant,
266
275
  workspaceId: params.workspaceId,
276
+ scope: params.scope,
267
277
  });
268
278
  reservedAfterGrant += grant;
269
279
  grants.push({
@@ -295,18 +305,61 @@ async function bindIdleRunnerInstancesTx(
295
305
  workspaceId: string;
296
306
  requiredLabels: string[];
297
307
  count: number;
308
+ scope: DemandScope;
298
309
  },
299
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
+
300
356
  const idleRunners = await tx
301
357
  .select({id: providerRunners.id})
302
358
  .from(providerRunners)
303
359
  .where(
304
360
  and(
305
361
  eq(providerRunners.provisionerId, params.provisionerId),
306
- isNull(providerRunners.workspaceId),
307
- isNull(providerRunners.reservationId),
308
- isNull(providerRunners.intendedReservationId),
309
- isNull(providerRunners.runnerSessionId),
362
+ canBindRunner(),
310
363
  isNotNull(providerRunners.providerRunnerId),
311
364
  eq(providerRunners.state, 'running'),
312
365
  arrayContains(providerRunners.labels, params.requiredLabels),
@@ -331,11 +384,26 @@ async function bindIdleRunnerInstancesTx(
331
384
 
332
385
  if (idleRunners.length === 0) return;
333
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
+
334
401
  await tx
335
402
  .update(providerRunners)
336
403
  .set({
337
404
  workspaceId: params.workspaceId,
338
405
  reservationId: params.reservationId,
406
+ intendedReservationId: null,
339
407
  assignedAt: sql`now()`,
340
408
  updatedAt: sql`now()`,
341
409
  })
@@ -345,9 +413,7 @@ async function bindIdleRunnerInstancesTx(
345
413
  providerRunners.id,
346
414
  idleRunners.map((runner) => runner.id),
347
415
  ),
348
- isNull(providerRunners.workspaceId),
349
- isNull(providerRunners.reservationId),
350
- isNull(providerRunners.runnerSessionId),
416
+ canBindRunner(),
351
417
  ),
352
418
  );
353
419
  }
@@ -432,16 +498,26 @@ async function deleteReservationsWithCleanupTx(
432
498
  ): Promise<number> {
433
499
  if (ids.length === 0) return 0;
434
500
 
435
- const assignedRunners = await tx
436
- .select({id: providerRunners.id, reservationId: providerRunners.reservationId})
501
+ const affectedRunners = await tx
502
+ .select({
503
+ id: providerRunners.id,
504
+ reservationId: providerRunners.reservationId,
505
+ intendedReservationId: providerRunners.intendedReservationId,
506
+ runnerSessionId: providerRunners.runnerSessionId,
507
+ reservationReleasedAt: providerRunners.reservationReleasedAt,
508
+ })
437
509
  .from(providerRunners)
438
510
  .where(
439
- and(
440
- inArray(providerRunners.reservationId, ids),
441
- isNull(providerRunners.runnerSessionId),
442
- isNull(providerRunners.reservationReleasedAt),
511
+ or(
512
+ and(
513
+ inArray(providerRunners.reservationId, ids),
514
+ isNull(providerRunners.runnerSessionId),
515
+ isNull(providerRunners.reservationReleasedAt),
516
+ ),
517
+ inArray(providerRunners.intendedReservationId, ids),
443
518
  ),
444
519
  )
520
+ .orderBy(asc(providerRunners.id))
445
521
  .for('update');
446
522
  const reservationRows = await tx
447
523
  .select({id: reservations.id})
@@ -457,8 +533,20 @@ async function deleteReservationsWithCleanupTx(
457
533
 
458
534
  if (reservationIds.length === 0) return 0;
459
535
 
460
- const assignedRunnerIds = assignedRunners
461
- .filter((runner) => runner.reservationId && reservationIds.includes(runner.reservationId))
536
+ const assignedRunnerIds = affectedRunners
537
+ .filter(
538
+ (runner) =>
539
+ runner.reservationId &&
540
+ reservationIds.includes(runner.reservationId) &&
541
+ !runner.runnerSessionId &&
542
+ !runner.reservationReleasedAt,
543
+ )
544
+ .map((runner) => runner.id);
545
+ const intendedRunnerIds = affectedRunners
546
+ .filter(
547
+ (runner) =>
548
+ runner.intendedReservationId && reservationIds.includes(runner.intendedReservationId),
549
+ )
462
550
  .map((runner) => runner.id);
463
551
  if (assignedRunnerIds.length > 0) {
464
552
  await tx
@@ -487,6 +575,17 @@ async function deleteReservationsWithCleanupTx(
487
575
  ),
488
576
  );
489
577
  }
578
+ if (intendedRunnerIds.length > 0) {
579
+ await tx
580
+ .update(providerRunners)
581
+ .set({intendedReservationId: null, updatedAt: sql`now()`})
582
+ .where(
583
+ and(
584
+ inArray(providerRunners.id, intendedRunnerIds),
585
+ inArray(providerRunners.intendedReservationId, reservationIds),
586
+ ),
587
+ );
588
+ }
490
589
 
491
590
  const deleted = await tx
492
591
  .delete(reservations)