@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.
Files changed (149) hide show
  1. package/.turbo/turbo-build.log +4 -4
  2. package/CHANGELOG.md +34 -0
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +10 -3
  6. package/dist/config.js.map +1 -1
  7. package/dist/core/demand.d.ts +4 -2
  8. package/dist/core/demand.d.ts.map +1 -1
  9. package/dist/core/demand.js +22 -6
  10. package/dist/core/demand.js.map +1 -1
  11. package/dist/core/entities/reservation.d.ts +1 -0
  12. package/dist/core/entities/reservation.d.ts.map +1 -1
  13. package/dist/core/entities/reservation.js.map +1 -1
  14. package/dist/core/entities/runner-instance.d.ts +3 -0
  15. package/dist/core/entities/runner-instance.d.ts.map +1 -1
  16. package/dist/core/entities/runner-instance.js.map +1 -1
  17. package/dist/core/runner-activation.d.ts +4 -1
  18. package/dist/core/runner-activation.d.ts.map +1 -1
  19. package/dist/core/runner-activation.js +32 -3
  20. package/dist/core/runner-activation.js.map +1 -1
  21. package/dist/core/runner-control-sessions.d.ts +8 -4
  22. package/dist/core/runner-control-sessions.d.ts.map +1 -1
  23. package/dist/core/runner-control-sessions.js +69 -11
  24. package/dist/core/runner-control-sessions.js.map +1 -1
  25. package/dist/core/runner-instances.d.ts +1 -0
  26. package/dist/core/runner-instances.d.ts.map +1 -1
  27. package/dist/core/runner-instances.js +1 -0
  28. package/dist/core/runner-instances.js.map +1 -1
  29. package/dist/db/db.d.ts +68 -0
  30. package/dist/db/db.d.ts.map +1 -1
  31. package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
  32. package/dist/db/ephemeral-registration-tokens.js +1 -1
  33. package/dist/db/ephemeral-registration-tokens.js.map +1 -1
  34. package/dist/db/index.d.ts +1 -1
  35. package/dist/db/index.d.ts.map +1 -1
  36. package/dist/db/index.js +1 -1
  37. package/dist/db/index.js.map +1 -1
  38. package/dist/db/reservation-locks.d.ts +6 -0
  39. package/dist/db/reservation-locks.d.ts.map +1 -0
  40. package/dist/db/reservation-locks.js +15 -0
  41. package/dist/db/reservation-locks.js.map +1 -0
  42. package/dist/db/reservations.d.ts +17 -3
  43. package/dist/db/reservations.d.ts.map +1 -1
  44. package/dist/db/reservations.js +256 -45
  45. package/dist/db/reservations.js.map +1 -1
  46. package/dist/db/runner-assignments.d.ts +17 -0
  47. package/dist/db/runner-assignments.d.ts.map +1 -1
  48. package/dist/db/runner-assignments.js +103 -8
  49. package/dist/db/runner-assignments.js.map +1 -1
  50. package/dist/db/runner-instances.d.ts +5 -2
  51. package/dist/db/runner-instances.d.ts.map +1 -1
  52. package/dist/db/runner-instances.js +134 -25
  53. package/dist/db/runner-instances.js.map +1 -1
  54. package/dist/db/runner-sessions.d.ts.map +1 -1
  55. package/dist/db/runner-sessions.js +22 -15
  56. package/dist/db/runner-sessions.js.map +1 -1
  57. package/dist/db/runner-states.d.ts +2 -0
  58. package/dist/db/runner-states.d.ts.map +1 -0
  59. package/dist/db/runner-states.js +7 -0
  60. package/dist/db/runner-states.js.map +1 -0
  61. package/dist/db/schema/reservations.d.ts +18 -0
  62. package/dist/db/schema/reservations.d.ts.map +1 -1
  63. package/dist/db/schema/reservations.js +7 -1
  64. package/dist/db/schema/reservations.js.map +1 -1
  65. package/dist/db/schema/runner-instances.d.ts +18 -0
  66. package/dist/db/schema/runner-instances.d.ts.map +1 -1
  67. package/dist/db/schema/runner-instances.js +8 -0
  68. package/dist/db/schema/runner-instances.js.map +1 -1
  69. package/dist/metrics/index.d.ts +2 -2
  70. package/dist/metrics/index.d.ts.map +1 -1
  71. package/dist/metrics/index.js +1 -1
  72. package/dist/metrics/index.js.map +1 -1
  73. package/dist/metrics/instance.d.ts +24 -2
  74. package/dist/metrics/instance.d.ts.map +1 -1
  75. package/dist/metrics/instance.js +23 -0
  76. package/dist/metrics/instance.js.map +1 -1
  77. package/dist/metrics/service.d.ts.map +1 -1
  78. package/dist/metrics/service.js +20 -4
  79. package/dist/metrics/service.js.map +1 -1
  80. package/dist/presentation/dto/poll-demand.d.ts.map +1 -1
  81. package/dist/presentation/dto/poll-demand.js +3 -0
  82. package/dist/presentation/dto/poll-demand.js.map +1 -1
  83. package/dist/presentation/dto/runner-instances.d.ts +4 -1
  84. package/dist/presentation/dto/runner-instances.d.ts.map +1 -1
  85. package/dist/presentation/dto/runner-instances.js +4 -1
  86. package/dist/presentation/dto/runner-instances.js.map +1 -1
  87. package/dist/presentation/routes/assign-runner-instances.d.ts.map +1 -1
  88. package/dist/presentation/routes/assign-runner-instances.js +3 -3
  89. package/dist/presentation/routes/assign-runner-instances.js.map +1 -1
  90. package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
  91. package/dist/presentation/routes/poll-demand.js +1 -0
  92. package/dist/presentation/routes/poll-demand.js.map +1 -1
  93. package/dist/presentation/routes/reconcile-runner-instances.d.ts.map +1 -1
  94. package/dist/presentation/routes/reconcile-runner-instances.js +4 -2
  95. package/dist/presentation/routes/reconcile-runner-instances.js.map +1 -1
  96. package/dist/presentation/routes/runner-enrollment.d.ts +4 -1
  97. package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
  98. package/dist/presentation/routes/runner-enrollment.js +11 -6
  99. package/dist/presentation/routes/runner-enrollment.js.map +1 -1
  100. package/dist/tsconfig.test.tsbuildinfo +1 -1
  101. package/drizzle/0005_previous_lenny_balinger.sql +5 -0
  102. package/drizzle/meta/0005_snapshot.json +2220 -0
  103. package/drizzle/meta/_journal.json +7 -0
  104. package/package.json +4 -4
  105. package/src/config.test.ts +24 -2
  106. package/src/config.ts +16 -3
  107. package/src/core/demand.test.ts +192 -3
  108. package/src/core/demand.ts +44 -4
  109. package/src/core/entities/reservation.ts +1 -0
  110. package/src/core/entities/runner-instance.ts +4 -0
  111. package/src/core/runner-activation.ts +48 -2
  112. package/src/core/runner-control-sessions.test.ts +230 -4
  113. package/src/core/runner-control-sessions.ts +84 -10
  114. package/src/core/runner-instances.test.ts +19 -0
  115. package/src/core/runner-instances.ts +2 -0
  116. package/src/core/runner-sessions.test.ts +121 -1
  117. package/src/db/ephemeral-registration-tokens.test.ts +20 -1
  118. package/src/db/ephemeral-registration-tokens.ts +1 -0
  119. package/src/db/index.ts +1 -0
  120. package/src/db/reservation-locks.ts +22 -0
  121. package/src/db/reservations.test.ts +1110 -31
  122. package/src/db/reservations.ts +434 -105
  123. package/src/db/runner-assignments.test.ts +105 -1
  124. package/src/db/runner-assignments.ts +178 -9
  125. package/src/db/runner-instances.test.ts +496 -2
  126. package/src/db/runner-instances.ts +247 -56
  127. package/src/db/runner-sessions.ts +26 -15
  128. package/src/db/runner-states.ts +7 -0
  129. package/src/db/schema/reservations.ts +5 -1
  130. package/src/db/schema/runner-instances.ts +9 -0
  131. package/src/metrics/index.ts +10 -1
  132. package/src/metrics/instance.ts +59 -2
  133. package/src/metrics/service.test.ts +106 -0
  134. package/src/metrics/service.ts +24 -4
  135. package/src/presentation/dto/poll-demand.ts +3 -0
  136. package/src/presentation/dto/runner-instances.ts +8 -1
  137. package/src/presentation/routes/assign-runner-instances.ts +6 -2
  138. package/src/presentation/routes/late-runner-enrollment.test.ts +447 -0
  139. package/src/presentation/routes/poll-demand.test.ts +79 -6
  140. package/src/presentation/routes/poll-demand.ts +1 -0
  141. package/src/presentation/routes/reconcile-runner-instances.test.ts +34 -2
  142. package/src/presentation/routes/reconcile-runner-instances.ts +7 -1
  143. package/src/presentation/routes/runner-enrollment.test.ts +316 -3
  144. package/src/presentation/routes/runner-enrollment.ts +8 -5
  145. package/test/factories/reservation.ts +2 -0
  146. package/test/factories/runner-instance.ts +6 -0
  147. package/test/fixtures/late-runner-enrollment.ts +103 -0
  148. package/test/index.ts +5 -0
  149. package/tsconfig.build.tsbuildinfo +1 -1
