@shipfox/api-runners 12.3.0 → 12.4.1
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 +36 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +9 -2
- package/dist/config.js.map +1 -1
- package/dist/core/demand.d.ts +4 -2
- package/dist/core/demand.d.ts.map +1 -1
- package/dist/core/demand.js +22 -6
- package/dist/core/demand.js.map +1 -1
- package/dist/core/entities/reservation.d.ts +1 -0
- package/dist/core/entities/reservation.d.ts.map +1 -1
- package/dist/core/entities/reservation.js.map +1 -1
- package/dist/core/entities/runner-instance.d.ts +3 -0
- package/dist/core/entities/runner-instance.d.ts.map +1 -1
- package/dist/core/entities/runner-instance.js.map +1 -1
- package/dist/core/errors.d.ts +5 -1
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +9 -2
- package/dist/core/errors.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 +116 -20
- 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/job-executions.d.ts.map +1 -1
- package/dist/db/job-executions.js +35 -4
- package/dist/db/job-executions.js.map +1 -1
- package/dist/db/reservation-locks.d.ts +6 -0
- package/dist/db/reservation-locks.d.ts.map +1 -0
- package/dist/db/reservation-locks.js +15 -0
- package/dist/db/reservation-locks.js.map +1 -0
- package/dist/db/reservations.d.ts +17 -3
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +246 -51
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +25 -1
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +160 -18
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +11 -2
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +167 -24
- 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 +14 -1
- 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 +72 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +161 -1
- package/dist/metrics/instance.js.map +1 -1
- package/dist/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +25 -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 +10 -4
- 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 +6 -6
- package/src/config.test.ts +22 -0
- package/src/config.ts +15 -2
- package/src/core/demand.test.ts +192 -3
- package/src/core/demand.ts +44 -4
- package/src/core/entities/reservation.ts +1 -0
- package/src/core/entities/runner-instance.ts +4 -0
- package/src/core/errors.ts +30 -1
- package/src/core/runner-activation.ts +48 -2
- package/src/core/runner-control-sessions.test.ts +135 -3
- package/src/core/runner-control-sessions.ts +141 -19
- 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/job-executions.test.ts +98 -12
- package/src/db/job-executions.ts +52 -4
- package/src/db/reservation-locks.ts +22 -0
- package/src/db/reservations.test.ts +733 -22
- package/src/db/reservations.ts +408 -144
- package/src/db/runner-assignments.test.ts +105 -1
- package/src/db/runner-assignments.ts +245 -25
- package/src/db/runner-instances.test.ts +357 -2
- package/src/db/runner-instances.ts +289 -55
- package/src/db/runner-sessions.ts +19 -1
- 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 +24 -1
- package/src/metrics/instance.test.ts +118 -0
- package/src/metrics/instance.ts +247 -3
- package/src/metrics/service.test.ts +46 -1
- package/src/metrics/service.ts +55 -8
- 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 +14 -2
- package/src/presentation/routes/late-runner-enrollment.test.ts +219 -2
- package/src/presentation/routes/poll-demand.test.ts +56 -0
- package/src/presentation/routes/poll-demand.ts +1 -0
- package/src/presentation/routes/reconcile-runner-instances.test.ts +34 -2
- package/src/presentation/routes/reconcile-runner-instances.ts +7 -1
- package/src/presentation/routes/runner-enrollment.test.ts +316 -3
- package/src/presentation/routes/runner-enrollment.ts +8 -5
- package/test/factories/reservation.ts +2 -0
- package/test/factories/runner-instance.ts +6 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -17,15 +17,20 @@ 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';
|
|
@@ -34,11 +39,6 @@ import {providerRunners, toRunnerInstance} from './schema/runner-instances.js';
|
|
|
34
39
|
import {runnerSessions} from './schema/runner-sessions.js';
|
|
35
40
|
import {runningJobExecutions} from './schema/running-job-executions.js';
|
|
36
41
|
|
|
37
|
-
export const terminalStates = [
|
|
38
|
-
'stopped',
|
|
39
|
-
'failed',
|
|
40
|
-
'terminated',
|
|
41
|
-
] as const satisfies readonly RunnerInstanceState[];
|
|
42
42
|
export const activeStates = [
|
|
43
43
|
'starting',
|
|
44
44
|
'running',
|
|
@@ -49,11 +49,12 @@ export const divergenceCountStates = ['starting', 'running'] as const satisfies
|
|
|
49
49
|
'starting' | 'running'
|
|
50
50
|
>[];
|
|
51
51
|
|
|
52
|
-
export type RunnerInstanceTerminateIntentReason = 'job-cancelled';
|
|
52
|
+
export type RunnerInstanceTerminateIntentReason = 'activation-timeout' | 'job-cancelled';
|
|
53
53
|
|
|
54
54
|
export interface RunnerInstanceTerminateIntent {
|
|
55
55
|
providerRunnerId: string;
|
|
56
56
|
reason: RunnerInstanceTerminateIntentReason;
|
|
57
|
+
activationTimeoutRetry?: boolean;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export interface ActiveRunnerInstanceTemplateCount {
|
|
@@ -155,13 +156,14 @@ export async function reportRunnerInstances(params: ReportRunnerInstancesParams)
|
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
const events = await hydrateRunnerSessionIdsFromConsumedTokens(tx, params, aggregatedEvents);
|
|
159
|
+
const reservationSafeEvents = await guardReportedReservationIdsTx(tx, params, events);
|
|
158
160
|
const terminateIntentsHonored = await listTerminateIntentsHonoredByTerminatedReportsTx(
|
|
159
161
|
tx,
|
|
160
162
|
params,
|
|
161
|
-
|
|
163
|
+
reservationSafeEvents,
|
|
162
164
|
);
|
|
163
165
|
|
|
164
|
-
const values =
|
|
166
|
+
const values = reservationSafeEvents.map((event) => ({
|
|
165
167
|
workspaceId: params.workspaceId,
|
|
166
168
|
provisionerId: params.provisionerId,
|
|
167
169
|
providerRunnerId: event.providerRunnerId,
|
|
@@ -213,10 +215,105 @@ export async function reportRunnerInstances(params: ReportRunnerInstancesParams)
|
|
|
213
215
|
});
|
|
214
216
|
|
|
215
217
|
const reservationsReleased = hasTerminalEvent
|
|
216
|
-
? await releaseTerminalRunnerInstanceReservations(tx, params,
|
|
218
|
+
? await releaseTerminalRunnerInstanceReservations(tx, params, reservationSafeEvents)
|
|
217
219
|
: 0;
|
|
218
220
|
|
|
219
|
-
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;
|
|
220
317
|
});
|
|
221
318
|
}
|
|
222
319
|
|
|
@@ -278,6 +375,80 @@ export async function countStaleEnrolledRunnerInstances(params: {
|
|
|
278
375
|
return row?.count ?? 0;
|
|
279
376
|
}
|
|
280
377
|
|
|
378
|
+
export type ProviderRunnerPhase =
|
|
379
|
+
| 'control_session'
|
|
380
|
+
| 'enrollment'
|
|
381
|
+
| 'assignment'
|
|
382
|
+
| 'activation'
|
|
383
|
+
| 'idle';
|
|
384
|
+
|
|
385
|
+
export interface ProviderRunnerPhaseMetric {
|
|
386
|
+
phase: ProviderRunnerPhase;
|
|
387
|
+
provider: string;
|
|
388
|
+
launchKind: 'demand' | 'warm' | 'manual';
|
|
389
|
+
count: number;
|
|
390
|
+
oldestAgeMilliseconds: number;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export async function listProviderRunnerByPhaseMetrics(): Promise<ProviderRunnerPhaseMetric[]> {
|
|
394
|
+
const phase = sql<ProviderRunnerPhase>`case
|
|
395
|
+
when ${runnerControlSessions.id} is null then 'control_session'
|
|
396
|
+
when ${providerRunners.state} <> 'running' then 'enrollment'
|
|
397
|
+
when ${providerRunners.intendedReservationId} is not null
|
|
398
|
+
and ${providerRunners.workspaceId} is null then 'assignment'
|
|
399
|
+
when ${providerRunners.workspaceId} is not null then 'activation'
|
|
400
|
+
else 'idle'
|
|
401
|
+
end`;
|
|
402
|
+
const startedAt = sql<Date | null>`case
|
|
403
|
+
when ${runnerControlSessions.id} is null then ${providerRunners.createdAt}
|
|
404
|
+
when ${providerRunners.state} <> 'running' then ${runnerControlSessions.createdAt}
|
|
405
|
+
when ${providerRunners.intendedReservationId} is not null
|
|
406
|
+
and ${providerRunners.workspaceId} is null then ${runnerControlSessions.createdAt}
|
|
407
|
+
when ${providerRunners.workspaceId} is not null
|
|
408
|
+
then coalesce(${providerRunners.assignedAt}, ${runnerControlSessions.createdAt})
|
|
409
|
+
else coalesce(
|
|
410
|
+
${runnerControlSessions.createdAt},
|
|
411
|
+
${providerRunners.assignedAt},
|
|
412
|
+
${providerRunners.createdAt}
|
|
413
|
+
)
|
|
414
|
+
end`;
|
|
415
|
+
const rows = await db()
|
|
416
|
+
.select({
|
|
417
|
+
phase,
|
|
418
|
+
provider: sql<string>`coalesce(${providerRunners.providerKind}, 'unknown')`,
|
|
419
|
+
launchKind: providerRunners.launchKind,
|
|
420
|
+
count: sql<number>`count(*)::int`,
|
|
421
|
+
oldestAgeMilliseconds: sql<number>`coalesce(
|
|
422
|
+
max(extract(epoch from (now() - (${startedAt}))) * 1000),
|
|
423
|
+
0
|
|
424
|
+
)::double precision`,
|
|
425
|
+
})
|
|
426
|
+
.from(providerRunners)
|
|
427
|
+
.leftJoin(
|
|
428
|
+
runnerControlSessions,
|
|
429
|
+
and(
|
|
430
|
+
eq(runnerControlSessions.runnerInstanceId, providerRunners.id),
|
|
431
|
+
isNull(runnerControlSessions.closedAt),
|
|
432
|
+
gt(runnerControlSessions.expiresAt, sql`now()`),
|
|
433
|
+
),
|
|
434
|
+
)
|
|
435
|
+
.where(
|
|
436
|
+
and(
|
|
437
|
+
inArray(providerRunners.state, ['starting', 'running']),
|
|
438
|
+
isNull(providerRunners.runnerSessionId),
|
|
439
|
+
),
|
|
440
|
+
)
|
|
441
|
+
.groupBy(phase, providerRunners.providerKind, providerRunners.launchKind);
|
|
442
|
+
|
|
443
|
+
return rows.map((row) => ({
|
|
444
|
+
phase: row.phase,
|
|
445
|
+
provider: row.provider,
|
|
446
|
+
launchKind: row.launchKind,
|
|
447
|
+
count: row.count,
|
|
448
|
+
oldestAgeMilliseconds: Math.max(0, row.oldestAgeMilliseconds),
|
|
449
|
+
}));
|
|
450
|
+
}
|
|
451
|
+
|
|
281
452
|
export async function listActiveRunnerInstances(params: {
|
|
282
453
|
workspaceId: string;
|
|
283
454
|
windowSeconds: number;
|
|
@@ -336,9 +507,42 @@ export async function listProvisionerTerminateIntentRowsTx(
|
|
|
336
507
|
);
|
|
337
508
|
}
|
|
338
509
|
|
|
339
|
-
|
|
340
|
-
row.providerRunnerId ? [
|
|
510
|
+
const activationTimeoutRunnerIds = returnedRows.flatMap((row) =>
|
|
511
|
+
row.reason === 'activation-timeout' && row.providerRunnerId ? [row.providerRunnerId] : [],
|
|
341
512
|
);
|
|
513
|
+
if (activationTimeoutRunnerIds.length > 0) {
|
|
514
|
+
await tx
|
|
515
|
+
.update(providerRunners)
|
|
516
|
+
.set({reservationReleasedAt: sql`now()`, updatedAt: sql`now()`})
|
|
517
|
+
.where(
|
|
518
|
+
and(
|
|
519
|
+
eq(providerRunners.workspaceId, params.workspaceId),
|
|
520
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
521
|
+
inArray(providerRunners.providerRunnerId, activationTimeoutRunnerIds),
|
|
522
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
523
|
+
),
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return returnedRows.flatMap((row) => toRunnerInstanceTerminateIntent(row));
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function toRunnerInstanceTerminateIntent(row: {
|
|
531
|
+
providerRunnerId: string | null;
|
|
532
|
+
reason: RunnerInstanceTerminateIntentReason;
|
|
533
|
+
activationTimeoutRetry: boolean;
|
|
534
|
+
}): RunnerInstanceTerminateIntent[] {
|
|
535
|
+
if (!row.providerRunnerId) return [];
|
|
536
|
+
|
|
537
|
+
return [
|
|
538
|
+
{
|
|
539
|
+
providerRunnerId: row.providerRunnerId,
|
|
540
|
+
reason: row.reason,
|
|
541
|
+
...(row.reason === 'activation-timeout' && row.activationTimeoutRetry
|
|
542
|
+
? {activationTimeoutRetry: true}
|
|
543
|
+
: {}),
|
|
544
|
+
},
|
|
545
|
+
];
|
|
342
546
|
}
|
|
343
547
|
|
|
344
548
|
function provisionerTerminateIntentsQuery(
|
|
@@ -346,59 +550,91 @@ function provisionerTerminateIntentsQuery(
|
|
|
346
550
|
params: {workspaceId: string; provisionerId: string; providerRunnerIds?: string[]},
|
|
347
551
|
) {
|
|
348
552
|
const newerRunningJobExecutions = alias(runningJobExecutions, 'newer_running_jobs');
|
|
553
|
+
const latestCancelledJob = exists(
|
|
554
|
+
tx
|
|
555
|
+
.select({id: runningJobExecutions.id})
|
|
556
|
+
.from(runningJobExecutions)
|
|
557
|
+
.where(
|
|
558
|
+
and(
|
|
559
|
+
eq(runningJobExecutions.workspaceId, params.workspaceId),
|
|
560
|
+
eq(runningJobExecutions.provisionerId, params.provisionerId),
|
|
561
|
+
eq(runningJobExecutions.providerRunnerId, providerRunners.providerRunnerId),
|
|
562
|
+
isNotNull(runningJobExecutions.cancellationRequestedAt),
|
|
563
|
+
notExists(
|
|
564
|
+
tx
|
|
565
|
+
.select({id: newerRunningJobExecutions.id})
|
|
566
|
+
.from(newerRunningJobExecutions)
|
|
567
|
+
.where(
|
|
568
|
+
and(
|
|
569
|
+
eq(newerRunningJobExecutions.workspaceId, runningJobExecutions.workspaceId),
|
|
570
|
+
eq(newerRunningJobExecutions.provisionerId, runningJobExecutions.provisionerId),
|
|
571
|
+
eq(
|
|
572
|
+
newerRunningJobExecutions.providerRunnerId,
|
|
573
|
+
runningJobExecutions.providerRunnerId,
|
|
574
|
+
),
|
|
575
|
+
or(
|
|
576
|
+
gt(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
577
|
+
and(
|
|
578
|
+
eq(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
579
|
+
gt(
|
|
580
|
+
newerRunningJobExecutions.jobExecutionId,
|
|
581
|
+
runningJobExecutions.jobExecutionId,
|
|
582
|
+
),
|
|
583
|
+
),
|
|
584
|
+
),
|
|
585
|
+
),
|
|
586
|
+
),
|
|
587
|
+
),
|
|
588
|
+
),
|
|
589
|
+
),
|
|
590
|
+
);
|
|
591
|
+
const activationTimeout = and(
|
|
592
|
+
eq(providerRunners.launchKind, 'demand'),
|
|
593
|
+
isNull(providerRunners.runnerSessionId),
|
|
594
|
+
lt(
|
|
595
|
+
providerRunners.createdAt,
|
|
596
|
+
sql`now() - (${config.RUNNER_DEMAND_ACTIVATION_TIMEOUT_SECONDS} || ' seconds')::interval`,
|
|
597
|
+
),
|
|
598
|
+
notExists(
|
|
599
|
+
tx
|
|
600
|
+
.select({id: reservations.id})
|
|
601
|
+
.from(reservations)
|
|
602
|
+
.where(
|
|
603
|
+
and(
|
|
604
|
+
or(
|
|
605
|
+
eq(reservations.id, providerRunners.reservationId),
|
|
606
|
+
eq(reservations.id, providerRunners.intendedReservationId),
|
|
607
|
+
),
|
|
608
|
+
eq(reservations.workspaceId, params.workspaceId),
|
|
609
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
610
|
+
gt(reservations.expiresAt, sql`now()`),
|
|
611
|
+
),
|
|
612
|
+
),
|
|
613
|
+
),
|
|
614
|
+
);
|
|
349
615
|
|
|
350
616
|
return tx
|
|
351
617
|
.select({
|
|
352
618
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
353
|
-
|
|
619
|
+
activationTimeoutRetry: sql<boolean>`${providerRunners.reservationReleasedAt} is not null`,
|
|
620
|
+
reason: sql<RunnerInstanceTerminateIntentReason>`case
|
|
621
|
+
when ${activationTimeout} then 'activation-timeout'
|
|
622
|
+
else 'job-cancelled'
|
|
623
|
+
end`,
|
|
354
624
|
})
|
|
355
|
-
.from(
|
|
356
|
-
.innerJoin(
|
|
357
|
-
providerRunners,
|
|
358
|
-
and(
|
|
359
|
-
eq(providerRunners.workspaceId, runningJobExecutions.workspaceId),
|
|
360
|
-
eq(providerRunners.provisionerId, runningJobExecutions.provisionerId),
|
|
361
|
-
eq(providerRunners.providerRunnerId, runningJobExecutions.providerRunnerId),
|
|
362
|
-
),
|
|
363
|
-
)
|
|
625
|
+
.from(providerRunners)
|
|
364
626
|
.where(
|
|
365
627
|
and(
|
|
366
|
-
eq(
|
|
367
|
-
eq(
|
|
628
|
+
eq(providerRunners.workspaceId, params.workspaceId),
|
|
629
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
368
630
|
isNotNull(providerRunners.providerRunnerId),
|
|
369
|
-
isNotNull(runningJobExecutions.cancellationRequestedAt),
|
|
370
631
|
inArray(providerRunners.state, activeStates),
|
|
371
632
|
params.providerRunnerIds && params.providerRunnerIds.length > 0
|
|
372
633
|
? inArray(providerRunners.providerRunnerId, params.providerRunnerIds)
|
|
373
634
|
: undefined,
|
|
374
|
-
|
|
375
|
-
tx
|
|
376
|
-
.select({id: newerRunningJobExecutions.id})
|
|
377
|
-
.from(newerRunningJobExecutions)
|
|
378
|
-
.where(
|
|
379
|
-
and(
|
|
380
|
-
eq(newerRunningJobExecutions.workspaceId, runningJobExecutions.workspaceId),
|
|
381
|
-
eq(newerRunningJobExecutions.provisionerId, runningJobExecutions.provisionerId),
|
|
382
|
-
eq(
|
|
383
|
-
newerRunningJobExecutions.providerRunnerId,
|
|
384
|
-
runningJobExecutions.providerRunnerId,
|
|
385
|
-
),
|
|
386
|
-
or(
|
|
387
|
-
gt(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
388
|
-
and(
|
|
389
|
-
eq(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt),
|
|
390
|
-
gt(
|
|
391
|
-
newerRunningJobExecutions.jobExecutionId,
|
|
392
|
-
runningJobExecutions.jobExecutionId,
|
|
393
|
-
),
|
|
394
|
-
),
|
|
395
|
-
),
|
|
396
|
-
),
|
|
397
|
-
),
|
|
398
|
-
),
|
|
635
|
+
or(latestCancelledJob, activationTimeout),
|
|
399
636
|
),
|
|
400
|
-
)
|
|
401
|
-
.groupBy(providerRunners.providerRunnerId);
|
|
637
|
+
);
|
|
402
638
|
}
|
|
403
639
|
|
|
404
640
|
async function listTerminateIntentsHonoredByTerminatedReportsTx(
|
|
@@ -419,9 +655,7 @@ async function listTerminateIntentsHonoredByTerminatedReportsTx(
|
|
|
419
655
|
provisionerId: params.provisionerId,
|
|
420
656
|
providerRunnerIds: terminatedRunnerInstanceIds,
|
|
421
657
|
}).orderBy(asc(providerRunners.providerRunnerId));
|
|
422
|
-
return rows.flatMap((row) =>
|
|
423
|
-
row.providerRunnerId ? [{...row, providerRunnerId: row.providerRunnerId}] : [],
|
|
424
|
-
);
|
|
658
|
+
return rows.flatMap((row) => toRunnerInstanceTerminateIntent(row));
|
|
425
659
|
}
|
|
426
660
|
|
|
427
661
|
export async function reconcileRunnerInstances(
|
|
@@ -3,6 +3,10 @@ import {and, asc, eq, gt, inArray, isNull, lt, notExists, or, sql} from 'drizzle
|
|
|
3
3
|
import type {RunnerSession} from '#core/entities/runner-session.js';
|
|
4
4
|
import {EmptyRunnerLabelsError} from '#core/errors.js';
|
|
5
5
|
import {sanitizeRunnerLabelsOrThrow} from '#core/runner-labels.js';
|
|
6
|
+
import {
|
|
7
|
+
type ProviderRunnerLifecycleObservation,
|
|
8
|
+
recordProviderRunnerAssignmentToActivation,
|
|
9
|
+
} from '#metrics/instance.js';
|
|
6
10
|
import {db} from './db.js';
|
|
7
11
|
import {provisionerTokens} from './schema/provisioner-tokens.js';
|
|
8
12
|
import {runnerActivationTokens} from './schema/runner-activation-tokens.js';
|
|
@@ -58,7 +62,8 @@ export async function createRunnerSessionConsumingActivationToken(params: {
|
|
|
58
62
|
labels: string[];
|
|
59
63
|
toolCapabilities?: RunnerToolCapabilitiesDto | null;
|
|
60
64
|
}) {
|
|
61
|
-
|
|
65
|
+
let assignmentToActivationObservation: ProviderRunnerLifecycleObservation | null = null;
|
|
66
|
+
const session = await db().transaction(async (tx) => {
|
|
62
67
|
const [runner] = await tx
|
|
63
68
|
.select({
|
|
64
69
|
activationTokenId: runnerActivationTokens.id,
|
|
@@ -66,6 +71,9 @@ export async function createRunnerSessionConsumingActivationToken(params: {
|
|
|
66
71
|
workspaceId: providerRunners.workspaceId,
|
|
67
72
|
provisionerId: providerRunners.provisionerId,
|
|
68
73
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
74
|
+
assignedAt: providerRunners.assignedAt,
|
|
75
|
+
provider: providerRunners.providerKind,
|
|
76
|
+
launchKind: providerRunners.launchKind,
|
|
69
77
|
runnerSessionId: providerRunners.runnerSessionId,
|
|
70
78
|
})
|
|
71
79
|
.from(runnerActivationTokens)
|
|
@@ -143,8 +151,18 @@ export async function createRunnerSessionConsumingActivationToken(params: {
|
|
|
143
151
|
isNull(runnerControlSessions.closedAt),
|
|
144
152
|
),
|
|
145
153
|
);
|
|
154
|
+
if (runner.assignedAt)
|
|
155
|
+
assignmentToActivationObservation = {
|
|
156
|
+
durationMilliseconds: session.createdAt.getTime() - runner.assignedAt.getTime(),
|
|
157
|
+
provider: runner.provider,
|
|
158
|
+
launchKind: runner.launchKind,
|
|
159
|
+
runnerInstanceId: runner.runnerInstanceId,
|
|
160
|
+
};
|
|
146
161
|
return toRunnerSession(session);
|
|
147
162
|
});
|
|
163
|
+
if (assignmentToActivationObservation)
|
|
164
|
+
recordProviderRunnerAssignmentToActivation(assignmentToActivationObservation);
|
|
165
|
+
return session;
|
|
148
166
|
}
|
|
149
167
|
|
|
150
168
|
export interface DeleteExpiredRunnerSessionsParams {
|
|
@@ -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,36 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type {
|
|
2
|
+
RunnerActivationTokenNotIssuedReason,
|
|
3
|
+
RunnerActivationTokenNotIssuedSurface,
|
|
4
|
+
RunnerAssignmentSurface,
|
|
5
|
+
RunnerLaunchKind,
|
|
6
|
+
RunnerReservationCapacityFailureReason,
|
|
7
|
+
RunnerReservationPromotionFailureReason,
|
|
8
|
+
} from './instance.js';
|
|
2
9
|
export {
|
|
3
10
|
jobExecutionClaimedCount,
|
|
4
11
|
jobExecutionEnqueuedCount,
|
|
5
12
|
jobExecutionLeaseExpiredCount,
|
|
13
|
+
jobExecutionQueueTimeDuration,
|
|
6
14
|
providerRunnerAbsentTerminatedCount,
|
|
15
|
+
providerRunnerActivationOutcomeCount,
|
|
16
|
+
providerRunnerActivationToFirstClaimDuration,
|
|
17
|
+
providerRunnerAssignmentRejectedCount,
|
|
18
|
+
providerRunnerAssignmentToActivationDuration,
|
|
19
|
+
providerRunnerControlSessionToAssignmentDuration,
|
|
7
20
|
providerRunnerCountDivergenceCount,
|
|
21
|
+
providerRunnerCreatedToControlSessionDuration,
|
|
8
22
|
providerRunnerReconcileCallCount,
|
|
9
23
|
providerRunnerTerminateIntentHonoredCount,
|
|
10
24
|
providerRunnerTerminateIntentIssuedCount,
|
|
25
|
+
recordJobExecutionQueueTime,
|
|
26
|
+
recordProviderRunnerActivationOutcome,
|
|
27
|
+
recordProviderRunnerActivationToFirstClaim,
|
|
28
|
+
recordProviderRunnerAssignmentRejected,
|
|
29
|
+
recordProviderRunnerAssignmentToActivation,
|
|
30
|
+
recordProviderRunnerControlSessionToAssignment,
|
|
31
|
+
recordProviderRunnerCreatedToControlSession,
|
|
32
|
+
recordRunnerActivationTokenNotIssued,
|
|
33
|
+
recordRunnerReservationCapacityFailure,
|
|
11
34
|
recordRunnerReservationPromotionFailure,
|
|
12
35
|
recordRunnersRateLimitCheck,
|
|
13
36
|
recordRunnersRateLimitPruneFailure,
|