@shipfox/api-runners 12.2.0 → 12.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +34 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -3
- package/dist/config.js.map +1 -1
- package/dist/core/demand.d.ts +4 -2
- package/dist/core/demand.d.ts.map +1 -1
- package/dist/core/demand.js +22 -6
- package/dist/core/demand.js.map +1 -1
- package/dist/core/entities/reservation.d.ts +1 -0
- package/dist/core/entities/reservation.d.ts.map +1 -1
- package/dist/core/entities/reservation.js.map +1 -1
- package/dist/core/entities/runner-instance.d.ts +3 -0
- package/dist/core/entities/runner-instance.d.ts.map +1 -1
- package/dist/core/entities/runner-instance.js.map +1 -1
- package/dist/core/runner-activation.d.ts +4 -1
- package/dist/core/runner-activation.d.ts.map +1 -1
- package/dist/core/runner-activation.js +32 -3
- package/dist/core/runner-activation.js.map +1 -1
- package/dist/core/runner-control-sessions.d.ts +8 -4
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +69 -11
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/core/runner-instances.d.ts +1 -0
- package/dist/core/runner-instances.d.ts.map +1 -1
- package/dist/core/runner-instances.js +1 -0
- package/dist/core/runner-instances.js.map +1 -1
- package/dist/db/db.d.ts +68 -0
- package/dist/db/db.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.js +1 -1
- package/dist/db/ephemeral-registration-tokens.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/reservation-locks.d.ts +6 -0
- package/dist/db/reservation-locks.d.ts.map +1 -0
- package/dist/db/reservation-locks.js +15 -0
- package/dist/db/reservation-locks.js.map +1 -0
- package/dist/db/reservations.d.ts +17 -3
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +256 -45
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +17 -0
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +103 -8
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +5 -2
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +134 -25
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-sessions.d.ts.map +1 -1
- package/dist/db/runner-sessions.js +22 -15
- package/dist/db/runner-sessions.js.map +1 -1
- package/dist/db/runner-states.d.ts +2 -0
- package/dist/db/runner-states.d.ts.map +1 -0
- package/dist/db/runner-states.js +7 -0
- package/dist/db/runner-states.js.map +1 -0
- package/dist/db/schema/reservations.d.ts +18 -0
- package/dist/db/schema/reservations.d.ts.map +1 -1
- package/dist/db/schema/reservations.js +7 -1
- package/dist/db/schema/reservations.js.map +1 -1
- package/dist/db/schema/runner-instances.d.ts +18 -0
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +8 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/metrics/index.d.ts +2 -2
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +24 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +23 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +20 -4
- package/dist/metrics/service.js.map +1 -1
- package/dist/presentation/dto/poll-demand.d.ts.map +1 -1
- package/dist/presentation/dto/poll-demand.js +3 -0
- package/dist/presentation/dto/poll-demand.js.map +1 -1
- package/dist/presentation/dto/runner-instances.d.ts +4 -1
- package/dist/presentation/dto/runner-instances.d.ts.map +1 -1
- package/dist/presentation/dto/runner-instances.js +4 -1
- package/dist/presentation/dto/runner-instances.js.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.js +3 -3
- package/dist/presentation/routes/assign-runner-instances.js.map +1 -1
- package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.js +1 -0
- package/dist/presentation/routes/poll-demand.js.map +1 -1
- package/dist/presentation/routes/reconcile-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/reconcile-runner-instances.js +4 -2
- package/dist/presentation/routes/reconcile-runner-instances.js.map +1 -1
- package/dist/presentation/routes/runner-enrollment.d.ts +4 -1
- package/dist/presentation/routes/runner-enrollment.d.ts.map +1 -1
- package/dist/presentation/routes/runner-enrollment.js +11 -6
- package/dist/presentation/routes/runner-enrollment.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0005_previous_lenny_balinger.sql +5 -0
- package/drizzle/meta/0005_snapshot.json +2220 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +4 -4
- package/src/config.test.ts +24 -2
- package/src/config.ts +16 -3
- package/src/core/demand.test.ts +192 -3
- package/src/core/demand.ts +44 -4
- package/src/core/entities/reservation.ts +1 -0
- package/src/core/entities/runner-instance.ts +4 -0
- package/src/core/runner-activation.ts +48 -2
- package/src/core/runner-control-sessions.test.ts +230 -4
- package/src/core/runner-control-sessions.ts +84 -10
- package/src/core/runner-instances.test.ts +19 -0
- package/src/core/runner-instances.ts +2 -0
- package/src/core/runner-sessions.test.ts +121 -1
- package/src/db/ephemeral-registration-tokens.test.ts +20 -1
- package/src/db/ephemeral-registration-tokens.ts +1 -0
- package/src/db/index.ts +1 -0
- package/src/db/reservation-locks.ts +22 -0
- package/src/db/reservations.test.ts +1110 -31
- package/src/db/reservations.ts +434 -105
- package/src/db/runner-assignments.test.ts +105 -1
- package/src/db/runner-assignments.ts +178 -9
- package/src/db/runner-instances.test.ts +496 -2
- package/src/db/runner-instances.ts +247 -56
- package/src/db/runner-sessions.ts +26 -15
- package/src/db/runner-states.ts +7 -0
- package/src/db/schema/reservations.ts +5 -1
- package/src/db/schema/runner-instances.ts +9 -0
- package/src/metrics/index.ts +10 -1
- package/src/metrics/instance.ts +59 -2
- package/src/metrics/service.test.ts +106 -0
- package/src/metrics/service.ts +24 -4
- package/src/presentation/dto/poll-demand.ts +3 -0
- package/src/presentation/dto/runner-instances.ts +8 -1
- package/src/presentation/routes/assign-runner-instances.ts +6 -2
- package/src/presentation/routes/late-runner-enrollment.test.ts +447 -0
- package/src/presentation/routes/poll-demand.test.ts +79 -6
- package/src/presentation/routes/poll-demand.ts +1 -0
- package/src/presentation/routes/reconcile-runner-instances.test.ts +34 -2
- package/src/presentation/routes/reconcile-runner-instances.ts +7 -1
- package/src/presentation/routes/runner-enrollment.test.ts +316 -3
- package/src/presentation/routes/runner-enrollment.ts +8 -5
- package/test/factories/reservation.ts +2 -0
- package/test/factories/runner-instance.ts +6 -0
- package/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -17,27 +17,28 @@ import {
|
|
|
17
17
|
sql,
|
|
18
18
|
} from 'drizzle-orm';
|
|
19
19
|
import {alias} from 'drizzle-orm/pg-core';
|
|
20
|
+
import {config} from '#config.js';
|
|
20
21
|
import type {RunnerInstance, RunnerInstanceState} from '#core/entities/runner-instance.js';
|
|
21
22
|
import {sanitizeRunnerLabels} from '#core/runner-labels.js';
|
|
23
|
+
import {recordRunnerReservationCapacityFailure} from '#metrics/index.js';
|
|
22
24
|
import type {Tx} from './db.js';
|
|
23
25
|
import {db} from './db.js';
|
|
24
26
|
import {
|
|
25
27
|
listRunningJobExecutionsByRunnerInstanceTx,
|
|
26
28
|
type RunnerInstanceBoundJobExecution,
|
|
27
29
|
} from './job-executions.js';
|
|
30
|
+
import {lockRunnerReservationAdvisoryKeysTx} from './reservation-locks.js';
|
|
28
31
|
import {releaseReservationUnits} from './reservations.js';
|
|
32
|
+
import {validateRunnerReservationCapacityTx} from './runner-assignments.js';
|
|
33
|
+
import {terminalStates} from './runner-states.js';
|
|
29
34
|
import {ephemeralRegistrationTokens} from './schema/ephemeral-registration-tokens.js';
|
|
30
35
|
import {provisionerTokens} from './schema/provisioner-tokens.js';
|
|
31
36
|
import {reservations} from './schema/reservations.js';
|
|
37
|
+
import {runnerControlSessions} from './schema/runner-control-sessions.js';
|
|
32
38
|
import {providerRunners, toRunnerInstance} from './schema/runner-instances.js';
|
|
33
39
|
import {runnerSessions} from './schema/runner-sessions.js';
|
|
34
40
|
import {runningJobExecutions} from './schema/running-job-executions.js';
|
|
35
41
|
|
|
36
|
-
export const terminalStates = [
|
|
37
|
-
'stopped',
|
|
38
|
-
'failed',
|
|
39
|
-
'terminated',
|
|
40
|
-
] as const satisfies readonly RunnerInstanceState[];
|
|
41
42
|
export const activeStates = [
|
|
42
43
|
'starting',
|
|
43
44
|
'running',
|
|
@@ -48,11 +49,12 @@ export const divergenceCountStates = ['starting', 'running'] as const satisfies
|
|
|
48
49
|
'starting' | 'running'
|
|
49
50
|
>[];
|
|
50
51
|
|
|
51
|
-
export type RunnerInstanceTerminateIntentReason = 'job-cancelled';
|
|
52
|
+
export type RunnerInstanceTerminateIntentReason = 'activation-timeout' | 'job-cancelled';
|
|
52
53
|
|
|
53
54
|
export interface RunnerInstanceTerminateIntent {
|
|
54
55
|
providerRunnerId: string;
|
|
55
56
|
reason: RunnerInstanceTerminateIntentReason;
|
|
57
|
+
activationTimeoutRetry?: boolean;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export interface ActiveRunnerInstanceTemplateCount {
|
|
@@ -154,13 +156,14 @@ export async function reportRunnerInstances(params: ReportRunnerInstancesParams)
|
|
|
154
156
|
}
|
|
155
157
|
|
|
156
158
|
const events = await hydrateRunnerSessionIdsFromConsumedTokens(tx, params, aggregatedEvents);
|
|
159
|
+
const reservationSafeEvents = await guardReportedReservationIdsTx(tx, params, events);
|
|
157
160
|
const terminateIntentsHonored = await listTerminateIntentsHonoredByTerminatedReportsTx(
|
|
158
161
|
tx,
|
|
159
162
|
params,
|
|
160
|
-
|
|
163
|
+
reservationSafeEvents,
|
|
161
164
|
);
|
|
162
165
|
|
|
163
|
-
const values =
|
|
166
|
+
const values = reservationSafeEvents.map((event) => ({
|
|
164
167
|
workspaceId: params.workspaceId,
|
|
165
168
|
provisionerId: params.provisionerId,
|
|
166
169
|
providerRunnerId: event.providerRunnerId,
|
|
@@ -190,7 +193,7 @@ export async function reportRunnerInstances(params: ReportRunnerInstancesParams)
|
|
|
190
193
|
target: [providerRunners.provisionerId, providerRunners.providerRunnerId],
|
|
191
194
|
targetWhere: isNotNull(providerRunners.providerRunnerId),
|
|
192
195
|
set: {
|
|
193
|
-
reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(
|
|
196
|
+
reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(${providerRunners.reservationId}, excluded.reservation_id) ELSE ${providerRunners.reservationId} END`,
|
|
194
197
|
templateKey: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(excluded.template_key, ${providerRunners.templateKey}) ELSE ${providerRunners.templateKey} END`,
|
|
195
198
|
labels: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.labels ELSE ${providerRunners.labels} END`,
|
|
196
199
|
state: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.state ELSE ${providerRunners.state} END`,
|
|
@@ -212,10 +215,105 @@ export async function reportRunnerInstances(params: ReportRunnerInstancesParams)
|
|
|
212
215
|
});
|
|
213
216
|
|
|
214
217
|
const reservationsReleased = hasTerminalEvent
|
|
215
|
-
? await releaseTerminalRunnerInstanceReservations(tx, params,
|
|
218
|
+
? await releaseTerminalRunnerInstanceReservations(tx, params, reservationSafeEvents)
|
|
216
219
|
: 0;
|
|
217
220
|
|
|
218
|
-
return {
|
|
221
|
+
return {
|
|
222
|
+
accepted: reservationSafeEvents.length,
|
|
223
|
+
reservationsReleased,
|
|
224
|
+
terminateIntentsHonored,
|
|
225
|
+
};
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function guardReportedReservationIdsTx(
|
|
230
|
+
tx: Tx,
|
|
231
|
+
params: ReportRunnerInstancesParams,
|
|
232
|
+
events: RunnerInstanceReportRow[],
|
|
233
|
+
): Promise<RunnerInstanceReportRow[]> {
|
|
234
|
+
const reservationIds = [
|
|
235
|
+
...new Set(events.flatMap((event) => (event.reservationId ? [event.reservationId] : []))),
|
|
236
|
+
].sort();
|
|
237
|
+
if (reservationIds.length === 0) return events;
|
|
238
|
+
|
|
239
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
240
|
+
provisionerId: params.provisionerId,
|
|
241
|
+
reservationIds,
|
|
242
|
+
});
|
|
243
|
+
const providerRunnerIds = events.map((event) => event.providerRunnerId);
|
|
244
|
+
const existingRows = await tx
|
|
245
|
+
.select({
|
|
246
|
+
providerRunnerId: providerRunners.providerRunnerId,
|
|
247
|
+
reservationId: providerRunners.reservationId,
|
|
248
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
249
|
+
reservationReleasedAt: providerRunners.reservationReleasedAt,
|
|
250
|
+
})
|
|
251
|
+
.from(providerRunners)
|
|
252
|
+
.where(
|
|
253
|
+
and(
|
|
254
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
255
|
+
inArray(providerRunners.providerRunnerId, providerRunnerIds),
|
|
256
|
+
),
|
|
257
|
+
)
|
|
258
|
+
.for('update');
|
|
259
|
+
const existingByProviderRunnerId = new Map(
|
|
260
|
+
existingRows.flatMap((row) =>
|
|
261
|
+
row.providerRunnerId ? [[row.providerRunnerId, row] as const] : [],
|
|
262
|
+
),
|
|
263
|
+
);
|
|
264
|
+
const candidateReservationByProviderRunnerId = new Map<string, string>();
|
|
265
|
+
for (const event of events) {
|
|
266
|
+
if (!event.reservationId) continue;
|
|
267
|
+
if (isTerminalState(event.state)) continue;
|
|
268
|
+
const existing = existingByProviderRunnerId.get(event.providerRunnerId);
|
|
269
|
+
if (existing?.reservationId || existing?.intendedReservationId === event.reservationId)
|
|
270
|
+
continue;
|
|
271
|
+
if (existing?.intendedReservationId || existing?.reservationReleasedAt) continue;
|
|
272
|
+
candidateReservationByProviderRunnerId.set(event.providerRunnerId, event.reservationId);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const validation = await validateRunnerReservationCapacityTx(
|
|
276
|
+
tx,
|
|
277
|
+
{
|
|
278
|
+
provisionerId: params.provisionerId,
|
|
279
|
+
requests: [...candidateReservationByProviderRunnerId.values()].map((reservationId) => ({
|
|
280
|
+
reservationId,
|
|
281
|
+
count: 1,
|
|
282
|
+
})),
|
|
283
|
+
},
|
|
284
|
+
{advisoryLocksHeld: true},
|
|
285
|
+
);
|
|
286
|
+
for (const {reason, count} of validation.unavailableByReservation.values())
|
|
287
|
+
recordRunnerReservationCapacityFailure(reason, count);
|
|
288
|
+
|
|
289
|
+
const remainingAcceptedByReservation = new Map(validation.acceptedByReservation);
|
|
290
|
+
return events.map((event) => {
|
|
291
|
+
const existing = existingByProviderRunnerId.get(event.providerRunnerId);
|
|
292
|
+
const reservationId = candidateReservationByProviderRunnerId.get(event.providerRunnerId);
|
|
293
|
+
if (!reservationId) {
|
|
294
|
+
if (event.reservationId && isTerminalState(event.state)) {
|
|
295
|
+
// Terminal cleanup releases the IDs stored on the projection row. An arbitrary
|
|
296
|
+
// terminal report must not consume reservation capacity during that cleanup.
|
|
297
|
+
const matchesExistingReservation =
|
|
298
|
+
existing !== undefined &&
|
|
299
|
+
(existing.reservationId === event.reservationId ||
|
|
300
|
+
existing.intendedReservationId === event.reservationId);
|
|
301
|
+
if (!matchesExistingReservation) return {...event, reservationId: null};
|
|
302
|
+
}
|
|
303
|
+
if (
|
|
304
|
+
event.reservationId &&
|
|
305
|
+
existing?.intendedReservationId &&
|
|
306
|
+
existing.intendedReservationId !== event.reservationId
|
|
307
|
+
)
|
|
308
|
+
return {...event, reservationId: null};
|
|
309
|
+
if (event.reservationId && existing?.reservationReleasedAt)
|
|
310
|
+
return {...event, reservationId: null};
|
|
311
|
+
return event;
|
|
312
|
+
}
|
|
313
|
+
const remaining = remainingAcceptedByReservation.get(reservationId) ?? 0;
|
|
314
|
+
if (remaining === 0) return {...event, reservationId: null};
|
|
315
|
+
remainingAcceptedByReservation.set(reservationId, remaining - 1);
|
|
316
|
+
return event;
|
|
219
317
|
});
|
|
220
318
|
}
|
|
221
319
|
|
|
@@ -247,6 +345,36 @@ export async function listActiveRunnerInstanceCountsByTemplateTx(
|
|
|
247
345
|
);
|
|
248
346
|
}
|
|
249
347
|
|
|
348
|
+
export async function countStaleEnrolledRunnerInstances(params: {
|
|
349
|
+
graceSeconds: number;
|
|
350
|
+
}): Promise<number> {
|
|
351
|
+
const [row] = await db()
|
|
352
|
+
.select({count: sql<number>`count(*)::int`})
|
|
353
|
+
.from(providerRunners)
|
|
354
|
+
.where(
|
|
355
|
+
and(
|
|
356
|
+
eq(providerRunners.state, 'running'),
|
|
357
|
+
isNull(providerRunners.workspaceId),
|
|
358
|
+
isNull(providerRunners.runnerSessionId),
|
|
359
|
+
lt(providerRunners.reportedAt, staleRunnerInstanceCutoff(params.graceSeconds)),
|
|
360
|
+
exists(
|
|
361
|
+
db()
|
|
362
|
+
.select({id: runnerControlSessions.id})
|
|
363
|
+
.from(runnerControlSessions)
|
|
364
|
+
.where(
|
|
365
|
+
and(
|
|
366
|
+
eq(runnerControlSessions.runnerInstanceId, providerRunners.id),
|
|
367
|
+
isNull(runnerControlSessions.closedAt),
|
|
368
|
+
gt(runnerControlSessions.expiresAt, sql`now()`),
|
|
369
|
+
),
|
|
370
|
+
),
|
|
371
|
+
),
|
|
372
|
+
),
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
return row?.count ?? 0;
|
|
376
|
+
}
|
|
377
|
+
|
|
250
378
|
export async function listActiveRunnerInstances(params: {
|
|
251
379
|
workspaceId: string;
|
|
252
380
|
windowSeconds: number;
|
|
@@ -305,9 +433,42 @@ export async function listProvisionerTerminateIntentRowsTx(
|
|
|
305
433
|
);
|
|
306
434
|
}
|
|
307
435
|
|
|
308
|
-
|
|
309
|
-
row.providerRunnerId ? [
|
|
436
|
+
const activationTimeoutRunnerIds = returnedRows.flatMap((row) =>
|
|
437
|
+
row.reason === 'activation-timeout' && row.providerRunnerId ? [row.providerRunnerId] : [],
|
|
310
438
|
);
|
|
439
|
+
if (activationTimeoutRunnerIds.length > 0) {
|
|
440
|
+
await tx
|
|
441
|
+
.update(providerRunners)
|
|
442
|
+
.set({reservationReleasedAt: sql`now()`, updatedAt: sql`now()`})
|
|
443
|
+
.where(
|
|
444
|
+
and(
|
|
445
|
+
eq(providerRunners.workspaceId, params.workspaceId),
|
|
446
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
447
|
+
inArray(providerRunners.providerRunnerId, activationTimeoutRunnerIds),
|
|
448
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
449
|
+
),
|
|
450
|
+
);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return returnedRows.flatMap((row) => toRunnerInstanceTerminateIntent(row));
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function toRunnerInstanceTerminateIntent(row: {
|
|
457
|
+
providerRunnerId: string | null;
|
|
458
|
+
reason: RunnerInstanceTerminateIntentReason;
|
|
459
|
+
activationTimeoutRetry: boolean;
|
|
460
|
+
}): RunnerInstanceTerminateIntent[] {
|
|
461
|
+
if (!row.providerRunnerId) return [];
|
|
462
|
+
|
|
463
|
+
return [
|
|
464
|
+
{
|
|
465
|
+
providerRunnerId: row.providerRunnerId,
|
|
466
|
+
reason: row.reason,
|
|
467
|
+
...(row.reason === 'activation-timeout' && row.activationTimeoutRetry
|
|
468
|
+
? {activationTimeoutRetry: true}
|
|
469
|
+
: {}),
|
|
470
|
+
},
|
|
471
|
+
];
|
|
311
472
|
}
|
|
312
473
|
|
|
313
474
|
function provisionerTerminateIntentsQuery(
|
|
@@ -315,59 +476,91 @@ function provisionerTerminateIntentsQuery(
|
|
|
315
476
|
params: {workspaceId: string; provisionerId: string; providerRunnerIds?: string[]},
|
|
316
477
|
) {
|
|
317
478
|
const newerRunningJobExecutions = alias(runningJobExecutions, 'newer_running_jobs');
|
|
479
|
+
const latestCancelledJob = exists(
|
|
480
|
+
tx
|
|
481
|
+
.select({id: runningJobExecutions.id})
|
|
482
|
+
.from(runningJobExecutions)
|
|
483
|
+
.where(
|
|
484
|
+
and(
|
|
485
|
+
eq(runningJobExecutions.workspaceId, params.workspaceId),
|
|
486
|
+
eq(runningJobExecutions.provisionerId, params.provisionerId),
|
|
487
|
+
eq(runningJobExecutions.providerRunnerId, providerRunners.providerRunnerId),
|
|
488
|
+
isNotNull(runningJobExecutions.cancellationRequestedAt),
|
|
489
|
+
notExists(
|
|
490
|
+
tx
|
|
491
|
+
.select({id: newerRunningJobExecutions.id})
|
|
492
|
+
.from(newerRunningJobExecutions)
|
|
493
|
+
.where(
|
|
494
|
+
and(
|
|
495
|
+
eq(newerRunningJobExecutions.workspaceId, runningJobExecutions.workspaceId),
|
|
496
|
+
eq(newerRunningJobExecutions.provisionerId, runningJobExecutions.provisionerId),
|
|
497
|
+
eq(
|
|
498
|
+
newerRunningJobExecutions.providerRunnerId,
|
|
499
|
+
runningJobExecutions.providerRunnerId,
|
|
500
|
+
),
|
|
501
|
+
or(
|
|
502
|
+
gt(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
503
|
+
and(
|
|
504
|
+
eq(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
505
|
+
gt(
|
|
506
|
+
newerRunningJobExecutions.jobExecutionId,
|
|
507
|
+
runningJobExecutions.jobExecutionId,
|
|
508
|
+
),
|
|
509
|
+
),
|
|
510
|
+
),
|
|
511
|
+
),
|
|
512
|
+
),
|
|
513
|
+
),
|
|
514
|
+
),
|
|
515
|
+
),
|
|
516
|
+
);
|
|
517
|
+
const activationTimeout = and(
|
|
518
|
+
eq(providerRunners.launchKind, 'demand'),
|
|
519
|
+
isNull(providerRunners.runnerSessionId),
|
|
520
|
+
lt(
|
|
521
|
+
providerRunners.createdAt,
|
|
522
|
+
sql`now() - (${config.RUNNER_DEMAND_ACTIVATION_TIMEOUT_SECONDS} || ' seconds')::interval`,
|
|
523
|
+
),
|
|
524
|
+
notExists(
|
|
525
|
+
tx
|
|
526
|
+
.select({id: reservations.id})
|
|
527
|
+
.from(reservations)
|
|
528
|
+
.where(
|
|
529
|
+
and(
|
|
530
|
+
or(
|
|
531
|
+
eq(reservations.id, providerRunners.reservationId),
|
|
532
|
+
eq(reservations.id, providerRunners.intendedReservationId),
|
|
533
|
+
),
|
|
534
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
535
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
536
|
+
gt(reservations.expiresAt, sql`now()`),
|
|
537
|
+
),
|
|
538
|
+
),
|
|
539
|
+
),
|
|
540
|
+
);
|
|
318
541
|
|
|
319
542
|
return tx
|
|
320
543
|
.select({
|
|
321
544
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
322
|
-
|
|
545
|
+
activationTimeoutRetry: sql<boolean>`${providerRunners.reservationReleasedAt} is not null`,
|
|
546
|
+
reason: sql<RunnerInstanceTerminateIntentReason>`case
|
|
547
|
+
when ${activationTimeout} then 'activation-timeout'
|
|
548
|
+
else 'job-cancelled'
|
|
549
|
+
end`,
|
|
323
550
|
})
|
|
324
|
-
.from(
|
|
325
|
-
.innerJoin(
|
|
326
|
-
providerRunners,
|
|
327
|
-
and(
|
|
328
|
-
eq(providerRunners.workspaceId, runningJobExecutions.workspaceId),
|
|
329
|
-
eq(providerRunners.provisionerId, runningJobExecutions.provisionerId),
|
|
330
|
-
eq(providerRunners.providerRunnerId, runningJobExecutions.providerRunnerId),
|
|
331
|
-
),
|
|
332
|
-
)
|
|
551
|
+
.from(providerRunners)
|
|
333
552
|
.where(
|
|
334
553
|
and(
|
|
335
|
-
eq(
|
|
336
|
-
eq(
|
|
554
|
+
eq(providerRunners.workspaceId, params.workspaceId),
|
|
555
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
337
556
|
isNotNull(providerRunners.providerRunnerId),
|
|
338
|
-
isNotNull(runningJobExecutions.cancellationRequestedAt),
|
|
339
557
|
inArray(providerRunners.state, activeStates),
|
|
340
558
|
params.providerRunnerIds && params.providerRunnerIds.length > 0
|
|
341
559
|
? inArray(providerRunners.providerRunnerId, params.providerRunnerIds)
|
|
342
560
|
: undefined,
|
|
343
|
-
|
|
344
|
-
tx
|
|
345
|
-
.select({id: newerRunningJobExecutions.id})
|
|
346
|
-
.from(newerRunningJobExecutions)
|
|
347
|
-
.where(
|
|
348
|
-
and(
|
|
349
|
-
eq(newerRunningJobExecutions.workspaceId, runningJobExecutions.workspaceId),
|
|
350
|
-
eq(newerRunningJobExecutions.provisionerId, runningJobExecutions.provisionerId),
|
|
351
|
-
eq(
|
|
352
|
-
newerRunningJobExecutions.providerRunnerId,
|
|
353
|
-
runningJobExecutions.providerRunnerId,
|
|
354
|
-
),
|
|
355
|
-
or(
|
|
356
|
-
gt(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
357
|
-
and(
|
|
358
|
-
eq(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
359
|
-
gt(
|
|
360
|
-
newerRunningJobExecutions.jobExecutionId,
|
|
361
|
-
runningJobExecutions.jobExecutionId,
|
|
362
|
-
),
|
|
363
|
-
),
|
|
364
|
-
),
|
|
365
|
-
),
|
|
366
|
-
),
|
|
367
|
-
),
|
|
561
|
+
or(latestCancelledJob, activationTimeout),
|
|
368
562
|
),
|
|
369
|
-
)
|
|
370
|
-
.groupBy(providerRunners.providerRunnerId);
|
|
563
|
+
);
|
|
371
564
|
}
|
|
372
565
|
|
|
373
566
|
async function listTerminateIntentsHonoredByTerminatedReportsTx(
|
|
@@ -388,9 +581,7 @@ async function listTerminateIntentsHonoredByTerminatedReportsTx(
|
|
|
388
581
|
provisionerId: params.provisionerId,
|
|
389
582
|
providerRunnerIds: terminatedRunnerInstanceIds,
|
|
390
583
|
}).orderBy(asc(providerRunners.providerRunnerId));
|
|
391
|
-
return rows.flatMap((row) =>
|
|
392
|
-
row.providerRunnerId ? [{...row, providerRunnerId: row.providerRunnerId}] : [],
|
|
393
|
-
);
|
|
584
|
+
return rows.flatMap((row) => toRunnerInstanceTerminateIntent(row));
|
|
394
585
|
}
|
|
395
586
|
|
|
396
587
|
export async function reconcileRunnerInstances(
|
|
@@ -59,33 +59,44 @@ export async function createRunnerSessionConsumingActivationToken(params: {
|
|
|
59
59
|
toolCapabilities?: RunnerToolCapabilitiesDto | null;
|
|
60
60
|
}) {
|
|
61
61
|
return await db().transaction(async (tx) => {
|
|
62
|
-
const [
|
|
62
|
+
const [runner] = await tx
|
|
63
63
|
.select({
|
|
64
|
-
|
|
65
|
-
runnerInstanceId:
|
|
64
|
+
activationTokenId: runnerActivationTokens.id,
|
|
65
|
+
runnerInstanceId: providerRunners.id,
|
|
66
66
|
workspaceId: providerRunners.workspaceId,
|
|
67
67
|
provisionerId: providerRunners.provisionerId,
|
|
68
68
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
69
|
+
runnerSessionId: providerRunners.runnerSessionId,
|
|
69
70
|
})
|
|
70
71
|
.from(runnerActivationTokens)
|
|
71
72
|
.innerJoin(providerRunners, eq(providerRunners.id, runnerActivationTokens.runnerInstanceId))
|
|
73
|
+
.where(eq(runnerActivationTokens.id, params.activationTokenId))
|
|
74
|
+
.limit(1)
|
|
75
|
+
.for('update', {of: providerRunners});
|
|
76
|
+
if (!runner?.workspaceId || !runner.providerRunnerId || runner.runnerSessionId)
|
|
77
|
+
throw new Error('Runner activation token is invalid, expired, or has already been used');
|
|
78
|
+
|
|
79
|
+
const [activationToken] = await tx
|
|
80
|
+
.select({id: runnerActivationTokens.id})
|
|
81
|
+
.from(runnerActivationTokens)
|
|
72
82
|
.where(
|
|
73
83
|
and(
|
|
74
|
-
eq(runnerActivationTokens.id,
|
|
84
|
+
eq(runnerActivationTokens.id, runner.activationTokenId),
|
|
85
|
+
eq(runnerActivationTokens.runnerInstanceId, runner.runnerInstanceId),
|
|
75
86
|
isNull(runnerActivationTokens.consumedAt),
|
|
76
87
|
isNull(runnerActivationTokens.revokedAt),
|
|
77
88
|
gt(runnerActivationTokens.expiresAt, sql`now()`),
|
|
78
|
-
isNull(providerRunners.runnerSessionId),
|
|
79
89
|
),
|
|
80
90
|
)
|
|
81
91
|
.limit(1)
|
|
82
92
|
.for('update');
|
|
83
|
-
if (!
|
|
93
|
+
if (!activationToken)
|
|
84
94
|
throw new Error('Runner activation token is invalid, expired, or has already been used');
|
|
95
|
+
|
|
85
96
|
const [provisioner] = await tx
|
|
86
97
|
.select({scope: provisionerTokens.scope})
|
|
87
98
|
.from(provisionerTokens)
|
|
88
|
-
.where(eq(provisionerTokens.id,
|
|
99
|
+
.where(eq(provisionerTokens.id, runner.provisionerId))
|
|
89
100
|
.limit(1);
|
|
90
101
|
const labels = sanitizeRunnerLabelsOrThrow(params.labels, {
|
|
91
102
|
scope: provisioner?.scope ?? 'workspace',
|
|
@@ -95,13 +106,13 @@ export async function createRunnerSessionConsumingActivationToken(params: {
|
|
|
95
106
|
const [session] = await tx
|
|
96
107
|
.insert(runnerSessions)
|
|
97
108
|
.values({
|
|
98
|
-
workspaceId:
|
|
109
|
+
workspaceId: runner.workspaceId,
|
|
99
110
|
scope: 'workspace',
|
|
100
|
-
registrationTokenId:
|
|
111
|
+
registrationTokenId: activationToken.id,
|
|
101
112
|
registrationTokenKind: 'activation',
|
|
102
|
-
runnerInstanceId:
|
|
103
|
-
provisionerId:
|
|
104
|
-
providerRunnerId:
|
|
113
|
+
runnerInstanceId: runner.runnerInstanceId,
|
|
114
|
+
provisionerId: runner.provisionerId,
|
|
115
|
+
providerRunnerId: runner.providerRunnerId,
|
|
105
116
|
labels,
|
|
106
117
|
toolCapabilities: params.toolCapabilities ?? null,
|
|
107
118
|
toolCapabilitiesReportedAt: params.toolCapabilities ? sql`now()` : null,
|
|
@@ -113,13 +124,13 @@ export async function createRunnerSessionConsumingActivationToken(params: {
|
|
|
113
124
|
await tx
|
|
114
125
|
.update(runnerActivationTokens)
|
|
115
126
|
.set({consumedAt: sql`now()`, consumedSessionId: session.id})
|
|
116
|
-
.where(eq(runnerActivationTokens.id,
|
|
127
|
+
.where(eq(runnerActivationTokens.id, activationToken.id));
|
|
117
128
|
await tx
|
|
118
129
|
.update(providerRunners)
|
|
119
130
|
.set({runnerSessionId: session.id, updatedAt: sql`now()`})
|
|
120
131
|
.where(
|
|
121
132
|
and(
|
|
122
|
-
eq(providerRunners.id,
|
|
133
|
+
eq(providerRunners.id, runner.runnerInstanceId),
|
|
123
134
|
isNull(providerRunners.runnerSessionId),
|
|
124
135
|
),
|
|
125
136
|
);
|
|
@@ -128,7 +139,7 @@ export async function createRunnerSessionConsumingActivationToken(params: {
|
|
|
128
139
|
.set({closedAt: sql`now()`, closeReason: 'activated'})
|
|
129
140
|
.where(
|
|
130
141
|
and(
|
|
131
|
-
eq(runnerControlSessions.runnerInstanceId,
|
|
142
|
+
eq(runnerControlSessions.runnerInstanceId, runner.runnerInstanceId),
|
|
132
143
|
isNull(runnerControlSessions.closedAt),
|
|
133
144
|
),
|
|
134
145
|
);
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import {uuidv7PrimaryKey} from '@shipfox/node-drizzle';
|
|
2
2
|
import {sql} from 'drizzle-orm';
|
|
3
|
-
import {check, index, integer, text, timestamp, uuid} from 'drizzle-orm/pg-core';
|
|
3
|
+
import {check, index, integer, pgEnum, text, timestamp, uuid} from 'drizzle-orm/pg-core';
|
|
4
4
|
import type {Reservation} from '#core/entities/reservation.js';
|
|
5
5
|
import {pgTable} from './common.js';
|
|
6
6
|
|
|
7
|
+
export const reservationKindEnum = pgEnum('runners_reservation_kind', ['bound', 'launch']);
|
|
8
|
+
|
|
7
9
|
export const reservations = pgTable(
|
|
8
10
|
'reservations',
|
|
9
11
|
{
|
|
@@ -12,6 +14,7 @@ export const reservations = pgTable(
|
|
|
12
14
|
provisionerId: uuid('provisioner_id').notNull(),
|
|
13
15
|
requiredLabels: text('required_labels').array().notNull(),
|
|
14
16
|
count: integer('count').notNull(),
|
|
17
|
+
kind: reservationKindEnum('kind').notNull().default('launch'),
|
|
15
18
|
createdAt: timestamp('created_at', {withTimezone: true}).notNull().defaultNow(),
|
|
16
19
|
expiresAt: timestamp('expires_at', {withTimezone: true}).notNull(),
|
|
17
20
|
},
|
|
@@ -32,6 +35,7 @@ export function toReservation(row: ReservationDb): Reservation {
|
|
|
32
35
|
provisionerId: row.provisionerId,
|
|
33
36
|
requiredLabels: row.requiredLabels,
|
|
34
37
|
count: row.count,
|
|
38
|
+
kind: row.kind,
|
|
35
39
|
createdAt: row.createdAt,
|
|
36
40
|
expiresAt: row.expiresAt,
|
|
37
41
|
};
|
|
@@ -14,6 +14,12 @@ export const providerRunnerStateEnum = pgEnum('runners_provider_runner_state', [
|
|
|
14
14
|
'terminated',
|
|
15
15
|
]);
|
|
16
16
|
|
|
17
|
+
export const providerRunnerLaunchKindEnum = pgEnum('runners_provider_runner_launch_kind', [
|
|
18
|
+
'demand',
|
|
19
|
+
'warm',
|
|
20
|
+
'manual',
|
|
21
|
+
]);
|
|
22
|
+
|
|
17
23
|
export const providerRunners = pgTable(
|
|
18
24
|
'runner_instances',
|
|
19
25
|
{
|
|
@@ -25,6 +31,7 @@ export const providerRunners = pgTable(
|
|
|
25
31
|
providerRunnerId: text('provider_runner_id'),
|
|
26
32
|
intendedReservationId: uuid('intended_reservation_id'),
|
|
27
33
|
reservationId: uuid('reservation_id'),
|
|
34
|
+
launchKind: providerRunnerLaunchKindEnum('launch_kind').notNull().default('manual'),
|
|
28
35
|
assignedAt: timestamp('assigned_at', {withTimezone: true}),
|
|
29
36
|
templateKey: text('template_key'),
|
|
30
37
|
labels: text('labels').array().notNull().default([]),
|
|
@@ -82,7 +89,9 @@ export function toRunnerInstance(row: RunnerInstanceDb): RunnerInstance {
|
|
|
82
89
|
workspaceId: row.workspaceId,
|
|
83
90
|
provisionerId: row.provisionerId,
|
|
84
91
|
providerRunnerId: row.providerRunnerId,
|
|
92
|
+
intendedReservationId: row.intendedReservationId,
|
|
85
93
|
reservationId: row.reservationId,
|
|
94
|
+
launchKind: row.launchKind,
|
|
86
95
|
assignedAt: row.assignedAt,
|
|
87
96
|
templateKey: row.templateKey,
|
|
88
97
|
labels: row.labels,
|
package/src/metrics/index.ts
CHANGED
|
@@ -1,13 +1,22 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type {
|
|
2
|
+
RunnerActivationTokenNotIssuedReason,
|
|
3
|
+
RunnerActivationTokenNotIssuedSurface,
|
|
4
|
+
RunnerReservationCapacityFailureReason,
|
|
5
|
+
RunnerReservationPromotionFailureReason,
|
|
6
|
+
} from './instance.js';
|
|
2
7
|
export {
|
|
3
8
|
jobExecutionClaimedCount,
|
|
4
9
|
jobExecutionEnqueuedCount,
|
|
5
10
|
jobExecutionLeaseExpiredCount,
|
|
6
11
|
providerRunnerAbsentTerminatedCount,
|
|
12
|
+
providerRunnerActivationOutcomeCount,
|
|
7
13
|
providerRunnerCountDivergenceCount,
|
|
8
14
|
providerRunnerReconcileCallCount,
|
|
9
15
|
providerRunnerTerminateIntentHonoredCount,
|
|
10
16
|
providerRunnerTerminateIntentIssuedCount,
|
|
17
|
+
recordProviderRunnerActivationOutcome,
|
|
18
|
+
recordRunnerActivationTokenNotIssued,
|
|
19
|
+
recordRunnerReservationCapacityFailure,
|
|
11
20
|
recordRunnerReservationPromotionFailure,
|
|
12
21
|
recordRunnersRateLimitCheck,
|
|
13
22
|
recordRunnersRateLimitPruneFailure,
|