@@ -10,4 +10,21 @@ export declare function assignRunnerInstancesTx(tx: Tx, params: {
10
10
  reservationId: string;
11
11
  runnerInstanceIds: string[];
12
12
  }): Promise<string[]>;
13
+ export type RunnerReservationCapacityFailureReason = 'reservation-not-found' | 'reservation-kind-mismatch' | 'reservation-expired' | 'capacity-exhausted';
14
+ export interface RunnerReservationCapacityValidation {
15
+ acceptedByReservation: Map<string, number>;
16
+ unavailableByReservation: Map<string, {
17
+ reason: RunnerReservationCapacityFailureReason;
18
+ count: number;
19
+ }>;
20
+ }
21
+ export declare function validateRunnerReservationCapacityTx(tx: Tx, params: {
22
+ provisionerId: string;
23
+ requests: readonly {
24
+ reservationId: string;
25
+ count: number;
26
+ }[];
27
+ }, options?: {
28
+ advisoryLocksHeld?: boolean;
29
+ }): Promise<RunnerReservationCapacityValidation>;
13
30
  //# sourceMappingURL=runner-assignments.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"runner-assignments.d.ts","sourceRoot":"","sources":["../../src/db/runner-assignments.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAOhC,gGAAgG;AAChG,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAEpB;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,EAAE,EACN,MAAM,EAAE;IACN,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GACA,OAAO,CAAC,MAAM,EAAE,CAAC,CAoInB"}
1
+ {"version":3,"file":"runner-assignments.d.ts","sourceRoot":"","sources":["../../src/db/runner-assignments.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAQhC,gGAAgG;AAChG,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAEpB;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,EAAE,EACN,MAAM,EAAE;IACN,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GACA,OAAO,CAAC,MAAM,EAAE,CAAC,CAkJnB;AAED,MAAM,MAAM,sCAAsC,GAC9C,uBAAuB,GACvB,2BAA2B,GAC3B,qBAAqB,GACrB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,mCAAmC;IAClD,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,wBAAwB,EAAE,GAAG,CAC3B,MAAM,EACN;QAAC,MAAM,EAAE,sCAAsC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAChE,CAAC;CACH;AAED,wBAAsB,mCAAmC,CACvD,EAAE,EAAE,EAAE,EACN,MAAM,EAAE;IACN,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,SAAS;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,EAAE,CAAC;CAC7D,EACD,OAAO,GAAE;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAM,GAC1C,OAAO,CAAC,mCAAmC,CAAC,CAmI9C"}
@@ -1,7 +1,8 @@
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 { ReservationExpiredError, ReservationNotFoundError, RunnerInstanceAlreadyAssignedError, RunnerInstanceNotAssignableError } from '#core/errors.js';
3
3
  import { db } from './db.js';
4
- import { terminalStates } from './runner-instances.js';
4
+ import { lockRunnerReservationAdvisoryKeysTx } from './reservation-locks.js';
5
+ import { terminalStates } from './runner-states.js';
5
6
  import { reservations } from './schema/reservations.js';
6
7
  import { runnerControlSessions } from './schema/runner-control-sessions.js';
7
8
  import { providerRunners } from './schema/runner-instances.js';
@@ -12,26 +13,32 @@ export async function assignRunnerInstancesTx(tx, params) {
12
13
  const runnerInstanceIds = [
13
14
  ...params.runnerInstanceIds
14
15
  ].sort();
15
- await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${params.reservationId}`}))`);
16
+ await lockRunnerReservationAdvisoryKeysTx(tx, {
17
+ provisionerId: params.provisionerId,
18
+ reservationIds: [
19
+ params.reservationId
20
+ ]
21
+ });
16
22
  const runnerRows = await tx.select({
17
23
  id: providerRunners.id,
18
24
  reservationId: providerRunners.reservationId,
19
25
  intendedReservationId: providerRunners.intendedReservationId,
20
26
  workspaceId: providerRunners.workspaceId,
27
+ assignedAt: providerRunners.assignedAt,
21
28
  providerRunnerId: providerRunners.providerRunnerId,
22
29
  labels: providerRunners.labels,
23
30
  state: providerRunners.state
24
31
  }).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.id, runnerInstanceIds))).for('update');
25
32
  // The assignment outlives its short reservation row. A retry after maintenance
