@shipfox/api-runners 12.4.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 +14 -0
- 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-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +62 -12
- package/dist/core/runner-control-sessions.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/runner-assignments.d.ts +8 -1
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +57 -10
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +9 -0
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +43 -0
- 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/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 +48 -0
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +138 -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/routes/assign-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/assign-runner-instances.js +7 -1
- package/dist/presentation/routes/assign-runner-instances.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +5 -5
- package/src/core/errors.ts +30 -1
- package/src/core/runner-control-sessions.test.ts +71 -0
- package/src/core/runner-control-sessions.ts +79 -10
- package/src/db/job-executions.test.ts +98 -12
- package/src/db/job-executions.ts +52 -4
- package/src/db/runner-assignments.ts +67 -16
- package/src/db/runner-instances.test.ts +92 -0
- package/src/db/runner-instances.ts +74 -0
- package/src/db/runner-sessions.ts +19 -1
- package/src/metrics/index.ts +14 -0
- package/src/metrics/instance.test.ts +118 -0
- package/src/metrics/instance.ts +188 -1
- package/src/metrics/service.test.ts +38 -0
- package/src/metrics/service.ts +55 -8
- package/src/presentation/routes/assign-runner-instances.ts +8 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -5,6 +5,11 @@ import {
|
|
|
5
5
|
RunnerInstanceAlreadyAssignedError,
|
|
6
6
|
RunnerInstanceNotAssignableError,
|
|
7
7
|
} from '#core/errors.js';
|
|
8
|
+
import {
|
|
9
|
+
type ProviderRunnerAssignmentObservation,
|
|
10
|
+
type RunnerAssignmentSurface,
|
|
11
|
+
recordProviderRunnerControlSessionToAssignment,
|
|
12
|
+
} from '#metrics/instance.js';
|
|
8
13
|
import type {Tx} from './db.js';
|
|
9
14
|
import {db} from './db.js';
|
|
10
15
|
import {lockRunnerReservationAdvisoryKeysTx} from './reservation-locks.js';
|
|
@@ -19,7 +24,17 @@ export async function assignRunnerInstances(params: {
|
|
|
19
24
|
reservationId: string;
|
|
20
25
|
runnerInstanceIds: string[];
|
|
21
26
|
}): Promise<string[]> {
|
|
22
|
-
|
|
27
|
+
const result = await db().transaction(async (tx) =>
|
|
28
|
+
assignRunnerInstancesTx(tx, {...params, surface: 'provisioner'}),
|
|
29
|
+
);
|
|
30
|
+
for (const observation of result.controlSessionToAssignment)
|
|
31
|
+
recordProviderRunnerControlSessionToAssignment(observation);
|
|
32
|
+
return result.runnerInstanceIds;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface AssignRunnerInstancesTxResult {
|
|
36
|
+
runnerInstanceIds: string[];
|
|
37
|
+
controlSessionToAssignment: ProviderRunnerAssignmentObservation[];
|
|
23
38
|
}
|
|
24
39
|
|
|
25
40
|
export async function assignRunnerInstancesTx(
|
|
@@ -28,8 +43,9 @@ export async function assignRunnerInstancesTx(
|
|
|
28
43
|
provisionerId: string;
|
|
29
44
|
reservationId: string;
|
|
30
45
|
runnerInstanceIds: string[];
|
|
46
|
+
surface: RunnerAssignmentSurface;
|
|
31
47
|
},
|
|
32
|
-
): Promise<
|
|
48
|
+
): Promise<AssignRunnerInstancesTxResult> {
|
|
33
49
|
const runnerInstanceIds = [...params.runnerInstanceIds].sort();
|
|
34
50
|
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
35
51
|
provisionerId: params.provisionerId,
|
|
@@ -43,6 +59,8 @@ export async function assignRunnerInstancesTx(
|
|
|
43
59
|
workspaceId: providerRunners.workspaceId,
|
|
44
60
|
assignedAt: providerRunners.assignedAt,
|
|
45
61
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
62
|
+
provider: providerRunners.providerKind,
|
|
63
|
+
launchKind: providerRunners.launchKind,
|
|
46
64
|
labels: providerRunners.labels,
|
|
47
65
|
state: providerRunners.state,
|
|
48
66
|
})
|
|
@@ -76,7 +94,10 @@ export async function assignRunnerInstancesTx(
|
|
|
76
94
|
inArray(providerRunners.id, runnerInstanceIds),
|
|
77
95
|
),
|
|
78
96
|
);
|
|
79
|
-
return
|
|
97
|
+
return {
|
|
98
|
+
runnerInstanceIds: params.runnerInstanceIds,
|
|
99
|
+
controlSessionToAssignment: [],
|
|
100
|
+
};
|
|
80
101
|
}
|
|
81
102
|
|
|
82
103
|
const [reservation] = await tx
|
|
@@ -94,10 +115,13 @@ export async function assignRunnerInstancesTx(
|
|
|
94
115
|
if (!reservation) throw new ReservationNotFoundError(params.reservationId);
|
|
95
116
|
if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);
|
|
96
117
|
if (runnerRows.length !== runnerInstanceIds.length)
|
|
97
|
-
throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');
|
|
118
|
+
throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '', 'runner-not-found');
|
|
98
119
|
|
|
99
120
|
const activeControlSessions = await tx
|
|
100
|
-
.select({
|
|
121
|
+
.select({
|
|
122
|
+
runnerInstanceId: runnerControlSessions.runnerInstanceId,
|
|
123
|
+
createdAt: runnerControlSessions.createdAt,
|
|
124
|
+
})
|
|
101
125
|
.from(runnerControlSessions)
|
|
102
126
|
.where(
|
|
103
127
|
and(
|
|
@@ -111,9 +135,13 @@ export async function assignRunnerInstancesTx(
|
|
|
111
135
|
const runnerInstanceIdsWithControlSession = new Set(
|
|
112
136
|
activeControlSessions.map((session) => session.runnerInstanceId),
|
|
113
137
|
);
|
|
138
|
+
const controlSessionCreatedAtByRunner = new Map(
|
|
139
|
+
activeControlSessions.map((session) => [session.runnerInstanceId, session.createdAt]),
|
|
140
|
+
);
|
|
114
141
|
const runners = runnerRows.map((runner) => ({
|
|
115
142
|
...runner,
|
|
116
143
|
controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,
|
|
144
|
+
controlSessionCreatedAt: controlSessionCreatedAtByRunner.get(runner.id) ?? null,
|
|
117
145
|
}));
|
|
118
146
|
|
|
119
147
|
const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);
|
|
@@ -147,18 +175,19 @@ export async function assignRunnerInstancesTx(
|
|
|
147
175
|
),
|
|
148
176
|
);
|
|
149
177
|
if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)
|
|
150
|
-
throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');
|
|
178
|
+
throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '', 'capacity-exhausted');
|
|
151
179
|
for (const runner of newRunners) {
|
|
152
|
-
if (
|
|
153
|
-
runner.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
180
|
+
if (runner.state !== 'running')
|
|
181
|
+
throw new RunnerInstanceNotAssignableError(runner.id, 'runner-not-running');
|
|
182
|
+
if (!runner.providerRunnerId)
|
|
183
|
+
throw new RunnerInstanceNotAssignableError(runner.id, 'provider-identity-missing');
|
|
184
|
+
if (!runner.controlSessionId)
|
|
185
|
+
throw new RunnerInstanceNotAssignableError(runner.id, 'control-session-not-active');
|
|
186
|
+
if (!reservation.requiredLabels.every((label) => runner.labels.includes(label)))
|
|
187
|
+
throw new RunnerInstanceNotAssignableError(runner.id, 'labels-mismatch');
|
|
159
188
|
}
|
|
160
189
|
if (newRunners.length > 0) {
|
|
161
|
-
await tx
|
|
190
|
+
const assignedRows = await tx
|
|
162
191
|
.update(providerRunners)
|
|
163
192
|
.set({
|
|
164
193
|
workspaceId: reservation.workspaceId,
|
|
@@ -172,9 +201,31 @@ export async function assignRunnerInstancesTx(
|
|
|
172
201
|
providerRunners.id,
|
|
173
202
|
newRunners.map((runner) => runner.id),
|
|
174
203
|
),
|
|
175
|
-
)
|
|
204
|
+
)
|
|
205
|
+
.returning({id: providerRunners.id, assignedAt: providerRunners.assignedAt});
|
|
206
|
+
const runnersById = new Map(newRunners.map((runner) => [runner.id, runner]));
|
|
207
|
+
const controlSessionToAssignment: ProviderRunnerAssignmentObservation[] = [];
|
|
208
|
+
for (const assigned of assignedRows) {
|
|
209
|
+
const runner = runnersById.get(assigned.id);
|
|
210
|
+
const controlSessionCreatedAt = runner?.controlSessionCreatedAt;
|
|
211
|
+
if (!runner || !assigned.assignedAt || !controlSessionCreatedAt) continue;
|
|
212
|
+
controlSessionToAssignment.push({
|
|
213
|
+
durationMilliseconds: assigned.assignedAt.getTime() - controlSessionCreatedAt.getTime(),
|
|
214
|
+
provider: runner.provider,
|
|
215
|
+
launchKind: runner.launchKind,
|
|
216
|
+
surface: params.surface,
|
|
217
|
+
runnerInstanceId: runner.id,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return {
|
|
221
|
+
runnerInstanceIds: params.runnerInstanceIds,
|
|
222
|
+
controlSessionToAssignment,
|
|
223
|
+
};
|
|
176
224
|
}
|
|
177
|
-
return
|
|
225
|
+
return {
|
|
226
|
+
runnerInstanceIds: params.runnerInstanceIds,
|
|
227
|
+
controlSessionToAssignment: [],
|
|
228
|
+
};
|
|
178
229
|
}
|
|
179
230
|
|
|
180
231
|
export type RunnerReservationCapacityFailureReason =
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
countStaleEnrolledRunnerInstances,
|
|
8
8
|
listActiveRunnerInstanceCountsByTemplateTx,
|
|
9
9
|
listActiveRunnerInstances,
|
|
10
|
+
listProviderRunnerByPhaseMetrics,
|
|
10
11
|
listProvisionerTerminateIntentRowsTx,
|
|
11
12
|
listProvisionerTerminateIntents,
|
|
12
13
|
reapStaleRunnerInstances,
|
|
@@ -2540,6 +2541,97 @@ describe('countStaleEnrolledRunnerInstances', () => {
|
|
|
2540
2541
|
}
|
|
2541
2542
|
});
|
|
2542
2543
|
|
|
2544
|
+
describe('listProviderRunnerByPhaseMetrics', () => {
|
|
2545
|
+
it('groups runners by the lifecycle phase that is currently blocking activation', async () => {
|
|
2546
|
+
const provisionerId = crypto.randomUUID();
|
|
2547
|
+
const provider = `metrics-test-${crypto.randomUUID()}`;
|
|
2548
|
+
const reservationId = crypto.randomUUID();
|
|
2549
|
+
const workspaceId = crypto.randomUUID();
|
|
2550
|
+
const old = new Date(Date.now() - 120_000);
|
|
2551
|
+
|
|
2552
|
+
const createRunner = async (params: {
|
|
2553
|
+
state: 'starting' | 'running';
|
|
2554
|
+
launchKind: 'demand' | 'warm' | 'manual';
|
|
2555
|
+
intendedReservationId?: string | null;
|
|
2556
|
+
workspaceId?: string | null;
|
|
2557
|
+
reservationId?: string | null;
|
|
2558
|
+
assignedAt?: Date | null;
|
|
2559
|
+
createdAt?: Date;
|
|
2560
|
+
}) => {
|
|
2561
|
+
const [runner] = await db()
|
|
2562
|
+
.insert(providerRunners)
|
|
2563
|
+
.values({
|
|
2564
|
+
provisionerId,
|
|
2565
|
+
providerRunnerId: crypto.randomUUID(),
|
|
2566
|
+
providerKind: provider,
|
|
2567
|
+
launchKind: params.launchKind,
|
|
2568
|
+
intendedReservationId: params.intendedReservationId ?? null,
|
|
2569
|
+
workspaceId: params.workspaceId ?? null,
|
|
2570
|
+
reservationId: params.reservationId ?? null,
|
|
2571
|
+
assignedAt: params.assignedAt ?? null,
|
|
2572
|
+
state: params.state,
|
|
2573
|
+
labels: ['linux'],
|
|
2574
|
+
reportedAt: new Date(),
|
|
2575
|
+
createdAt: params.createdAt ?? old,
|
|
2576
|
+
})
|
|
2577
|
+
.returning({id: providerRunners.id});
|
|
2578
|
+
if (!runner) throw new Error('Expected runner instance');
|
|
2579
|
+
return runner.id;
|
|
2580
|
+
};
|
|
2581
|
+
|
|
2582
|
+
await createRunner({state: 'starting', launchKind: 'demand'});
|
|
2583
|
+
const enrollmentRunnerId = await createRunner({state: 'starting', launchKind: 'warm'});
|
|
2584
|
+
const assignmentRunnerId = await createRunner({
|
|
2585
|
+
state: 'running',
|
|
2586
|
+
launchKind: 'demand',
|
|
2587
|
+
intendedReservationId: reservationId,
|
|
2588
|
+
});
|
|
2589
|
+
const activationRunnerId = await createRunner({
|
|
2590
|
+
state: 'running',
|
|
2591
|
+
launchKind: 'manual',
|
|
2592
|
+
workspaceId,
|
|
2593
|
+
reservationId,
|
|
2594
|
+
assignedAt: old,
|
|
2595
|
+
});
|
|
2596
|
+
const idleRunnerId = await createRunner({state: 'running', launchKind: 'warm'});
|
|
2597
|
+
const demandIdleRunnerId = await createRunner({state: 'running', launchKind: 'demand'});
|
|
2598
|
+
|
|
2599
|
+
await db()
|
|
2600
|
+
.insert(runnerControlSessions)
|
|
2601
|
+
.values(
|
|
2602
|
+
[
|
|
2603
|
+
enrollmentRunnerId,
|
|
2604
|
+
assignmentRunnerId,
|
|
2605
|
+
activationRunnerId,
|
|
2606
|
+
idleRunnerId,
|
|
2607
|
+
demandIdleRunnerId,
|
|
2608
|
+
].map((runnerInstanceId) => ({
|
|
2609
|
+
runnerInstanceId,
|
|
2610
|
+
provisionerId,
|
|
2611
|
+
hashedToken: crypto.randomUUID(),
|
|
2612
|
+
prefix: 'metrics-test',
|
|
2613
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
2614
|
+
createdAt: old,
|
|
2615
|
+
})),
|
|
2616
|
+
);
|
|
2617
|
+
|
|
2618
|
+
const metrics = await listProviderRunnerByPhaseMetrics();
|
|
2619
|
+
const ownMetrics = metrics.filter((metric) => metric.provider === provider);
|
|
2620
|
+
|
|
2621
|
+
expect(ownMetrics).toEqual(
|
|
2622
|
+
expect.arrayContaining([
|
|
2623
|
+
expect.objectContaining({phase: 'control_session', launchKind: 'demand', count: 1}),
|
|
2624
|
+
expect.objectContaining({phase: 'enrollment', launchKind: 'warm', count: 1}),
|
|
2625
|
+
expect.objectContaining({phase: 'assignment', launchKind: 'demand', count: 1}),
|
|
2626
|
+
expect.objectContaining({phase: 'activation', launchKind: 'manual', count: 1}),
|
|
2627
|
+
expect.objectContaining({phase: 'idle', launchKind: 'warm', count: 1}),
|
|
2628
|
+
expect.objectContaining({phase: 'idle', launchKind: 'demand', count: 1}),
|
|
2629
|
+
]),
|
|
2630
|
+
);
|
|
2631
|
+
expect(ownMetrics).toHaveLength(6);
|
|
2632
|
+
});
|
|
2633
|
+
});
|
|
2634
|
+
|
|
2543
2635
|
describe('runner instance provider attachment', () => {
|
|
2544
2636
|
it('updates the pre-created runner instance before its first lifecycle report', async () => {
|
|
2545
2637
|
const provisionerId = crypto.randomUUID();
|
|
@@ -375,6 +375,80 @@ export async function countStaleEnrolledRunnerInstances(params: {
|
|
|
375
375
|
return row?.count ?? 0;
|
|
376
376
|
}
|
|
377
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
|
+
|
|
378
452
|
export async function listActiveRunnerInstances(params: {
|
|
379
453
|
workspaceId: string;
|
|
380
454
|
windowSeconds: number;
|
|
@@ -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 {
|
package/src/metrics/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export type {
|
|
2
2
|
RunnerActivationTokenNotIssuedReason,
|
|
3
3
|
RunnerActivationTokenNotIssuedSurface,
|
|
4
|
+
RunnerAssignmentSurface,
|
|
5
|
+
RunnerLaunchKind,
|
|
4
6
|
RunnerReservationCapacityFailureReason,
|
|
5
7
|
RunnerReservationPromotionFailureReason,
|
|
6
8
|
} from './instance.js';
|
|
@@ -8,13 +10,25 @@ export {
|
|
|
8
10
|
jobExecutionClaimedCount,
|
|
9
11
|
jobExecutionEnqueuedCount,
|
|
10
12
|
jobExecutionLeaseExpiredCount,
|
|
13
|
+
jobExecutionQueueTimeDuration,
|
|
11
14
|
providerRunnerAbsentTerminatedCount,
|
|
12
15
|
providerRunnerActivationOutcomeCount,
|
|
16
|
+
providerRunnerActivationToFirstClaimDuration,
|
|
17
|
+
providerRunnerAssignmentRejectedCount,
|
|
18
|
+
providerRunnerAssignmentToActivationDuration,
|
|
19
|
+
providerRunnerControlSessionToAssignmentDuration,
|
|
13
20
|
providerRunnerCountDivergenceCount,
|
|
21
|
+
providerRunnerCreatedToControlSessionDuration,
|
|
14
22
|
providerRunnerReconcileCallCount,
|
|
15
23
|
providerRunnerTerminateIntentHonoredCount,
|
|
16
24
|
providerRunnerTerminateIntentIssuedCount,
|
|
25
|
+
recordJobExecutionQueueTime,
|
|
17
26
|
recordProviderRunnerActivationOutcome,
|
|
27
|
+
recordProviderRunnerActivationToFirstClaim,
|
|
28
|
+
recordProviderRunnerAssignmentRejected,
|
|
29
|
+
recordProviderRunnerAssignmentToActivation,
|
|
30
|
+
recordProviderRunnerControlSessionToAssignment,
|
|
31
|
+
recordProviderRunnerCreatedToControlSession,
|
|
18
32
|
recordRunnerActivationTokenNotIssued,
|
|
19
33
|
recordRunnerReservationCapacityFailure,
|
|
20
34
|
recordRunnerReservationPromotionFailure,
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const metricMocks = vi.hoisted(() => {
|
|
2
|
+
const histograms = new Map<string, {record: ReturnType<typeof vi.fn>}>();
|
|
3
|
+
const counters = new Map<string, {add: ReturnType<typeof vi.fn>}>();
|
|
4
|
+
const createHistogram = vi.fn((name: string) => {
|
|
5
|
+
const histogram = {record: vi.fn()};
|
|
6
|
+
histograms.set(name, histogram);
|
|
7
|
+
return histogram;
|
|
8
|
+
});
|
|
9
|
+
const createCounter = vi.fn((name: string) => {
|
|
10
|
+
const counter = {add: vi.fn()};
|
|
11
|
+
counters.set(name, counter);
|
|
12
|
+
return counter;
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return {counters, createCounter, createHistogram, histograms};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
vi.mock('@shipfox/node-opentelemetry', () => ({
|
|
19
|
+
instanceMetrics: {
|
|
20
|
+
getMeter: () => ({
|
|
21
|
+
createCounter: metricMocks.createCounter,
|
|
22
|
+
createHistogram: metricMocks.createHistogram,
|
|
23
|
+
}),
|
|
24
|
+
},
|
|
25
|
+
logger: () => ({debug: vi.fn()}),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
let metrics: typeof import('./instance.js');
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
vi.resetModules();
|
|
32
|
+
metricMocks.counters.clear();
|
|
33
|
+
metricMocks.createCounter.mockClear();
|
|
34
|
+
metricMocks.createHistogram.mockClear();
|
|
35
|
+
metricMocks.histograms.clear();
|
|
36
|
+
metrics = await import('./instance.js');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('runner lifecycle metrics', () => {
|
|
40
|
+
it('defines provider-runner histograms with millisecond units and buckets', () => {
|
|
41
|
+
const calls = metricMocks.createHistogram.mock.calls as unknown as Array<
|
|
42
|
+
[string, {unit?: string; advice?: {explicitBucketBoundaries?: number[]}}]
|
|
43
|
+
>;
|
|
44
|
+
|
|
45
|
+
expect(
|
|
46
|
+
calls
|
|
47
|
+
.filter(([name]) => name.startsWith('runners_provider_runner_'))
|
|
48
|
+
.map(([name, options]) => ({
|
|
49
|
+
name,
|
|
50
|
+
unit: options.unit,
|
|
51
|
+
buckets: options.advice?.explicitBucketBoundaries,
|
|
52
|
+
})),
|
|
53
|
+
).toEqual([
|
|
54
|
+
{
|
|
55
|
+
name: 'runners_provider_runner_created_to_control_session',
|
|
56
|
+
unit: 'ms',
|
|
57
|
+
buckets: [
|
|
58
|
+
100, 500, 1_000, 5_000, 10_000, 15_000, 20_000, 30_000, 45_000, 60_000, 90_000, 120_000,
|
|
59
|
+
300_000, 600_000,
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'runners_provider_runner_control_session_to_assignment',
|
|
64
|
+
unit: 'ms',
|
|
65
|
+
buckets: [
|
|
66
|
+
100, 500, 1_000, 5_000, 10_000, 15_000, 20_000, 30_000, 45_000, 60_000, 90_000, 120_000,
|
|
67
|
+
300_000, 600_000,
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
name: 'runners_provider_runner_assignment_to_activation',
|
|
72
|
+
unit: 'ms',
|
|
73
|
+
buckets: [10, 25, 50, 100, 250, 500, 1_000, 5_000, 10_000],
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: 'runners_provider_runner_activation_to_first_claim',
|
|
77
|
+
unit: 'ms',
|
|
78
|
+
buckets: [10, 25, 50, 100, 250, 500, 1_000, 5_000, 10_000],
|
|
79
|
+
},
|
|
80
|
+
]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('records lifecycle durations without converting milliseconds', () => {
|
|
84
|
+
metrics.recordProviderRunnerCreatedToControlSession({
|
|
85
|
+
durationMilliseconds: 1_234,
|
|
86
|
+
provider: 'ec2',
|
|
87
|
+
launchKind: 'demand',
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(
|
|
91
|
+
metricMocks.histograms.get('runners_provider_runner_created_to_control_session')?.record,
|
|
92
|
+
).toHaveBeenCalledWith(1_234, {provider: 'ec2', launch_kind: 'demand'});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('job execution queue time metrics', () => {
|
|
97
|
+
it('records milliseconds with bounded labels and ignores negative durations', () => {
|
|
98
|
+
const record = metricMocks.histograms.get('runners_job_execution_queue_time')?.record;
|
|
99
|
+
expect(record).toBeDefined();
|
|
100
|
+
|
|
101
|
+
metrics.recordJobExecutionQueueTime({
|
|
102
|
+
durationMilliseconds: 1_234,
|
|
103
|
+
provider: null,
|
|
104
|
+
launchKind: 'manual',
|
|
105
|
+
});
|
|
106
|
+
metrics.recordJobExecutionQueueTime({
|
|
107
|
+
durationMilliseconds: -1,
|
|
108
|
+
provider: 'ec2',
|
|
109
|
+
launchKind: 'demand',
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(record).toHaveBeenCalledTimes(1);
|
|
113
|
+
expect(record).toHaveBeenCalledWith(1_234, {
|
|
114
|
+
provider: 'unknown',
|
|
115
|
+
launch_kind: 'manual',
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|