@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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {eq} from 'drizzle-orm';
|
|
2
2
|
import {
|
|
3
3
|
ReservationExpiredError,
|
|
4
|
+
ReservationNotFoundError,
|
|
4
5
|
RunnerInstanceAlreadyAssignedError,
|
|
5
6
|
RunnerInstanceNotAssignableError,
|
|
6
7
|
} from '#core/errors.js';
|
|
@@ -41,6 +42,50 @@ describe('assignRunnerInstances', () => {
|
|
|
41
42
|
});
|
|
42
43
|
});
|
|
43
44
|
|
|
45
|
+
it('repairs a provider-reported reservation before assignment commit', async () => {
|
|
46
|
+
const reservation = await createReservation();
|
|
47
|
+
const [runner] = await db()
|
|
48
|
+
.insert(providerRunners)
|
|
49
|
+
.values({
|
|
50
|
+
provisionerId,
|
|
51
|
+
intendedReservationId: reservation.id,
|
|
52
|
+
reservationId: reservation.id,
|
|
53
|
+
providerRunnerId: crypto.randomUUID(),
|
|
54
|
+
labels: ['linux'],
|
|
55
|
+
state: 'running',
|
|
56
|
+
reportedAt: new Date(),
|
|
57
|
+
})
|
|
58
|
+
.returning();
|
|
59
|
+
if (!runner) throw new Error('Runner instance insert returned no row');
|
|
60
|
+
await db()
|
|
61
|
+
.insert(runnerControlSessions)
|
|
62
|
+
.values({
|
|
63
|
+
runnerInstanceId: runner.id,
|
|
64
|
+
provisionerId,
|
|
65
|
+
hashedToken: crypto.randomUUID(),
|
|
66
|
+
prefix: 'test',
|
|
67
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const assigned = await assignRunnerInstances({
|
|
71
|
+
provisionerId,
|
|
72
|
+
reservationId: reservation.id,
|
|
73
|
+
runnerInstanceIds: [runner.id],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
expect(assigned).toEqual([runner.id]);
|
|
77
|
+
const [stored] = await db()
|
|
78
|
+
.select()
|
|
79
|
+
.from(providerRunners)
|
|
80
|
+
.where(eq(providerRunners.id, runner.id));
|
|
81
|
+
expect(stored).toMatchObject({
|
|
82
|
+
workspaceId,
|
|
83
|
+
reservationId: reservation.id,
|
|
84
|
+
intendedReservationId: null,
|
|
85
|
+
assignedAt: expect.any(Date),
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
44
89
|
it('is idempotent for concurrent retries of the same assignment', async () => {
|
|
45
90
|
const reservation = await createReservation();
|
|
46
91
|
const runner = await createEnrolledRunner();
|
|
@@ -80,6 +125,32 @@ describe('assignRunnerInstances', () => {
|
|
|
80
125
|
expect(assigned).toEqual([runner.id]);
|
|
81
126
|
});
|
|
82
127
|
|
|
128
|
+
it('is idempotent for a committed bound reservation assignment', async () => {
|
|
129
|
+
const reservation = await createReservation({kind: 'bound'});
|
|
130
|
+
const runner = await createEnrolledRunner();
|
|
131
|
+
await db()
|
|
132
|
+
.update(providerRunners)
|
|
133
|
+
.set({
|
|
134
|
+
workspaceId,
|
|
135
|
+
reservationId: reservation.id,
|
|
136
|
+
assignedAt: new Date(),
|
|
137
|
+
})
|
|
138
|
+
.where(eq(providerRunners.id, runner.id));
|
|
139
|
+
|
|
140
|
+
const assigned = await assignRunnerInstances({
|
|
141
|
+
provisionerId,
|
|
142
|
+
reservationId: reservation.id,
|
|
143
|
+
runnerInstanceIds: [runner.id],
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(assigned).toEqual([runner.id]);
|
|
147
|
+
const [stored] = await db()
|
|
148
|
+
.select()
|
|
149
|
+
.from(providerRunners)
|
|
150
|
+
.where(eq(providerRunners.id, runner.id));
|
|
151
|
+
expect(stored?.intendedReservationId).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
83
154
|
it('rejects expired reservations', async () => {
|
|
84
155
|
const reservation = await createReservation({expiresAt: new Date(Date.now() - 1_000)});
|
|
85
156
|
const runner = await createEnrolledRunner();
|
|
@@ -93,6 +164,19 @@ describe('assignRunnerInstances', () => {
|
|
|
93
164
|
await expect(assignment).rejects.toThrow(ReservationExpiredError);
|
|
94
165
|
});
|
|
95
166
|
|
|
167
|
+
it('rejects bound reservations', async () => {
|
|
168
|
+
const reservation = await createReservation({kind: 'bound'});
|
|
169
|
+
const runner = await createEnrolledRunner();
|
|
170
|
+
|
|
171
|
+
const assignment = assignRunnerInstances({
|
|
172
|
+
provisionerId,
|
|
173
|
+
reservationId: reservation.id,
|
|
174
|
+
runnerInstanceIds: [runner.id],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
await expect(assignment).rejects.toThrow(ReservationNotFoundError);
|
|
178
|
+
});
|
|
179
|
+
|
|
96
180
|
it('rejects unenrolled or incompatible runners', async () => {
|
|
97
181
|
const reservation = await createReservation({requiredLabels: ['linux', 'gpu']});
|
|
98
182
|
const runner = await createEnrolledRunner({labels: ['linux']});
|
|
@@ -139,6 +223,25 @@ describe('assignRunnerInstances', () => {
|
|
|
139
223
|
await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
|
|
140
224
|
});
|
|
141
225
|
|
|
226
|
+
it('charges capacity for a committed runner replayed alongside a new one', async () => {
|
|
227
|
+
const reservation = await createReservation();
|
|
228
|
+
const firstRunner = await createEnrolledRunner();
|
|
229
|
+
const secondRunner = await createEnrolledRunner();
|
|
230
|
+
await assignRunnerInstances({
|
|
231
|
+
provisionerId,
|
|
232
|
+
reservationId: reservation.id,
|
|
233
|
+
runnerInstanceIds: [firstRunner.id],
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
const assignment = assignRunnerInstances({
|
|
237
|
+
provisionerId,
|
|
238
|
+
reservationId: reservation.id,
|
|
239
|
+
runnerInstanceIds: [firstRunner.id, secondRunner.id],
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
await expect(assignment).rejects.toThrow(RunnerInstanceNotAssignableError);
|
|
243
|
+
});
|
|
244
|
+
|
|
142
245
|
it('does not charge capacity for an already released reservation intent', async () => {
|
|
143
246
|
const reservation = await createReservation();
|
|
144
247
|
await db().insert(providerRunners).values({
|
|
@@ -202,7 +305,7 @@ describe('assignRunnerInstances', () => {
|
|
|
202
305
|
});
|
|
203
306
|
|
|
204
307
|
async function createReservation(
|
|
205
|
-
overrides: Partial<{expiresAt: Date; requiredLabels: string[]}> = {},
|
|
308
|
+
overrides: Partial<{expiresAt: Date; requiredLabels: string[]; kind: 'bound' | 'launch'}> = {},
|
|
206
309
|
) {
|
|
207
310
|
const [reservation] = await db()
|
|
208
311
|
.insert(reservations)
|
|
@@ -211,6 +314,7 @@ describe('assignRunnerInstances', () => {
|
|
|
211
314
|
provisionerId,
|
|
212
315
|
requiredLabels: overrides.requiredLabels ?? ['linux'],
|
|
213
316
|
count: 1,
|
|
317
|
+
kind: overrides.kind ?? 'launch',
|
|
214
318
|
expiresAt: overrides.expiresAt ?? new Date(Date.now() + 60_000),
|
|
215
319
|
})
|
|
216
320
|
.returning();
|
|
@@ -1,13 +1,19 @@
|
|
|
1
|
-
import {and, eq, inArray, isNull, notInArray, or, sql} from 'drizzle-orm';
|
|
1
|
+
import {and, eq, inArray, isNull, ne, notInArray, or, sql} from 'drizzle-orm';
|
|
2
2
|
import {
|
|
3
3
|
ReservationExpiredError,
|
|
4
4
|
ReservationNotFoundError,
|
|
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
|
-
import {
|
|
15
|
+
import {lockRunnerReservationAdvisoryKeysTx} from './reservation-locks.js';
|
|
16
|
+
import {terminalStates} from './runner-states.js';
|
|
11
17
|
import {reservations} from './schema/reservations.js';
|
|
12
18
|
import {runnerControlSessions} from './schema/runner-control-sessions.js';
|
|
13
19
|
import {providerRunners} from './schema/runner-instances.js';
|
|
@@ -18,7 +24,17 @@ export async function assignRunnerInstances(params: {
|
|
|
18
24
|
reservationId: string;
|
|
19
25
|
runnerInstanceIds: string[];
|
|
20
26
|
}): Promise<string[]> {
|
|
21
|
-
|
|
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[];
|
|
22
38
|
}
|
|
23
39
|
|
|
24
40
|
export async function assignRunnerInstancesTx(
|
|
@@ -27,19 +43,24 @@ export async function assignRunnerInstancesTx(
|
|
|
27
43
|
provisionerId: string;
|
|
28
44
|
reservationId: string;
|
|
29
45
|
runnerInstanceIds: string[];
|
|
46
|
+
surface: RunnerAssignmentSurface;
|
|
30
47
|
},
|
|
31
|
-
): Promise<
|
|
48
|
+
): Promise<AssignRunnerInstancesTxResult> {
|
|
32
49
|
const runnerInstanceIds = [...params.runnerInstanceIds].sort();
|
|
33
|
-
await tx
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
51
|
+
provisionerId: params.provisionerId,
|
|
52
|
+
reservationIds: [params.reservationId],
|
|
53
|
+
});
|
|
36
54
|
const runnerRows = await tx
|
|
37
55
|
.select({
|
|
38
56
|
id: providerRunners.id,
|
|
39
57
|
reservationId: providerRunners.reservationId,
|
|
40
58
|
intendedReservationId: providerRunners.intendedReservationId,
|
|
41
59
|
workspaceId: providerRunners.workspaceId,
|
|
60
|
+
assignedAt: providerRunners.assignedAt,
|
|
42
61
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
62
|
+
provider: providerRunners.providerKind,
|
|
63
|
+
launchKind: providerRunners.launchKind,
|
|
43
64
|
labels: providerRunners.labels,
|
|
44
65
|
state: providerRunners.state,
|
|
45
66
|
})
|
|
@@ -53,11 +74,16 @@ export async function assignRunnerInstancesTx(
|
|
|
53
74
|
.for('update');
|
|
54
75
|
|
|
55
76
|
// The assignment outlives its short reservation row. A retry after maintenance
|
|
56
|
-
// cleanup is complete when every owned runner already carries the
|
|
77
|
+
// cleanup is complete when every owned runner already carries the committed assignment.
|
|
57
78
|
if (
|
|
58
79
|
runnerInstanceIds.length > 0 &&
|
|
59
80
|
runnerRows.length === runnerInstanceIds.length &&
|
|
60
|
-
runnerRows.every(
|
|
81
|
+
runnerRows.every(
|
|
82
|
+
(runner) =>
|
|
83
|
+
runner.reservationId === params.reservationId &&
|
|
84
|
+
runner.assignedAt !== null &&
|
|
85
|
+
runner.workspaceId !== null,
|
|
86
|
+
)
|
|
61
87
|
) {
|
|
62
88
|
await tx
|
|
63
89
|
.update(providerRunners)
|
|
@@ -68,7 +94,10 @@ export async function assignRunnerInstancesTx(
|
|
|
68
94
|
inArray(providerRunners.id, runnerInstanceIds),
|
|
69
95
|
),
|
|
70
96
|
);
|
|
71
|
-
return
|
|
97
|
+
return {
|
|
98
|
+
runnerInstanceIds: params.runnerInstanceIds,
|
|
99
|
+
controlSessionToAssignment: [],
|
|
100
|
+
};
|
|
72
101
|
}
|
|
73
102
|
|
|
74
103
|
const [reservation] = await tx
|
|
@@ -78,6 +107,7 @@ export async function assignRunnerInstancesTx(
|
|
|
78
107
|
and(
|
|
79
108
|
eq(reservations.id, params.reservationId),
|
|
80
109
|
eq(reservations.provisionerId, params.provisionerId),
|
|
110
|
+
eq(reservations.kind, 'launch'),
|
|
81
111
|
),
|
|
82
112
|
)
|
|
83
113
|
.limit(1)
|
|
@@ -85,10 +115,13 @@ export async function assignRunnerInstancesTx(
|
|
|
85
115
|
if (!reservation) throw new ReservationNotFoundError(params.reservationId);
|
|
86
116
|
if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);
|
|
87
117
|
if (runnerRows.length !== runnerInstanceIds.length)
|
|
88
|
-
throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');
|
|
118
|
+
throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '', 'runner-not-found');
|
|
89
119
|
|
|
90
120
|
const activeControlSessions = await tx
|
|
91
|
-
.select({
|
|
121
|
+
.select({
|
|
122
|
+
runnerInstanceId: runnerControlSessions.runnerInstanceId,
|
|
123
|
+
createdAt: runnerControlSessions.createdAt,
|
|
124
|
+
})
|
|
92
125
|
.from(runnerControlSessions)
|
|
93
126
|
.where(
|
|
94
127
|
and(
|
|
@@ -102,21 +135,32 @@ export async function assignRunnerInstancesTx(
|
|
|
102
135
|
const runnerInstanceIdsWithControlSession = new Set(
|
|
103
136
|
activeControlSessions.map((session) => session.runnerInstanceId),
|
|
104
137
|
);
|
|
138
|
+
const controlSessionCreatedAtByRunner = new Map(
|
|
139
|
+
activeControlSessions.map((session) => [session.runnerInstanceId, session.createdAt]),
|
|
140
|
+
);
|
|
105
141
|
const runners = runnerRows.map((runner) => ({
|
|
106
142
|
...runner,
|
|
107
143
|
controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,
|
|
144
|
+
controlSessionCreatedAt: controlSessionCreatedAtByRunner.get(runner.id) ?? null,
|
|
108
145
|
}));
|
|
109
146
|
|
|
110
147
|
const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);
|
|
111
148
|
if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))
|
|
112
149
|
throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');
|
|
113
|
-
const newRunners = runners.filter(
|
|
150
|
+
const newRunners = runners.filter(
|
|
151
|
+
(runner) =>
|
|
152
|
+
runner.reservationId === null ||
|
|
153
|
+
(runner.reservationId === reservation.id &&
|
|
154
|
+
(runner.assignedAt === null || runner.workspaceId === null)),
|
|
155
|
+
);
|
|
156
|
+
const newRunnerIds = newRunners.map((runner) => runner.id);
|
|
114
157
|
const assignedCount = await tx
|
|
115
158
|
.select({count: sql<number>`count(*)::int`})
|
|
116
159
|
.from(providerRunners)
|
|
117
160
|
.where(
|
|
118
161
|
and(
|
|
119
162
|
eq(providerRunners.provisionerId, params.provisionerId),
|
|
163
|
+
notInArray(providerRunners.id, newRunnerIds),
|
|
120
164
|
or(
|
|
121
165
|
and(
|
|
122
166
|
eq(providerRunners.reservationId, reservation.id),
|
|
@@ -124,7 +168,6 @@ export async function assignRunnerInstancesTx(
|
|
|
124
168
|
),
|
|
125
169
|
and(
|
|
126
170
|
eq(providerRunners.intendedReservationId, reservation.id),
|
|
127
|
-
notInArray(providerRunners.id, runnerInstanceIds),
|
|
128
171
|
isNull(providerRunners.reservationReleasedAt),
|
|
129
172
|
notInArray(providerRunners.state, [...terminalStates]),
|
|
130
173
|
),
|
|
@@ -132,18 +175,19 @@ export async function assignRunnerInstancesTx(
|
|
|
132
175
|
),
|
|
133
176
|
);
|
|
134
177
|
if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)
|
|
135
|
-
throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');
|
|
178
|
+
throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '', 'capacity-exhausted');
|
|
136
179
|
for (const runner of newRunners) {
|
|
137
|
-
if (
|
|
138
|
-
runner.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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');
|
|
144
188
|
}
|
|
145
189
|
if (newRunners.length > 0) {
|
|
146
|
-
await tx
|
|
190
|
+
const assignedRows = await tx
|
|
147
191
|
.update(providerRunners)
|
|
148
192
|
.set({
|
|
149
193
|
workspaceId: reservation.workspaceId,
|
|
@@ -157,7 +201,183 @@ export async function assignRunnerInstancesTx(
|
|
|
157
201
|
providerRunners.id,
|
|
158
202
|
newRunners.map((runner) => runner.id),
|
|
159
203
|
),
|
|
160
|
-
)
|
|
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
|
+
};
|
|
161
224
|
}
|
|
162
|
-
return
|
|
225
|
+
return {
|
|
226
|
+
runnerInstanceIds: params.runnerInstanceIds,
|
|
227
|
+
controlSessionToAssignment: [],
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export type RunnerReservationCapacityFailureReason =
|
|
232
|
+
| 'reservation-not-found'
|
|
233
|
+
| 'reservation-kind-mismatch'
|
|
234
|
+
| 'reservation-expired'
|
|
235
|
+
| 'capacity-exhausted';
|
|
236
|
+
|
|
237
|
+
export interface RunnerReservationCapacityValidation {
|
|
238
|
+
acceptedByReservation: Map<string, number>;
|
|
239
|
+
unavailableByReservation: Map<
|
|
240
|
+
string,
|
|
241
|
+
{reason: RunnerReservationCapacityFailureReason; count: number}
|
|
242
|
+
>;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export async function validateRunnerReservationCapacityTx(
|
|
246
|
+
tx: Tx,
|
|
247
|
+
params: {
|
|
248
|
+
provisionerId: string;
|
|
249
|
+
requests: readonly {reservationId: string; count: number}[];
|
|
250
|
+
},
|
|
251
|
+
options: {advisoryLocksHeld?: boolean} = {},
|
|
252
|
+
): Promise<RunnerReservationCapacityValidation> {
|
|
253
|
+
const requestedByReservation = new Map<string, number>();
|
|
254
|
+
for (const request of params.requests) {
|
|
255
|
+
if (request.count <= 0) continue;
|
|
256
|
+
requestedByReservation.set(
|
|
257
|
+
request.reservationId,
|
|
258
|
+
(requestedByReservation.get(request.reservationId) ?? 0) + request.count,
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
const reservationIds = [...requestedByReservation.keys()].sort();
|
|
262
|
+
if (reservationIds.length === 0)
|
|
263
|
+
return {acceptedByReservation: new Map(), unavailableByReservation: new Map()};
|
|
264
|
+
|
|
265
|
+
if (!options.advisoryLocksHeld)
|
|
266
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
267
|
+
provisionerId: params.provisionerId,
|
|
268
|
+
reservationIds,
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const reservationRows = await tx
|
|
272
|
+
.select({
|
|
273
|
+
id: reservations.id,
|
|
274
|
+
count: reservations.count,
|
|
275
|
+
expiresAt: reservations.expiresAt,
|
|
276
|
+
isExpired: sql<boolean>`${reservations.expiresAt} <= now()`,
|
|
277
|
+
})
|
|
278
|
+
.from(reservations)
|
|
279
|
+
.where(
|
|
280
|
+
and(
|
|
281
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
282
|
+
inArray(reservations.id, reservationIds),
|
|
283
|
+
eq(reservations.kind, 'launch'),
|
|
284
|
+
),
|
|
285
|
+
)
|
|
286
|
+
.for('update');
|
|
287
|
+
const nonLaunchReservationRows = await tx
|
|
288
|
+
.select({id: reservations.id})
|
|
289
|
+
.from(reservations)
|
|
290
|
+
.where(
|
|
291
|
+
and(
|
|
292
|
+
eq(reservations.provisionerId, params.provisionerId),
|
|
293
|
+
inArray(reservations.id, reservationIds),
|
|
294
|
+
ne(reservations.kind, 'launch'),
|
|
295
|
+
),
|
|
296
|
+
)
|
|
297
|
+
.for('update');
|
|
298
|
+
const reservationsById = new Map(
|
|
299
|
+
reservationRows.map((reservation) => [reservation.id, reservation]),
|
|
300
|
+
);
|
|
301
|
+
const nonLaunchReservationIds = new Set(
|
|
302
|
+
nonLaunchReservationRows.map((reservation) => reservation.id),
|
|
303
|
+
);
|
|
304
|
+
const requestedReservationIds = new Set(reservationIds);
|
|
305
|
+
const usedRunnerIdsByReservation = new Map<string, Set<string>>();
|
|
306
|
+
const usedRunnerRows = await tx
|
|
307
|
+
.select({
|
|
308
|
+
id: providerRunners.id,
|
|
309
|
+
reservationId: providerRunners.reservationId,
|
|
310
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
311
|
+
state: providerRunners.state,
|
|
312
|
+
})
|
|
313
|
+
.from(providerRunners)
|
|
314
|
+
.where(
|
|
315
|
+
and(
|
|
316
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
317
|
+
isNull(providerRunners.reservationReleasedAt),
|
|
318
|
+
or(
|
|
319
|
+
inArray(providerRunners.reservationId, reservationIds),
|
|
320
|
+
and(
|
|
321
|
+
inArray(providerRunners.intendedReservationId, reservationIds),
|
|
322
|
+
notInArray(providerRunners.state, [...terminalStates]),
|
|
323
|
+
),
|
|
324
|
+
),
|
|
325
|
+
),
|
|
326
|
+
);
|
|
327
|
+
for (const runner of usedRunnerRows) {
|
|
328
|
+
if (runner.reservationId && requestedReservationIds.has(runner.reservationId)) {
|
|
329
|
+
const usedRunnerIds =
|
|
330
|
+
usedRunnerIdsByReservation.get(runner.reservationId) ?? new Set<string>();
|
|
331
|
+
usedRunnerIds.add(runner.id);
|
|
332
|
+
usedRunnerIdsByReservation.set(runner.reservationId, usedRunnerIds);
|
|
333
|
+
}
|
|
334
|
+
if (
|
|
335
|
+
runner.intendedReservationId &&
|
|
336
|
+
requestedReservationIds.has(runner.intendedReservationId) &&
|
|
337
|
+
!terminalStates.includes(runner.state as (typeof terminalStates)[number])
|
|
338
|
+
) {
|
|
339
|
+
const usedRunnerIds =
|
|
340
|
+
usedRunnerIdsByReservation.get(runner.intendedReservationId) ?? new Set<string>();
|
|
341
|
+
usedRunnerIds.add(runner.id);
|
|
342
|
+
usedRunnerIdsByReservation.set(runner.intendedReservationId, usedRunnerIds);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const acceptedByReservation = new Map<string, number>();
|
|
347
|
+
const unavailableByReservation = new Map<
|
|
348
|
+
string,
|
|
349
|
+
{reason: RunnerReservationCapacityFailureReason; count: number}
|
|
350
|
+
>();
|
|
351
|
+
for (const reservationId of reservationIds) {
|
|
352
|
+
const requested = requestedByReservation.get(reservationId) ?? 0;
|
|
353
|
+
const reservation = reservationsById.get(reservationId);
|
|
354
|
+
if (!reservation) {
|
|
355
|
+
unavailableByReservation.set(reservationId, {
|
|
356
|
+
reason: nonLaunchReservationIds.has(reservationId)
|
|
357
|
+
? 'reservation-kind-mismatch'
|
|
358
|
+
: 'reservation-not-found',
|
|
359
|
+
count: requested,
|
|
360
|
+
});
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
if (reservation.isExpired) {
|
|
364
|
+
unavailableByReservation.set(reservationId, {
|
|
365
|
+
reason: 'reservation-expired',
|
|
366
|
+
count: requested,
|
|
367
|
+
});
|
|
368
|
+
continue;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const used = usedRunnerIdsByReservation.get(reservationId)?.size ?? 0;
|
|
372
|
+
const accepted = Math.min(requested, Math.max(0, reservation.count - used));
|
|
373
|
+
acceptedByReservation.set(reservationId, accepted);
|
|
374
|
+
if (accepted < requested) {
|
|
375
|
+
unavailableByReservation.set(reservationId, {
|
|
376
|
+
reason: 'capacity-exhausted',
|
|
377
|
+
count: requested - accepted,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return {acceptedByReservation, unavailableByReservation};
|
|
163
383
|
}
|