26
- // cleanup is complete when every owned runner already carries the requested assignment.
27
- if (runnerInstanceIds.length > 0 && runnerRows.length === runnerInstanceIds.length && runnerRows.every((runner)=>runner.reservationId === params.reservationId)) {
33
+ // cleanup is complete when every owned runner already carries the committed assignment.
34
+ if (runnerInstanceIds.length > 0 && runnerRows.length === runnerInstanceIds.length && runnerRows.every((runner)=>runner.reservationId === params.reservationId && runner.assignedAt !== null && runner.workspaceId !== null)) {
28
35
  await tx.update(providerRunners).set({
29
36
  intendedReservationId: null,
30
37
  updatedAt: sql`now()`
31
38
  }).where(and(eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.id, runnerInstanceIds)));
32
39
  return params.runnerInstanceIds;
33
40
  }
34
- const [reservation] = await tx.select().from(reservations).where(and(eq(reservations.id, params.reservationId), eq(reservations.provisionerId, params.provisionerId))).limit(1).for('update');
41
+ const [reservation] = await tx.select().from(reservations).where(and(eq(reservations.id, params.reservationId), eq(reservations.provisionerId, params.provisionerId), eq(reservations.kind, 'launch'))).limit(1).for('update');
35
42
  if (!reservation) throw new ReservationNotFoundError(params.reservationId);
36
43
  if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);
37
44
  if (runnerRows.length !== runnerInstanceIds.length) throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');
@@ -45,10 +52,11 @@ export async function assignRunnerInstancesTx(tx, params) {
45
52
  }));
46
53
  const alreadyAssigned = runners.filter((runner)=>runner.reservationId !== null);
47
54
  if (alreadyAssigned.some((runner)=>runner.reservationId !== reservation.id)) throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');
48
- const newRunners = runners.filter((runner)=>runner.reservationId === null);
55
+ const newRunners = runners.filter((runner)=>runner.reservationId === null || runner.reservationId === reservation.id && (runner.assignedAt === null || runner.workspaceId === null));
56
+ const newRunnerIds = newRunners.map((runner)=>runner.id);
49
57
  const assignedCount = await tx.select({
50
58
  count: sql`count(*)::int`
51
- }).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), or(and(eq(providerRunners.reservationId, reservation.id), isNull(providerRunners.reservationReleasedAt)), and(eq(providerRunners.intendedReservationId, reservation.id), notInArray(providerRunners.id, runnerInstanceIds), isNull(providerRunners.reservationReleasedAt), notInArray(providerRunners.state, [
59
+ }).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), notInArray(providerRunners.id, newRunnerIds), or(and(eq(providerRunners.reservationId, reservation.id), isNull(providerRunners.reservationReleasedAt)), and(eq(providerRunners.intendedReservationId, reservation.id), isNull(providerRunners.reservationReleasedAt), notInArray(providerRunners.state, [
52
60
  ...terminalStates
53
61
  ])))));
54
62
  if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count) throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');
@@ -66,5 +74,92 @@ export async function assignRunnerInstancesTx(tx, params) {
66
74
  }
67
75
  return params.runnerInstanceIds;
68
76
  }
77
+ export async function validateRunnerReservationCapacityTx(tx, params, options = {}) {
78
+ const requestedByReservation = new Map();
79
+ for (const request of params.requests){
80
+ if (request.count <= 0) continue;
81
+ requestedByReservation.set(request.reservationId, (requestedByReservation.get(request.reservationId) ?? 0) + request.count);
82
+ }
83
+ const reservationIds = [
84
+ ...requestedByReservation.keys()
85
+ ].sort();
86
+ if (reservationIds.length === 0) return {
87
+ acceptedByReservation: new Map(),
88
+ unavailableByReservation: new Map()
89
+ };
90
+ if (!options.advisoryLocksHeld) await lockRunnerReservationAdvisoryKeysTx(tx, {
91
+ provisionerId: params.provisionerId,
92
+ reservationIds
93
+ });
94
+ const reservationRows = await tx.select({
95
+ id: reservations.id,
96
+ count: reservations.count,
97
+ expiresAt: reservations.expiresAt,
98
+ isExpired: sql`${reservations.expiresAt} <= now()`
99
+ }).from(reservations).where(and(eq(reservations.provisionerId, params.provisionerId), inArray(reservations.id, reservationIds), eq(reservations.kind, 'launch'))).for('update');
100
+ const nonLaunchReservationRows = await tx.select({
101
+ id: reservations.id
102
+ }).from(reservations).where(and(eq(reservations.provisionerId, params.provisionerId), inArray(reservations.id, reservationIds), ne(reservations.kind, 'launch'))).for('update');
103
+ const reservationsById = new Map(reservationRows.map((reservation)=>[
104
+ reservation.id,
105
+ reservation
106
+ ]));
107
+ const nonLaunchReservationIds = new Set(nonLaunchReservationRows.map((reservation)=>reservation.id));
108
+ const requestedReservationIds = new Set(reservationIds);
109
+ const usedRunnerIdsByReservation = new Map();
110
+ const usedRunnerRows = await tx.select({
111
+ id: providerRunners.id,
112
+ reservationId: providerRunners.reservationId,
113
+ intendedReservationId: providerRunners.intendedReservationId,
114
+ state: providerRunners.state
115
+ }).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), isNull(providerRunners.reservationReleasedAt), or(inArray(providerRunners.reservationId, reservationIds), and(inArray(providerRunners.intendedReservationId, reservationIds), notInArray(providerRunners.state, [
116
+ ...terminalStates
117
+ ])))));
118
+ for (const runner of usedRunnerRows){
119
+ if (runner.reservationId && requestedReservationIds.has(runner.reservationId)) {
120
+ const usedRunnerIds = usedRunnerIdsByReservation.get(runner.reservationId) ?? new Set();
121
+ usedRunnerIds.add(runner.id);
122
+ usedRunnerIdsByReservation.set(runner.reservationId, usedRunnerIds);
123
+ }
124
+ if (runner.intendedReservationId && requestedReservationIds.has(runner.intendedReservationId) && !terminalStates.includes(runner.state)) {
125
+ const usedRunnerIds = usedRunnerIdsByReservation.get(runner.intendedReservationId) ?? new Set();
126
+ usedRunnerIds.add(runner.id);
127
+ usedRunnerIdsByReservation.set(runner.intendedReservationId, usedRunnerIds);
128
+ }
129
+ }
130
+ const acceptedByReservation = new Map();
131
+ const unavailableByReservation = new Map();
132
+ for (const reservationId of reservationIds){
133
+ const requested = requestedByReservation.get(reservationId) ?? 0;
134
+ const reservation = reservationsById.get(reservationId);
135
+ if (!reservation) {
136
+ unavailableByReservation.set(reservationId, {
137
+ reason: nonLaunchReservationIds.has(reservationId) ? 'reservation-kind-mismatch' : 'reservation-not-found',
138
+ count: requested
139
+ });
140
+ continue;
141
+ }
142
+ if (reservation.isExpired) {
143
+ unavailableByReservation.set(reservationId, {
144
+ reason: 'reservation-expired',
145
+ count: requested
146
+ });
147
+ continue;
148
+ }
149
+ const used = usedRunnerIdsByReservation.get(reservationId)?.size ?? 0;
150
+ const accepted = Math.min(requested, Math.max(0, reservation.count - used));
151
+ acceptedByReservation.set(reservationId, accepted);
152
+ if (accepted < requested) {
153
+ unavailableByReservation.set(reservationId, {
154
+ reason: 'capacity-exhausted',
155
+ count: requested - accepted
156
+ });
157
+ }
158
+ }
159
+ return {
160
+ acceptedByReservation,
161
+ unavailableByReservation
162
+ };
163
+ }
69
164
 
