@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
|
@@ -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
|
+
});
|
package/src/metrics/instance.ts
CHANGED
|
@@ -1,7 +1,116 @@
|
|
|
1
|
-
import {instanceMetrics} from '@shipfox/node-opentelemetry';
|
|
1
|
+
import {instanceMetrics, logger} from '@shipfox/node-opentelemetry';
|
|
2
|
+
import type {RunnerAssignmentRejectionReason} from '#core/errors.js';
|
|
2
3
|
|
|
3
4
|
const meter = instanceMetrics.getMeter('runners');
|
|
4
5
|
|
|
6
|
+
export type RunnerLaunchKind = 'demand' | 'warm' | 'manual';
|
|
7
|
+
export type RunnerAssignmentSurface = 'provisioner' | 'enrollment';
|
|
8
|
+
|
|
9
|
+
type ProviderRunnerLifecycleLabels = {
|
|
10
|
+
provider: string;
|
|
11
|
+
launch_kind: RunnerLaunchKind;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ProviderRunnerAssignmentLifecycleLabels = ProviderRunnerLifecycleLabels & {
|
|
15
|
+
surface: RunnerAssignmentSurface;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type JobExecutionQueueTimeLabels = {
|
|
19
|
+
provider: string;
|
|
20
|
+
launch_kind: RunnerLaunchKind | 'unknown';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface JobExecutionQueueTimeObservation {
|
|
24
|
+
durationMilliseconds: number;
|
|
25
|
+
provider: string | null;
|
|
26
|
+
launchKind: RunnerLaunchKind | 'unknown';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ProviderRunnerLifecycleObservation {
|
|
30
|
+
durationMilliseconds: number;
|
|
31
|
+
provider: string | null;
|
|
32
|
+
launchKind: RunnerLaunchKind;
|
|
33
|
+
runnerInstanceId?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ProviderRunnerAssignmentObservation extends ProviderRunnerLifecycleObservation {
|
|
37
|
+
surface: RunnerAssignmentSurface;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const UNKNOWN_PROVIDER_KIND = 'unknown';
|
|
41
|
+
|
|
42
|
+
const lifecycleDurationBuckets = {
|
|
43
|
+
long: [
|
|
44
|
+
100, 500, 1_000, 5_000, 10_000, 15_000, 20_000, 30_000, 45_000, 60_000, 90_000, 120_000,
|
|
45
|
+
300_000, 600_000,
|
|
46
|
+
],
|
|
47
|
+
short: [10, 25, 50, 100, 250, 500, 1_000, 5_000, 10_000],
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const queueTimeBucketsMilliseconds = [
|
|
51
|
+
100, 500, 1_000, 5_000, 10_000, 15_000, 20_000, 30_000, 45_000, 60_000, 90_000, 120_000, 300_000,
|
|
52
|
+
600_000, 900_000, 1_800_000, 3_600_000, 7_200_000, 14_400_000,
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
// Keep the ordered lifecycle phases as separate histograms so long boot times do not flatten
|
|
56
|
+
// short handoffs. Assignment is the only phase with a surface dimension.
|
|
57
|
+
export const providerRunnerCreatedToControlSessionDuration =
|
|
58
|
+
meter.createHistogram<ProviderRunnerLifecycleLabels>(
|
|
59
|
+
'runners_provider_runner_created_to_control_session',
|
|
60
|
+
{
|
|
61
|
+
description: 'Provider runner row creation to control-session creation duration',
|
|
62
|
+
unit: 'ms',
|
|
63
|
+
advice: {explicitBucketBoundaries: lifecycleDurationBuckets.long},
|
|
64
|
+
},
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
export const providerRunnerControlSessionToAssignmentDuration =
|
|
68
|
+
meter.createHistogram<ProviderRunnerAssignmentLifecycleLabels>(
|
|
69
|
+
'runners_provider_runner_control_session_to_assignment',
|
|
70
|
+
{
|
|
71
|
+
description: 'Provider runner control-session creation to reservation assignment duration',
|
|
72
|
+
unit: 'ms',
|
|
73
|
+
advice: {explicitBucketBoundaries: lifecycleDurationBuckets.long},
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
export const providerRunnerAssignmentToActivationDuration =
|
|
78
|
+
meter.createHistogram<ProviderRunnerLifecycleLabels>(
|
|
79
|
+
'runners_provider_runner_assignment_to_activation',
|
|
80
|
+
{
|
|
81
|
+
description:
|
|
82
|
+
'Provider runner reservation assignment to workspace runner-session creation duration',
|
|
83
|
+
unit: 'ms',
|
|
84
|
+
advice: {explicitBucketBoundaries: lifecycleDurationBuckets.short},
|
|
85
|
+
},
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
export const providerRunnerActivationToFirstClaimDuration =
|
|
89
|
+
meter.createHistogram<ProviderRunnerLifecycleLabels>(
|
|
90
|
+
'runners_provider_runner_activation_to_first_claim',
|
|
91
|
+
{
|
|
92
|
+
description: 'Provider runner workspace runner-session creation to first job claim duration',
|
|
93
|
+
unit: 'ms',
|
|
94
|
+
advice: {explicitBucketBoundaries: lifecycleDurationBuckets.short},
|
|
95
|
+
},
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
export const jobExecutionQueueTimeDuration = meter.createHistogram<JobExecutionQueueTimeLabels>(
|
|
99
|
+
'runners_job_execution_queue_time',
|
|
100
|
+
{
|
|
101
|
+
description: 'Job execution pending queue duration from enqueue to runner claim',
|
|
102
|
+
unit: 'ms',
|
|
103
|
+
advice: {explicitBucketBoundaries: queueTimeBucketsMilliseconds},
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
export const providerRunnerAssignmentRejectedCount = meter.createCounter<{
|
|
108
|
+
reason: RunnerAssignmentRejectionReason;
|
|
109
|
+
surface: RunnerAssignmentSurface;
|
|
110
|
+
}>('runners_provider_runner_assignment_rejected', {
|
|
111
|
+
description: 'Provider runner assignment operations rejected by bounded reason and surface',
|
|
112
|
+
});
|
|
113
|
+
|
|
5
114
|
export const jobExecutionEnqueuedCount = meter.createCounter<Record<string, never>>(
|
|
6
115
|
'runners_job_execution_enqueued',
|
|
7
116
|
{
|
|
@@ -31,6 +140,21 @@ export const runnerBootstrapExchangeCount = meter.createCounter<{
|
|
|
31
140
|
description: 'Runner bootstrap-token exchanges by outcome',
|
|
32
141
|
});
|
|
33
142
|
|
|
143
|
+
export type RunnerActivationTokenNotIssuedReason =
|
|
144
|
+
| 'runner-not-found'
|
|
145
|
+
| 'missing-workspace'
|
|
146
|
+
| 'existing-session'
|
|
147
|
+
| 'not-running';
|
|
148
|
+
|
|
149
|
+
export type RunnerActivationTokenNotIssuedSurface = 'enrollment' | 'poll';
|
|
150
|
+
|
|
151
|
+
export const runnerActivationTokenNotIssuedCount = meter.createCounter<{
|
|
152
|
+
reason: RunnerActivationTokenNotIssuedReason;
|
|
153
|
+
surface: RunnerActivationTokenNotIssuedSurface;
|
|
154
|
+
}>('runners_runner_activation_token_not_issued', {
|
|
155
|
+
description: 'Runner activation token issuance skips by reason and surface',
|
|
156
|
+
});
|
|
157
|
+
|
|
34
158
|
export const runnerControlHeartbeatCount = meter.createCounter<Record<string, never>>(
|
|
35
159
|
'runners_runner_control_heartbeat',
|
|
36
160
|
{description: 'Pre-workspace runner-control heartbeats accepted'},
|
|
@@ -67,17 +191,23 @@ export const providerRunnerAbsentTerminatedCount = meter.createCounter<Record<st
|
|
|
67
191
|
|
|
68
192
|
export const providerRunnerTerminateIntentIssuedCount = meter.createCounter<{
|
|
69
193
|
surface: 'poll-demand' | 'reconcile';
|
|
70
|
-
reason: 'job-cancelled' | 'terminal-state';
|
|
194
|
+
reason: 'activation-timeout' | 'job-cancelled' | 'terminal-state';
|
|
71
195
|
}>('runners_provider_runner_terminate_intent_issued', {
|
|
72
196
|
description: 'Provisioned runner terminate intents returned to provisioners',
|
|
73
197
|
});
|
|
74
198
|
|
|
75
199
|
export const providerRunnerTerminateIntentHonoredCount = meter.createCounter<{
|
|
76
|
-
reason: 'job-cancelled';
|
|
200
|
+
reason: 'activation-timeout' | 'job-cancelled';
|
|
77
201
|
}>('runners_provider_runner_terminate_intent_honored', {
|
|
78
202
|
description: 'Provisioned runner terminate intents honored by first transition to terminated',
|
|
79
203
|
});
|
|
80
204
|
|
|
205
|
+
export const providerRunnerActivationOutcomeCount = meter.createCounter<{
|
|
206
|
+
outcome: 'reaped' | 'rebound';
|
|
207
|
+
}>('runners_provider_runner_activation_outcome', {
|
|
208
|
+
description: 'Demand-backed runner activation outcomes by recovery action',
|
|
209
|
+
});
|
|
210
|
+
|
|
81
211
|
export const reservationReleasedCount = meter.createCounter<Record<string, never>>(
|
|
82
212
|
'runners_reservation_released',
|
|
83
213
|
{description: 'Reservation units released from terminal provisioned runner reports'},
|
|
@@ -95,6 +225,18 @@ export const runnerReservationPromotionFailureCount = meter.createCounter<{
|
|
|
95
225
|
description: 'Runner reservation promotion failures during enrollment by reason',
|
|
96
226
|
});
|
|
97
227
|
|
|
228
|
+
export type RunnerReservationCapacityFailureReason =
|
|
229
|
+
| 'reservation-not-found'
|
|
230
|
+
| 'reservation-kind-mismatch'
|
|
231
|
+
| 'reservation-expired'
|
|
232
|
+
| 'capacity-exhausted';
|
|
233
|
+
|
|
234
|
+
export const runnerReservationCapacityFailureCount = meter.createCounter<{
|
|
235
|
+
reason: RunnerReservationCapacityFailureReason;
|
|
236
|
+
}>('runners_reservation_capacity_failures', {
|
|
237
|
+
description: 'Runner reservation admission shortfalls by reason',
|
|
238
|
+
});
|
|
239
|
+
|
|
98
240
|
export type RunnersRateLimitAction = 'provisioner-mint' | 'ephemeral-register';
|
|
99
241
|
export type RunnersRateLimitScope = 'provisioner' | 'ephemeral-token';
|
|
100
242
|
export type RunnersRateLimitOutcome = 'allowed' | 'blocked' | 'unavailable';
|
|
@@ -119,12 +261,105 @@ function recordMetric(record: () => void): void {
|
|
|
119
261
|
}
|
|
120
262
|
}
|
|
121
263
|
|
|
264
|
+
function resolveProviderRunnerMetricProvider(params: {
|
|
265
|
+
provider: string | null;
|
|
266
|
+
runnerInstanceId?: string;
|
|
267
|
+
}): string {
|
|
268
|
+
if (params.provider) return params.provider;
|
|
269
|
+
logger().debug(
|
|
270
|
+
{runnerInstanceId: params.runnerInstanceId},
|
|
271
|
+
'Provider runner metric missing provider kind',
|
|
272
|
+
);
|
|
273
|
+
return UNKNOWN_PROVIDER_KIND;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
export function recordProviderRunnerCreatedToControlSession(
|
|
277
|
+
params: ProviderRunnerLifecycleObservation,
|
|
278
|
+
): void {
|
|
279
|
+
if (params.durationMilliseconds < 0) return;
|
|
280
|
+
recordMetric(() =>
|
|
281
|
+
providerRunnerCreatedToControlSessionDuration.record(params.durationMilliseconds, {
|
|
282
|
+
provider: resolveProviderRunnerMetricProvider(params),
|
|
283
|
+
launch_kind: params.launchKind,
|
|
284
|
+
}),
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export function recordProviderRunnerControlSessionToAssignment(
|
|
289
|
+
params: ProviderRunnerAssignmentObservation,
|
|
290
|
+
): void {
|
|
291
|
+
if (params.durationMilliseconds < 0) return;
|
|
292
|
+
recordMetric(() =>
|
|
293
|
+
providerRunnerControlSessionToAssignmentDuration.record(params.durationMilliseconds, {
|
|
294
|
+
provider: resolveProviderRunnerMetricProvider(params),
|
|
295
|
+
launch_kind: params.launchKind,
|
|
296
|
+
surface: params.surface,
|
|
297
|
+
}),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function recordProviderRunnerAssignmentToActivation(
|
|
302
|
+
params: ProviderRunnerLifecycleObservation,
|
|
303
|
+
): void {
|
|
304
|
+
if (params.durationMilliseconds < 0) return;
|
|
305
|
+
recordMetric(() =>
|
|
306
|
+
providerRunnerAssignmentToActivationDuration.record(params.durationMilliseconds, {
|
|
307
|
+
provider: resolveProviderRunnerMetricProvider(params),
|
|
308
|
+
launch_kind: params.launchKind,
|
|
309
|
+
}),
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export function recordProviderRunnerActivationToFirstClaim(
|
|
314
|
+
params: ProviderRunnerLifecycleObservation,
|
|
315
|
+
): void {
|
|
316
|
+
if (params.durationMilliseconds < 0) return;
|
|
317
|
+
recordMetric(() =>
|
|
318
|
+
providerRunnerActivationToFirstClaimDuration.record(params.durationMilliseconds, {
|
|
319
|
+
provider: resolveProviderRunnerMetricProvider(params),
|
|
320
|
+
launch_kind: params.launchKind,
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function recordJobExecutionQueueTime(params: JobExecutionQueueTimeObservation): void {
|
|
326
|
+
if (params.durationMilliseconds < 0) return;
|
|
327
|
+
recordMetric(() =>
|
|
328
|
+
jobExecutionQueueTimeDuration.record(params.durationMilliseconds, {
|
|
329
|
+
provider: params.provider ?? UNKNOWN_PROVIDER_KIND,
|
|
330
|
+
launch_kind: params.launchKind,
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export function recordProviderRunnerAssignmentRejected(params: {
|
|
336
|
+
reason: RunnerAssignmentRejectionReason;
|
|
337
|
+
surface: RunnerAssignmentSurface;
|
|
338
|
+
}): void {
|
|
339
|
+
recordMetric(() => providerRunnerAssignmentRejectedCount.add(1, params));
|
|
340
|
+
}
|
|
341
|
+
|
|
122
342
|
export function recordRunnerReservationPromotionFailure(
|
|
123
343
|
reason: RunnerReservationPromotionFailureReason,
|
|
124
344
|
): void {
|
|
125
345
|
recordMetric(() => runnerReservationPromotionFailureCount.add(1, {reason}));
|
|
126
346
|
}
|
|
127
347
|
|
|
348
|
+
export function recordRunnerActivationTokenNotIssued(params: {
|
|
349
|
+
reason: RunnerActivationTokenNotIssuedReason;
|
|
350
|
+
surface: RunnerActivationTokenNotIssuedSurface;
|
|
351
|
+
}): void {
|
|
352
|
+
recordMetric(() => runnerActivationTokenNotIssuedCount.add(1, params));
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function recordRunnerReservationCapacityFailure(
|
|
356
|
+
reason: RunnerReservationCapacityFailureReason,
|
|
357
|
+
count: number,
|
|
358
|
+
): void {
|
|
359
|
+
if (count <= 0) return;
|
|
360
|
+
recordMetric(() => runnerReservationCapacityFailureCount.add(count, {reason}));
|
|
361
|
+
}
|
|
362
|
+
|
|
128
363
|
export function recordRunnersRateLimitCheck(params: {
|
|
129
364
|
action: RunnersRateLimitAction;
|
|
130
365
|
scope: RunnersRateLimitScope;
|
|
@@ -142,3 +377,12 @@ export function recordRunnersRateLimitCheck(params: {
|
|
|
142
377
|
export function recordRunnersRateLimitPruneFailure(): void {
|
|
143
378
|
recordMetric(() => rateLimitPruneFailureCount.add(1));
|
|
144
379
|
}
|
|
380
|
+
|
|
381
|
+
export function recordProviderRunnerActivationOutcome(params: {
|
|
382
|
+
outcome: 'reaped' | 'rebound';
|
|
383
|
+
count?: number;
|
|
384
|
+
}): void {
|
|
385
|
+
recordMetric(() =>
|
|
386
|
+
providerRunnerActivationOutcomeCount.add(params.count ?? 1, {outcome: params.outcome}),
|
|
387
|
+
);
|
|
388
|
+
}
|
|
@@ -2,11 +2,15 @@ const mocks = vi.hoisted(() => {
|
|
|
2
2
|
const gauges = {
|
|
3
3
|
enrolledRunnersWithoutRecentReport: {},
|
|
4
4
|
pendingJobExecutions: {},
|
|
5
|
+
providerRunnersByPhase: {},
|
|
6
|
+
providerRunnersByPhaseOldestAge: {},
|
|
5
7
|
runningJobExecutions: {},
|
|
6
8
|
};
|
|
7
9
|
const gaugeByName = {
|
|
8
10
|
runners_enrolled_without_recent_report: gauges.enrolledRunnersWithoutRecentReport,
|
|
9
11
|
runners_pending_job_executions: gauges.pendingJobExecutions,
|
|
12
|
+
runners_provider_runner_by_phase: gauges.providerRunnersByPhase,
|
|
13
|
+
runners_provider_runner_by_phase_oldest_age: gauges.providerRunnersByPhaseOldestAge,
|
|
10
14
|
runners_running_job_executions: gauges.runningJobExecutions,
|
|
11
15
|
};
|
|
12
16
|
return {
|
|
@@ -16,6 +20,7 @@ const mocks = vi.hoisted(() => {
|
|
|
16
20
|
gauges,
|
|
17
21
|
getMeter: vi.fn(),
|
|
18
22
|
getJobExecutionQueueDepth: vi.fn(),
|
|
23
|
+
listProviderRunnerByPhaseMetrics: vi.fn(),
|
|
19
24
|
getServiceMetricsProvider: vi.fn(),
|
|
20
25
|
};
|
|
21
26
|
});
|
|
@@ -31,9 +36,17 @@ vi.mock('#db/job-executions.js', () => ({
|
|
|
31
36
|
}));
|
|
32
37
|
vi.mock('#db/runner-instances.js', () => ({
|
|
33
38
|
countStaleEnrolledRunnerInstances: mocks.countStaleEnrolledRunnerInstances,
|
|
39
|
+
listProviderRunnerByPhaseMetrics: mocks.listProviderRunnerByPhaseMetrics,
|
|
34
40
|
}));
|
|
35
41
|
|
|
36
|
-
|
|
42
|
+
let registerRunnersServiceMetrics: typeof import('./service.js').registerRunnersServiceMetrics;
|
|
43
|
+
|
|
44
|
+
beforeAll(async () => {
|
|
45
|
+
// This package intentionally runs with isolate: false. Reset the shared module cache before
|
|
46
|
+
// importing the service so a previous file's real OpenTelemetry module cannot bypass this mock.
|
|
47
|
+
vi.resetModules();
|
|
48
|
+
({registerRunnersServiceMetrics} = await import('./service.js'));
|
|
49
|
+
});
|
|
37
50
|
|
|
38
51
|
describe('registerRunnersServiceMetrics', () => {
|
|
39
52
|
beforeEach(() => {
|
|
@@ -41,12 +54,14 @@ describe('registerRunnersServiceMetrics', () => {
|
|
|
41
54
|
mocks.countStaleEnrolledRunnerInstances.mockReset();
|
|
42
55
|
mocks.createObservableGauge.mockClear();
|
|
43
56
|
mocks.getJobExecutionQueueDepth.mockReset();
|
|
57
|
+
mocks.listProviderRunnerByPhaseMetrics.mockReset();
|
|
44
58
|
mocks.getMeter.mockReset();
|
|
45
59
|
mocks.getServiceMetricsProvider.mockReset();
|
|
46
60
|
mocks.getJobExecutionQueueDepth.mockResolvedValue({
|
|
47
61
|
pendingJobExecutions: 0,
|
|
48
62
|
runningJobExecutions: 0,
|
|
49
63
|
});
|
|
64
|
+
mocks.listProviderRunnerByPhaseMetrics.mockResolvedValue([]);
|
|
50
65
|
mocks.getMeter.mockReturnValue({
|
|
51
66
|
createObservableGauge: mocks.createObservableGauge,
|
|
52
67
|
addBatchObservableCallback: mocks.addBatchObservableCallback,
|
|
@@ -96,4 +111,34 @@ describe('registerRunnersServiceMetrics', () => {
|
|
|
96
111
|
expect(observer.observe).toHaveBeenCalledWith(mocks.gauges.runningJobExecutions, 4);
|
|
97
112
|
expect(observer.observe).toHaveBeenCalledTimes(2);
|
|
98
113
|
});
|
|
114
|
+
|
|
115
|
+
it('observes provider runners by lifecycle phase', async () => {
|
|
116
|
+
mocks.listProviderRunnerByPhaseMetrics.mockResolvedValue([
|
|
117
|
+
{
|
|
118
|
+
phase: 'assignment',
|
|
119
|
+
provider: 'ec2',
|
|
120
|
+
launchKind: 'demand',
|
|
121
|
+
count: 3,
|
|
122
|
+
oldestAgeMilliseconds: 42,
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
|
|
126
|
+
registerRunnersServiceMetrics();
|
|
127
|
+
const callback = mocks.addBatchObservableCallback.mock.calls[0]?.[0];
|
|
128
|
+
if (typeof callback !== 'function') throw new Error('Expected metrics callback');
|
|
129
|
+
const observer = {observe: vi.fn()};
|
|
130
|
+
|
|
131
|
+
await callback(observer);
|
|
132
|
+
|
|
133
|
+
expect(observer.observe).toHaveBeenCalledWith(mocks.gauges.providerRunnersByPhase, 3, {
|
|
134
|
+
phase: 'assignment',
|
|
135
|
+
provider: 'ec2',
|
|
136
|
+
launch_kind: 'demand',
|
|
137
|
+
});
|
|
138
|
+
expect(observer.observe).toHaveBeenCalledWith(
|
|
139
|
+
mocks.gauges.providerRunnersByPhaseOldestAge,
|
|
140
|
+
42,
|
|
141
|
+
{phase: 'assignment', provider: 'ec2', launch_kind: 'demand'},
|
|
142
|
+
);
|
|
143
|
+
});
|
|
99
144
|
});
|
package/src/metrics/service.ts
CHANGED
|
@@ -1,7 +1,17 @@
|
|
|
1
1
|
import {getServiceMetricsProvider} from '@shipfox/node-opentelemetry';
|
|
2
2
|
import {config} from '#config.js';
|
|
3
3
|
import {getJobExecutionQueueDepth} from '#db/job-executions.js';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
countStaleEnrolledRunnerInstances,
|
|
6
|
+
listProviderRunnerByPhaseMetrics,
|
|
7
|
+
type ProviderRunnerPhaseMetric,
|
|
8
|
+
} from '#db/runner-instances.js';
|
|
9
|
+
|
|
10
|
+
type ProviderRunnerPhaseLabels = {
|
|
11
|
+
phase: ProviderRunnerPhaseMetric['phase'];
|
|
12
|
+
provider: string;
|
|
13
|
+
launch_kind: ProviderRunnerPhaseMetric['launchKind'];
|
|
14
|
+
};
|
|
5
15
|
|
|
6
16
|
export function registerRunnersServiceMetrics(): void {
|
|
7
17
|
const meter = getServiceMetricsProvider().getMeter('runners');
|
|
@@ -19,15 +29,31 @@ export function registerRunnersServiceMetrics(): void {
|
|
|
19
29
|
'Running enrolled runners with a live control session, no workspace or runner session, and no recent provisioner report after the stale-runner grace window',
|
|
20
30
|
},
|
|
21
31
|
);
|
|
32
|
+
const providerRunnersByPhase = meter.createObservableGauge<ProviderRunnerPhaseLabels>(
|
|
33
|
+
'runners_provider_runner_by_phase',
|
|
34
|
+
{
|
|
35
|
+
description:
|
|
36
|
+
'Provider runners by lifecycle phase, including idle runners without an active assignment',
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
const providerRunnersByPhaseOldestAge = meter.createObservableGauge<ProviderRunnerPhaseLabels>(
|
|
40
|
+
'runners_provider_runner_by_phase_oldest_age',
|
|
41
|
+
{
|
|
42
|
+
description: 'Oldest provider runner age by lifecycle phase',
|
|
43
|
+
unit: 'ms',
|
|
44
|
+
},
|
|
45
|
+
);
|
|
22
46
|
|
|
23
47
|
meter.addBatchObservableCallback(
|
|
24
48
|
async (observer) => {
|
|
25
|
-
const [depthResult, staleEnrolledRunnerCountResult] =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
const [depthResult, staleEnrolledRunnerCountResult, providerRunnersByPhaseResult] =
|
|
50
|
+
await Promise.allSettled([
|
|
51
|
+
getJobExecutionQueueDepth(),
|
|
52
|
+
countStaleEnrolledRunnerInstances({
|
|
53
|
+
graceSeconds: config.RUNNER_STALE_PROVISIONED_RUNNER_THRESHOLD_SECONDS,
|
|
54
|
+
}),
|
|
55
|
+
listProviderRunnerByPhaseMetrics(),
|
|
56
|
+
]);
|
|
31
57
|
|
|
32
58
|
if (depthResult.status === 'fulfilled') {
|
|
33
59
|
observer.observe(pendingJobExecutions, depthResult.value.pendingJobExecutions);
|
|
@@ -36,7 +62,28 @@ export function registerRunnersServiceMetrics(): void {
|
|
|
36
62
|
if (staleEnrolledRunnerCountResult.status === 'fulfilled') {
|
|
37
63
|
observer.observe(enrolledRunnersWithoutRecentReport, staleEnrolledRunnerCountResult.value);
|
|
38
64
|
}
|
|
65
|
+
if (providerRunnersByPhaseResult.status === 'fulfilled') {
|
|
66
|
+
for (const metric of providerRunnersByPhaseResult.value) {
|
|
67
|
+
const attributes: ProviderRunnerPhaseLabels = {
|
|
68
|
+
phase: metric.phase,
|
|
69
|
+
provider: metric.provider,
|
|
70
|
+
launch_kind: metric.launchKind,
|
|
71
|
+
};
|
|
72
|
+
observer.observe(providerRunnersByPhase, metric.count, attributes);
|
|
73
|
+
observer.observe(
|
|
74
|
+
providerRunnersByPhaseOldestAge,
|
|
75
|
+
metric.oldestAgeMilliseconds,
|
|
76
|
+
attributes,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
39
80
|
},
|
|
40
|
-
[
|
|
81
|
+
[
|
|
82
|
+
pendingJobExecutions,
|
|
83
|
+
runningJobExecutions,
|
|
84
|
+
enrolledRunnersWithoutRecentReport,
|
|
85
|
+
providerRunnersByPhase,
|
|
86
|
+
providerRunnersByPhaseOldestAge,
|
|
87
|
+
],
|
|
41
88
|
);
|
|
42
89
|
}
|
|
@@ -17,6 +17,9 @@ export function toPollDemandResponseDto(result: PollDemandResult): PollDemandRes
|
|
|
17
17
|
count: reservation.count,
|
|
18
18
|
expires_at: reservation.expiresAt.toISOString(),
|
|
19
19
|
})),
|
|
20
|
+
...(result.newlyReservedCount !== undefined
|
|
21
|
+
? {newly_reserved_count: result.newlyReservedCount}
|
|
22
|
+
: {}),
|
|
20
23
|
terminate_provider_runner_ids: result.terminateRunnerInstanceIds,
|
|
21
24
|
};
|
|
22
25
|
}
|
|
@@ -14,10 +14,14 @@ export function toReportRunnerInstancesResponseDto(result: ReportRunnerInstances
|
|
|
14
14
|
};
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export function toReconcileRunnerInstancesResponseDto(
|
|
17
|
+
export function toReconcileRunnerInstancesResponseDto(
|
|
18
|
+
result: ReconcileRunnerInstancesResult,
|
|
19
|
+
options: {includeIntendedReservationId: boolean},
|
|
20
|
+
): {
|
|
18
21
|
runners: Array<{
|
|
19
22
|
provider_runner_id: string;
|
|
20
23
|
state: ReconcileRunnerInstancesResult['runners'][number]['state'];
|
|
24
|
+
intended_reservation_id?: string | null;
|
|
21
25
|
reservation_id: string | null;
|
|
22
26
|
runner_session_id: string | null;
|
|
23
27
|
bound_job: {
|
|
@@ -34,6 +38,9 @@ export function toReconcileRunnerInstancesResponseDto(result: ReconcileRunnerIns
|
|
|
34
38
|
runners: result.runners.map((runner) => ({
|
|
35
39
|
provider_runner_id: runner.providerRunnerId,
|
|
36
40
|
state: runner.state,
|
|
41
|
+
...(options.includeIntendedReservationId
|
|
42
|
+
? {intended_reservation_id: runner.intendedReservationId}
|
|
43
|
+
: {}),
|
|
37
44
|
reservation_id: runner.reservationId,
|
|
38
45
|
runner_session_id: runner.runnerSessionId,
|
|
39
46
|
bound_job: runner.boundJobExecution
|
|
@@ -2,15 +2,19 @@ import {requireProvisionerContext} from '@shipfox/api-auth-context';
|
|
|
2
2
|
import {
|
|
3
3
|
assignRunnerInstancesBodySchema,
|
|
4
4
|
assignRunnerInstancesResponseSchema,
|
|
5
|
+
RESERVATION_EXPIRED_ERROR_CODE,
|
|
6
|
+
RUNNER_INSTANCE_NOT_ASSIGNABLE_ERROR_CODE,
|
|
5
7
|
} from '@shipfox/api-runners-dto';
|
|
6
8
|
import {ClientError, defineRoute} from '@shipfox/node-fastify';
|
|
7
9
|
import {
|
|
10
|
+
getRunnerAssignmentRejectionReason,
|
|
8
11
|
ReservationExpiredError,
|
|
9
12
|
ReservationNotFoundError,
|
|
10
13
|
RunnerInstanceAlreadyAssignedError,
|
|
11
14
|
RunnerInstanceNotAssignableError,
|
|
12
15
|
} from '#core/errors.js';
|
|
13
16
|
import {assignRunnerInstances} from '#core/runner-assignments.js';
|
|
17
|
+
import {recordProviderRunnerAssignmentRejected} from '#metrics/instance.js';
|
|
14
18
|
|
|
15
19
|
export const assignRunnerInstancesRoute = defineRoute({
|
|
16
20
|
method: 'POST',
|
|
@@ -21,17 +25,25 @@ export const assignRunnerInstancesRoute = defineRoute({
|
|
|
21
25
|
response: {200: assignRunnerInstancesResponseSchema},
|
|
22
26
|
},
|
|
23
27
|
errorHandler: (error) => {
|
|
28
|
+
const rejectionReason = getRunnerAssignmentRejectionReason(error);
|
|
29
|
+
if (rejectionReason)
|
|
30
|
+
recordProviderRunnerAssignmentRejected({
|
|
31
|
+
reason: rejectionReason,
|
|
32
|
+
surface: 'provisioner',
|
|
33
|
+
});
|
|
24
34
|
if (error instanceof ReservationNotFoundError)
|
|
25
35
|
throw new ClientError('Reservation not found', 'reservation-not-found', {status: 404});
|
|
26
36
|
if (error instanceof ReservationExpiredError)
|
|
27
|
-
throw new ClientError('Reservation has expired',
|
|
37
|
+
throw new ClientError('Reservation has expired', RESERVATION_EXPIRED_ERROR_CODE, {
|
|
38
|
+
status: 409,
|
|
39
|
+
});
|
|
28
40
|
if (
|
|
29
41
|
error instanceof RunnerInstanceAlreadyAssignedError ||
|
|
30
42
|
error instanceof RunnerInstanceNotAssignableError
|
|
31
43
|
)
|
|
32
44
|
throw new ClientError(
|
|
33
45
|
'Runner instance cannot be assigned',
|
|
34
|
-
|
|
46
|
+
RUNNER_INSTANCE_NOT_ASSIGNABLE_ERROR_CODE,
|
|
35
47
|
{status: 409},
|
|
36
48
|
);
|
|
37
49
|
throw error;
|