70
165
  //# sourceMappingURL=runner-assignments.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/db/runner-assignments.ts"],"sourcesContent":["import {and, eq, inArray, isNull, notInArray, or, sql} from 'drizzle-orm';\nimport {\n ReservationExpiredError,\n ReservationNotFoundError,\n RunnerInstanceAlreadyAssignedError,\n RunnerInstanceNotAssignableError,\n} from '#core/errors.js';\nimport type {Tx} from './db.js';\nimport {db} from './db.js';\nimport {terminalStates} from './runner-instances.js';\nimport {reservations} from './schema/reservations.js';\nimport {runnerControlSessions} from './schema/runner-control-sessions.js';\nimport {providerRunners} from './schema/runner-instances.js';\n\n/** Atomically consumes reservation units by writing the immutable assignment on each runner. */\nexport async function assignRunnerInstances(params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n}): Promise<string[]> {\n return await db().transaction(async (tx) => assignRunnerInstancesTx(tx, params));\n}\n\nexport async function assignRunnerInstancesTx(\n tx: Tx,\n params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n },\n): Promise<string[]> {\n const runnerInstanceIds = [...params.runnerInstanceIds].sort();\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${params.reservationId}`}))`,\n );\n const runnerRows = await tx\n .select({\n id: providerRunners.id,\n reservationId: providerRunners.reservationId,\n intendedReservationId: providerRunners.intendedReservationId,\n workspaceId: providerRunners.workspaceId,\n providerRunnerId: providerRunners.providerRunnerId,\n labels: providerRunners.labels,\n state: providerRunners.state,\n })\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n )\n .for('update');\n\n // The assignment outlives its short reservation row. A retry after maintenance\n // cleanup is complete when every owned runner already carries the requested assignment.\n if (\n runnerInstanceIds.length > 0 &&\n runnerRows.length === runnerInstanceIds.length &&\n runnerRows.every((runner) => runner.reservationId === params.reservationId)\n ) {\n await tx\n .update(providerRunners)\n .set({intendedReservationId: null, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n );\n return params.runnerInstanceIds;\n }\n\n const [reservation] = await tx\n .select()\n .from(reservations)\n .where(\n and(\n eq(reservations.id, params.reservationId),\n eq(reservations.provisionerId, params.provisionerId),\n ),\n )\n .limit(1)\n .for('update');\n if (!reservation) throw new ReservationNotFoundError(params.reservationId);\n if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);\n if (runnerRows.length !== runnerInstanceIds.length)\n throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');\n\n const activeControlSessions = await tx\n .select({runnerInstanceId: runnerControlSessions.runnerInstanceId})\n .from(runnerControlSessions)\n .where(\n and(\n inArray(\n runnerControlSessions.runnerInstanceId,\n runnerRows.map((runner) => runner.id),\n ),\n isNull(runnerControlSessions.closedAt),\n ),\n );\n const runnerInstanceIdsWithControlSession = new Set(\n activeControlSessions.map((session) => session.runnerInstanceId),\n );\n const runners = runnerRows.map((runner) => ({\n ...runner,\n controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,\n }));\n\n const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);\n if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))\n throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');\n const newRunners = runners.filter((runner) => runner.reservationId === null);\n const assignedCount = await tx\n .select({count: sql<number>`count(*)::int`})\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n or(\n and(\n eq(providerRunners.reservationId, reservation.id),\n isNull(providerRunners.reservationReleasedAt),\n ),\n and(\n eq(providerRunners.intendedReservationId, reservation.id),\n notInArray(providerRunners.id, runnerInstanceIds),\n isNull(providerRunners.reservationReleasedAt),\n notInArray(providerRunners.state, [...terminalStates]),\n ),\n ),\n ),\n );\n if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)\n throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');\n for (const runner of newRunners) {\n if (\n runner.state !== 'running' ||\n !runner.providerRunnerId ||\n !runner.controlSessionId ||\n !reservation.requiredLabels.every((label) => runner.labels.includes(label))\n )\n throw new RunnerInstanceNotAssignableError(runner.id);\n }\n if (newRunners.length > 0) {\n await tx\n .update(providerRunners)\n .set({\n workspaceId: reservation.workspaceId,\n reservationId: reservation.id,\n intendedReservationId: null,\n assignedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n inArray(\n providerRunners.id,\n newRunners.map((runner) => runner.id),\n ),\n );\n }\n return params.runnerInstanceIds;\n}\n"],"names":["and","eq","inArray","isNull","notInArray","or","sql","ReservationExpiredError","ReservationNotFoundError","RunnerInstanceAlreadyAssignedError","RunnerInstanceNotAssignableError","db","terminalStates","reservations","runnerControlSessions","providerRunners","assignRunnerInstances","params","transaction","tx","assignRunnerInstancesTx","runnerInstanceIds","sort","execute","provisionerId","reservationId","runnerRows","select","id","intendedReservationId","workspaceId","providerRunnerId","labels","state","from","where","for","length","every","runner","update","set","updatedAt","reservation","limit","expiresAt","Date","activeControlSessions","runnerInstanceId","map","closedAt","runnerInstanceIdsWithControlSession","Set","session","runners","controlSessionId","has","alreadyAssigned","filter","some","newRunners","assignedCount","count","reservationReleasedAt","requiredLabels","label","includes","assignedAt"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/db/runner-assignments.ts"],"sourcesContent":["import {and, eq, inArray, isNull, ne, notInArray, or, sql} from 'drizzle-orm';\nimport {\n ReservationExpiredError,\n ReservationNotFoundError,\n RunnerInstanceAlreadyAssignedError,\n RunnerInstanceNotAssignableError,\n} from '#core/errors.js';\nimport type {Tx} from './db.js';\nimport {db} from './db.js';\nimport {lockRunnerReservationAdvisoryKeysTx} from './reservation-locks.js';\nimport {terminalStates} from './runner-states.js';\nimport {reservations} from './schema/reservations.js';\nimport {runnerControlSessions} from './schema/runner-control-sessions.js';\nimport {providerRunners} from './schema/runner-instances.js';\n\n/** Atomically consumes reservation units by writing the immutable assignment on each runner. */\nexport async function assignRunnerInstances(params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n}): Promise<string[]> {\n return await db().transaction(async (tx) => assignRunnerInstancesTx(tx, params));\n}\n\nexport async function assignRunnerInstancesTx(\n tx: Tx,\n params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n },\n): Promise<string[]> {\n const runnerInstanceIds = [...params.runnerInstanceIds].sort();\n await lockRunnerReservationAdvisoryKeysTx(tx, {\n provisionerId: params.provisionerId,\n reservationIds: [params.reservationId],\n });\n const runnerRows = await tx\n .select({\n id: providerRunners.id,\n reservationId: providerRunners.reservationId,\n intendedReservationId: providerRunners.intendedReservationId,\n workspaceId: providerRunners.workspaceId,\n assignedAt: providerRunners.assignedAt,\n providerRunnerId: providerRunners.providerRunnerId,\n labels: providerRunners.labels,\n state: providerRunners.state,\n })\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n )\n .for('update');\n\n // The assignment outlives its short reservation row. A retry after maintenance\n // cleanup is complete when every owned runner already carries the committed assignment.\n if (\n runnerInstanceIds.length > 0 &&\n runnerRows.length === runnerInstanceIds.length &&\n runnerRows.every(\n (runner) =>\n runner.reservationId === params.reservationId &&\n runner.assignedAt !== null &&\n runner.workspaceId !== null,\n )\n ) {\n await tx\n .update(providerRunners)\n .set({intendedReservationId: null, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n );\n return params.runnerInstanceIds;\n }\n\n const [reservation] = await tx\n .select()\n .from(reservations)\n .where(\n and(\n eq(reservations.id, params.reservationId),\n eq(reservations.provisionerId, params.provisionerId),\n eq(reservations.kind, 'launch'),\n ),\n )\n .limit(1)\n .for('update');\n if (!reservation) throw new ReservationNotFoundError(params.reservationId);\n if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);\n if (runnerRows.length !== runnerInstanceIds.length)\n throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');\n\n const activeControlSessions = await tx\n .select({runnerInstanceId: runnerControlSessions.runnerInstanceId})\n .from(runnerControlSessions)\n .where(\n and(\n inArray(\n runnerControlSessions.runnerInstanceId,\n runnerRows.map((runner) => runner.id),\n ),\n isNull(runnerControlSessions.closedAt),\n ),\n );\n const runnerInstanceIdsWithControlSession = new Set(\n activeControlSessions.map((session) => session.runnerInstanceId),\n );\n const runners = runnerRows.map((runner) => ({\n ...runner,\n controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,\n }));\n\n const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);\n if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))\n throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');\n const newRunners = runners.filter(\n (runner) =>\n runner.reservationId === null ||\n (runner.reservationId === reservation.id &&\n (runner.assignedAt === null || runner.workspaceId === null)),\n );\n const newRunnerIds = newRunners.map((runner) => runner.id);\n const assignedCount = await tx\n .select({count: sql<number>`count(*)::int`})\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n notInArray(providerRunners.id, newRunnerIds),\n or(\n and(\n eq(providerRunners.reservationId, reservation.id),\n isNull(providerRunners.reservationReleasedAt),\n ),\n and(\n eq(providerRunners.intendedReservationId, reservation.id),\n isNull(providerRunners.reservationReleasedAt),\n notInArray(providerRunners.state, [...terminalStates]),\n ),\n ),\n ),\n );\n if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)\n throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');\n for (const runner of newRunners) {\n if (\n runner.state !== 'running' ||\n !runner.providerRunnerId ||\n !runner.controlSessionId ||\n !reservation.requiredLabels.every((label) => runner.labels.includes(label))\n )\n throw new RunnerInstanceNotAssignableError(runner.id);\n }\n if (newRunners.length > 0) {\n await tx\n .update(providerRunners)\n .set({\n workspaceId: reservation.workspaceId,\n reservationId: reservation.id,\n intendedReservationId: null,\n assignedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n inArray(\n providerRunners.id,\n newRunners.map((runner) => runner.id),\n ),\n );\n }\n return params.runnerInstanceIds;\n}\n\nexport type RunnerReservationCapacityFailureReason =\n | 'reservation-not-found'\n | 'reservation-kind-mismatch'\n | 'reservation-expired'\n | 'capacity-exhausted';\n\nexport interface RunnerReservationCapacityValidation {\n acceptedByReservation: Map<string, number>;\n unavailableByReservation: Map<\n string,\n {reason: RunnerReservationCapacityFailureReason; count: number}\n >;\n}\n\nexport async function validateRunnerReservationCapacityTx(\n tx: Tx,\n params: {\n provisionerId: string;\n requests: readonly {reservationId: string; count: number}[];\n },\n options: {advisoryLocksHeld?: boolean} = {},\n): Promise<RunnerReservationCapacityValidation> {\n const requestedByReservation = new Map<string, number>();\n for (const request of params.requests) {\n if (request.count <= 0) continue;\n requestedByReservation.set(\n request.reservationId,\n (requestedByReservation.get(request.reservationId) ?? 0) + request.count,\n );\n }\n const reservationIds = [...requestedByReservation.keys()].sort();\n if (reservationIds.length === 0)\n return {acceptedByReservation: new Map(), unavailableByReservation: new Map()};\n\n if (!options.advisoryLocksHeld)\n await lockRunnerReservationAdvisoryKeysTx(tx, {\n provisionerId: params.provisionerId,\n reservationIds,\n });\n\n const reservationRows = await tx\n .select({\n id: reservations.id,\n count: reservations.count,\n expiresAt: reservations.expiresAt,\n isExpired: sql<boolean>`${reservations.expiresAt} <= now()`,\n })\n .from(reservations)\n .where(\n and(\n eq(reservations.provisionerId, params.provisionerId),\n inArray(reservations.id, reservationIds),\n eq(reservations.kind, 'launch'),\n ),\n )\n .for('update');\n const nonLaunchReservationRows = await tx\n .select({id: reservations.id})\n .from(reservations)\n .where(\n and(\n eq(reservations.provisionerId, params.provisionerId),\n inArray(reservations.id, reservationIds),\n ne(reservations.kind, 'launch'),\n ),\n )\n .for('update');\n const reservationsById = new Map(\n reservationRows.map((reservation) => [reservation.id, reservation]),\n );\n const nonLaunchReservationIds = new Set(\n nonLaunchReservationRows.map((reservation) => reservation.id),\n );\n const requestedReservationIds = new Set(reservationIds);\n const usedRunnerIdsByReservation = new Map<string, Set<string>>();\n const usedRunnerRows = await tx\n .select({\n id: providerRunners.id,\n reservationId: providerRunners.reservationId,\n intendedReservationId: providerRunners.intendedReservationId,\n state: providerRunners.state,\n })\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n isNull(providerRunners.reservationReleasedAt),\n or(\n inArray(providerRunners.reservationId, reservationIds),\n and(\n inArray(providerRunners.intendedReservationId, reservationIds),\n notInArray(providerRunners.state, [...terminalStates]),\n ),\n ),\n ),\n );\n for (const runner of usedRunnerRows) {\n if (runner.reservationId && requestedReservationIds.has(runner.reservationId)) {\n const usedRunnerIds =\n usedRunnerIdsByReservation.get(runner.reservationId) ?? new Set<string>();\n usedRunnerIds.add(runner.id);\n usedRunnerIdsByReservation.set(runner.reservationId, usedRunnerIds);\n }\n if (\n runner.intendedReservationId &&\n requestedReservationIds.has(runner.intendedReservationId) &&\n !terminalStates.includes(runner.state as (typeof terminalStates)[number])\n ) {\n const usedRunnerIds =\n usedRunnerIdsByReservation.get(runner.intendedReservationId) ?? new Set<string>();\n usedRunnerIds.add(runner.id);\n usedRunnerIdsByReservation.set(runner.intendedReservationId, usedRunnerIds);\n }\n }\n\n const acceptedByReservation = new Map<string, number>();\n const unavailableByReservation = new Map<\n string,\n {reason: RunnerReservationCapacityFailureReason; count: number}\n >();\n for (const reservationId of reservationIds) {\n const requested = requestedByReservation.get(reservationId) ?? 0;\n const reservation = reservationsById.get(reservationId);\n if (!reservation) {\n unavailableByReservation.set(reservationId, {\n reason: nonLaunchReservationIds.has(reservationId)\n ? 'reservation-kind-mismatch'\n : 'reservation-not-found',\n count: requested,\n });\n continue;\n }\n if (reservation.isExpired) {\n unavailableByReservation.set(reservationId, {\n reason: 'reservation-expired',\n count: requested,\n });\n continue;\n }\n\n const used = usedRunnerIdsByReservation.get(reservationId)?.size ?? 0;\n const accepted = Math.min(requested, Math.max(0, reservation.count - used));\n acceptedByReservation.set(reservationId, accepted);\n if (accepted < requested) {\n unavailableByReservation.set(reservationId, {\n reason: 'capacity-exhausted',\n count: requested - accepted,\n });\n }\n }\n\n return {acceptedByReservation, unavailableByReservation};\n}\n"],"names":["and","eq","inArray","isNull","ne","notInArray","or","sql","ReservationExpiredError","ReservationNotFoundError","RunnerInstanceAlreadyAssignedError","RunnerInstanceNotAssignableError","db","lockRunnerReservationAdvisoryKeysTx","terminalStates","reservations","runnerControlSessions","providerRunners","assignRunnerInstances","params","transaction","tx","assignRunnerInstancesTx","runnerInstanceIds","sort","provisionerId","reservationIds","reservationId","runnerRows","select","id","intendedReservationId","workspaceId","assignedAt","providerRunnerId","labels","state","from","where","for","length","every","runner","update","set","updatedAt","reservation","kind","limit","expiresAt","Date","activeControlSessions","runnerInstanceId","map","closedAt","runnerInstanceIdsWithControlSession","Set","session","runners","controlSessionId","has","alreadyAssigned","filter","some","newRunners","newRunnerIds","assignedCount","count","reservationReleasedAt","requiredLabels","label","includes","validateRunnerReservationCapacityTx","options","requestedByReservation","Map","request","requests","get","keys","acceptedByReservation","unavailableByReservation","advisoryLocksHeld","reservationRows","isExpired","nonLaunchReservationRows","reservationsById","nonLaunchReservationIds","requestedReservationIds","usedRunnerIdsByReservation","usedRunnerRows","usedRunnerIds","add","requested","reason","used","size","accepted","Math","min","max"],"mappings":"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"}
@@ -1,13 +1,13 @@
1
1
  import type { RunnerInstance, RunnerInstanceState } from '#core/entities/runner-instance.js';
2
2
  import type { Tx } from './db.js';
3
3
  import { type RunnerInstanceBoundJobExecution } from './job-executions.js';
4
- export declare const terminalStates: readonly ["stopped", "failed", "terminated"];
5
4
  export declare const activeStates: readonly ["starting", "running", "stopping"];
6
5
  export declare const divergenceCountStates: readonly ["starting", "running"];
7
- export type RunnerInstanceTerminateIntentReason = 'job-cancelled';
6
+ export type RunnerInstanceTerminateIntentReason = 'activation-timeout' | 'job-cancelled';
8
7
  export interface RunnerInstanceTerminateIntent {
9
8
  providerRunnerId: string;
10
9
  reason: RunnerInstanceTerminateIntentReason;
10
+ activationTimeoutRetry?: boolean;
11
11
  }
12
12
  export interface ActiveRunnerInstanceTemplateCount {
13
13
  templateKey: string;
@@ -61,6 +61,9 @@ export declare function listActiveRunnerInstanceCountsByTemplateTx(tx: Tx, param
61
61
  workspaceId: string;
62
62
  provisionerId: string;
63
63
  }): Promise<ActiveRunnerInstanceTemplateCount[]>;
64
+ export declare function countStaleEnrolledRunnerInstances(params: {
65
+ graceSeconds: number;
66
+ }): Promise<number>;
64
67
  export declare function listActiveRunnerInstances(params: {
65
68
  workspaceId: string;
66
69
  windowSeconds: number;
@@ -1 +1 @@
1
- {"version":3,"file":"runner-instances.d.ts","sourceRoot":"","sources":["../../src/db/runner-instances.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"runner-instances.d.ts","sourceRoot":"","sources":["../../src/db/runner-instances.ts"],"names":[],"mappings":"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"}
@@ -1,21 +1,22 @@
1
1
  import { logger } from '@shipfox/node-opentelemetry';
2
2
  import { and, asc, desc, eq, exists, gt, inArray, isNotNull, isNull, lt, notExists, notInArray, or, sql } from 'drizzle-orm';
3
3
  import { alias } from 'drizzle-orm/pg-core';
4
+ import { config } from '#config.js';
4
5
  import { sanitizeRunnerLabels } from '#core/runner-labels.js';
6
+ import { recordRunnerReservationCapacityFailure } from '#metrics/index.js';
5
7
  import { db } from './db.js';
6
8
  import { listRunningJobExecutionsByRunnerInstanceTx } from './job-executions.js';
9
+ import { lockRunnerReservationAdvisoryKeysTx } from './reservation-locks.js';
7
10
  import { releaseReservationUnits } from './reservations.js';
11
+ import { validateRunnerReservationCapacityTx } from './runner-assignments.js';
12
+ import { terminalStates } from './runner-states.js';
8
13
  import { ephemeralRegistrationTokens } from './schema/ephemeral-registration-tokens.js';
9
14
  import { provisionerTokens } from './schema/provisioner-tokens.js';
10
15
  import { reservations } from './schema/reservations.js';
16
+ import { runnerControlSessions } from './schema/runner-control-sessions.js';
11
17
  import { providerRunners, toRunnerInstance } from './schema/runner-instances.js';
12
18
  import { runnerSessions } from './schema/runner-sessions.js';
13
19
  import { runningJobExecutions } from './schema/running-job-executions.js';
14
- export const terminalStates = [
15
- 'stopped',
16
- 'failed',
17
- 'terminated'
18
- ];
19
20
  export const activeStates = [
20
21
  'starting',
21
22
  'running',
@@ -50,8 +51,9 @@ export async function reportRunnerInstances(params) {
50
51
  await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId ?? params.provisionerId}))`);
51
52
  }
52
53
  const events = await hydrateRunnerSessionIdsFromConsumedTokens(tx, params, aggregatedEvents);
53
- const terminateIntentsHonored = await listTerminateIntentsHonoredByTerminatedReportsTx(tx, params, events);
54
- const values = events.map((event)=>({
54
+ const reservationSafeEvents = await guardReportedReservationIdsTx(tx, params, events);
55
+ const terminateIntentsHonored = await listTerminateIntentsHonoredByTerminatedReportsTx(tx, params, reservationSafeEvents);
56
+ const values = reservationSafeEvents.map((event)=>({
55
57
  workspaceId: params.workspaceId,
56
58
  provisionerId: params.provisionerId,
57
59
  providerRunnerId: event.providerRunnerId,
@@ -80,7 +82,7 @@ export async function reportRunnerInstances(params) {
80
82
  ],
81
83
  targetWhere: isNotNull(providerRunners.providerRunnerId),
82
84
  set: {
83
- reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(excluded.reservation_id, ${providerRunners.reservationId}) ELSE ${providerRunners.reservationId} END`,
85
+ reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(${providerRunners.reservationId}, excluded.reservation_id) ELSE ${providerRunners.reservationId} END`,
84
86
  templateKey: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(excluded.template_key, ${providerRunners.templateKey}) ELSE ${providerRunners.templateKey} END`,
85
87
  labels: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.labels ELSE ${providerRunners.labels} END`,
86
88
  state: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.state ELSE ${providerRunners.state} END`,
@@ -100,14 +102,92 @@ export async function reportRunnerInstances(params) {
100
102
  OR ${providerRunnerMilestoneUpdateCondition()}
101
103
  `
102
104
  });
103
- const reservationsReleased = hasTerminalEvent ? await releaseTerminalRunnerInstanceReservations(tx, params, events) : 0;
105
+ const reservationsReleased = hasTerminalEvent ? await releaseTerminalRunnerInstanceReservations(tx, params, reservationSafeEvents) : 0;
104
106
  return {
105
- accepted: events.length,
107
+ accepted: reservationSafeEvents.length,
106
108
  reservationsReleased,
107
109
  terminateIntentsHonored
108
110
  };
109
111
  });
110
112
  }
113
+ async function guardReportedReservationIdsTx(tx, params, events) {
114
+ const reservationIds = [
115
+ ...new Set(events.flatMap((event)=>event.reservationId ? [
116
+ event.reservationId
117
+ ] : []))
118
+ ].sort();
119
+ if (reservationIds.length === 0) return events;
120
+ await lockRunnerReservationAdvisoryKeysTx(tx, {
121
+ provisionerId: params.provisionerId,
122
+ reservationIds
123
+ });
124
+ const providerRunnerIds = events.map((event)=>event.providerRunnerId);
125
+ const existingRows = await tx.select({
126
+ providerRunnerId: providerRunners.providerRunnerId,
127
+ reservationId: providerRunners.reservationId,
128
+ intendedReservationId: providerRunners.intendedReservationId,
129
+ reservationReleasedAt: providerRunners.reservationReleasedAt
130
+ }).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.providerRunnerId, providerRunnerIds))).for('update');
131
+ const existingByProviderRunnerId = new Map(existingRows.flatMap((row)=>row.providerRunnerId ? [
132
+ [
133
+ row.providerRunnerId,
134
+ row
135
+ ]
136
+ ] : []));
137
+ const candidateReservationByProviderRunnerId = new Map();
138
+ for (const event of events){
139
+ if (!event.reservationId) continue;
140
+ if (isTerminalState(event.state)) continue;
141
+ const existing = existingByProviderRunnerId.get(event.providerRunnerId);
142
+ if (existing?.reservationId || existing?.intendedReservationId === event.reservationId) continue;
143
+ if (existing?.intendedReservationId || existing?.reservationReleasedAt) continue;
144
+ candidateReservationByProviderRunnerId.set(event.providerRunnerId, event.reservationId);
145
+ }
146
+ const validation = await validateRunnerReservationCapacityTx(tx, {
147
+ provisionerId: params.provisionerId,
148
+ requests: [
149
+ ...candidateReservationByProviderRunnerId.values()
150
+ ].map((reservationId)=>({
151
+ reservationId,
152
+ count: 1
153
+ }))
154
+ }, {
155
+ advisoryLocksHeld: true
156
+ });
157
+ for (const { reason, count } of validation.unavailableByReservation.values())recordRunnerReservationCapacityFailure(reason, count);
158
+ const remainingAcceptedByReservation = new Map(validation.acceptedByReservation);
159
+ return events.map((event)=>{
160
+ const existing = existingByProviderRunnerId.get(event.providerRunnerId);
161
+ const reservationId = candidateReservationByProviderRunnerId.get(event.providerRunnerId);
162
+ if (!reservationId) {
163
+ if (event.reservationId && isTerminalState(event.state)) {
164
+ // Terminal cleanup releases the IDs stored on the projection row. An arbitrary
165
+ // terminal report must not consume reservation capacity during that cleanup.
166
+ const matchesExistingReservation = existing !== undefined && (existing.reservationId === event.reservationId || existing.intendedReservationId === event.reservationId);
167
+ if (!matchesExistingReservation) return {
168
+ ...event,
169
+ reservationId: null
170
+ };
171
+ }
172
+ if (event.reservationId && existing?.intendedReservationId && existing.intendedReservationId !== event.reservationId) return {
173
+ ...event,
174
+ reservationId: null
175
+ };
176
+ if (event.reservationId && existing?.reservationReleasedAt) return {
177
+ ...event,
178
+ reservationId: null
179
+ };
180
+ return event;
181
+ }
182
+ const remaining = remainingAcceptedByReservation.get(reservationId) ?? 0;
183
+ if (remaining === 0) return {
184
+ ...event,
185
+ reservationId: null
186
+ };
187
+ remainingAcceptedByReservation.set(reservationId, remaining - 1);
188
+ return event;
189
+ });
190
+ }
111
191
  export async function listActiveRunnerInstanceCountsByTemplateTx(tx, params) {
112
192
  const rows = await tx.select({
113
193
  templateKey: providerRunners.templateKey,
@@ -122,6 +202,14 @@ export async function listActiveRunnerInstanceCountsByTemplateTx(tx, params) {
122
202
  }
123
203
  ] : []);
124
204
  }
205
+ export async function countStaleEnrolledRunnerInstances(params) {
206
+ const [row] = await db().select({
207
+ count: sql`count(*)::int`
208
+ }).from(providerRunners).where(and(eq(providerRunners.state, 'running'), isNull(providerRunners.workspaceId), isNull(providerRunners.runnerSessionId), lt(providerRunners.reportedAt, staleRunnerInstanceCutoff(params.graceSeconds)), exists(db().select({
209
+ id: runnerControlSessions.id
210
+ }).from(runnerControlSessions).where(and(eq(runnerControlSessions.runnerInstanceId, providerRunners.id), isNull(runnerControlSessions.closedAt), gt(runnerControlSessions.expiresAt, sql`now()`))))));
211
+ return row?.count ?? 0;
212
+ }
125
213
  export async function listActiveRunnerInstances(params) {
126
214
  const rows = await db().select().from(providerRunners).where(and(eq(providerRunners.workspaceId, params.workspaceId), inArray(providerRunners.state, activeStates), sql`${providerRunners.updatedAt} > now() - (${params.windowSeconds} || ' seconds')::interval`)).orderBy(desc(providerRunners.updatedAt), desc(providerRunners.id)).limit(params.limit ?? 1000);
127
215
  return rows.map(toRunnerInstance);
@@ -144,21 +232,47 @@ export async function listProvisionerTerminateIntentRowsTx(tx, params) {
144
232
  returnedCount: returnedRows.length
145
233
  }, 'provisioner terminate intents truncated by poll-demand limit');
146
234
  }
147
- return returnedRows.flatMap((row)=>row.providerRunnerId ? [
148
- {
149
- ...row,
150
- providerRunnerId: row.providerRunnerId
151
- }
235
+ const activationTimeoutRunnerIds = returnedRows.flatMap((row)=>row.reason === 'activation-timeout' && row.providerRunnerId ? [
236
+ row.providerRunnerId
152
237
  ] : []);
238
+ if (activationTimeoutRunnerIds.length > 0) {
239
+ await tx.update(providerRunners).set({
240
+ reservationReleasedAt: sql`now()`,
241
+ updatedAt: sql`now()`
242
+ }).where(and(eq(providerRunners.workspaceId, params.workspaceId), eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.providerRunnerId, activationTimeoutRunnerIds), isNull(providerRunners.reservationReleasedAt)));
243
+ }
244
+ return returnedRows.flatMap((row)=>toRunnerInstanceTerminateIntent(row));
245
+ }
246
+ function toRunnerInstanceTerminateIntent(row) {
247
+ if (!row.providerRunnerId) return [];
248
+ return [
249
+ {
250
+ providerRunnerId: row.providerRunnerId,
251
+ reason: row.reason,
252
+ ...row.reason === 'activation-timeout' && row.activationTimeoutRetry ? {
253
+ activationTimeoutRetry: true
254
+ } : {}
255
+ }
256
+ ];
153
257
  }
154
258
  function provisionerTerminateIntentsQuery(tx, params) {
155
259
  const newerRunningJobExecutions = alias(runningJobExecutions, 'newer_running_jobs');
260
+ const latestCancelledJob = exists(tx.select({
261
+ id: runningJobExecutions.id
262
+ }).from(runningJobExecutions).where(and(eq(runningJobExecutions.workspaceId, params.workspaceId), eq(runningJobExecutions.provisionerId, params.provisionerId), eq(runningJobExecutions.providerRunnerId, providerRunners.providerRunnerId), isNotNull(runningJobExecutions.cancellationRequestedAt), notExists(tx.select({
263
+ id: newerRunningJobExecutions.id
264
+ }).from(newerRunningJobExecutions).where(and(eq(newerRunningJobExecutions.workspaceId, runningJobExecutions.workspaceId), eq(newerRunningJobExecutions.provisionerId, runningJobExecutions.provisionerId), eq(newerRunningJobExecutions.providerRunnerId, runningJobExecutions.providerRunnerId), or(gt(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt), and(eq(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt), gt(newerRunningJobExecutions.jobExecutionId, runningJobExecutions.jobExecutionId)))))))));
265
+ const activationTimeout = and(eq(providerRunners.launchKind, 'demand'), isNull(providerRunners.runnerSessionId), lt(providerRunners.createdAt, sql`now() - (${config.RUNNER_DEMAND_ACTIVATION_TIMEOUT_SECONDS} || ' seconds')::interval`), notExists(tx.select({
266
+ id: reservations.id
267
+ }).from(reservations).where(and(or(eq(reservations.id, providerRunners.reservationId), eq(reservations.id, providerRunners.intendedReservationId)), eq(reservations.workspaceId, params.workspaceId), eq(reservations.provisionerId, params.provisionerId), gt(reservations.expiresAt, sql`now()`)))));
156
268
  return tx.select({
157
269
  providerRunnerId: providerRunners.providerRunnerId,
158
- reason: sql`'job-cancelled'`
159
- }).from(runningJobExecutions).innerJoin(providerRunners, and(eq(providerRunners.workspaceId, runningJobExecutions.workspaceId), eq(providerRunners.provisionerId, runningJobExecutions.provisionerId), eq(providerRunners.providerRunnerId, runningJobExecutions.providerRunnerId))).where(and(eq(runningJobExecutions.workspaceId, params.workspaceId), eq(runningJobExecutions.provisionerId, params.provisionerId), isNotNull(providerRunners.providerRunnerId), isNotNull(runningJobExecutions.cancellationRequestedAt), inArray(providerRunners.state, activeStates), params.providerRunnerIds && params.providerRunnerIds.length > 0 ? inArray(providerRunners.providerRunnerId, params.providerRunnerIds) : undefined, notExists(tx.select({
160
- id: newerRunningJobExecutions.id
161
- }).from(newerRunningJobExecutions).where(and(eq(newerRunningJobExecutions.workspaceId, runningJobExecutions.workspaceId), eq(newerRunningJobExecutions.provisionerId, runningJobExecutions.provisionerId), eq(newerRunningJobExecutions.providerRunnerId, runningJobExecutions.providerRunnerId), or(gt(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt), and(eq(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt), gt(newerRunningJobExecutions.jobExecutionId, runningJobExecutions.jobExecutionId)))))))).groupBy(providerRunners.providerRunnerId);
270
+ activationTimeoutRetry: sql`${providerRunners.reservationReleasedAt} is not null`,
271
+ reason: sql`case
272
+ when ${activationTimeout} then 'activation-timeout'
273
+ else 'job-cancelled'
274
+ end`
275
+ }).from(providerRunners).where(and(eq(providerRunners.workspaceId, params.workspaceId), eq(providerRunners.provisionerId, params.provisionerId), isNotNull(providerRunners.providerRunnerId), inArray(providerRunners.state, activeStates), params.providerRunnerIds && params.providerRunnerIds.length > 0 ? inArray(providerRunners.providerRunnerId, params.providerRunnerIds) : undefined, or(latestCancelledJob, activationTimeout)));
162
276
  }
163
277
  async function listTerminateIntentsHonoredByTerminatedReportsTx(tx, params, events) {
164
278
  if (!params.workspaceId) return [];
@@ -171,12 +285,7 @@ async function listTerminateIntentsHonoredByTerminatedReportsTx(tx, params, even
171
285
  provisionerId: params.provisionerId,
172
286
  providerRunnerIds: terminatedRunnerInstanceIds
173
287
  }).orderBy(asc(providerRunners.providerRunnerId));
174
- return rows.flatMap((row)=>row.providerRunnerId ? [
175
- {
176
- ...row,
177
- providerRunnerId: row.providerRunnerId
178
- }
179
- ] : []);
288
+ return rows.flatMap((row)=>toRunnerInstanceTerminateIntent(row));
180
289
  }
181
290
  export async function reconcileRunnerInstances(params) {
182
291
  const observedRunnerInstanceIds = [