@shipfox/api-runners 12.2.0 → 12.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +12 -0
- package/dist/config.js +3 -3
- package/dist/config.js.map +1 -1
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +14 -2
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +22 -6
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-instances.d.ts +3 -0
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +10 -1
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-sessions.d.ts.map +1 -1
- package/dist/db/runner-sessions.js +22 -15
- package/dist/db/runner-sessions.js.map +1 -1
- package/dist/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +20 -4
- package/dist/metrics/service.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/config.test.ts +2 -2
- package/src/config.ts +3 -3
- package/src/core/runner-control-sessions.test.ts +169 -4
- package/src/core/runner-control-sessions.ts +22 -1
- package/src/db/index.ts +1 -0
- package/src/db/reservations.test.ts +369 -1
- package/src/db/reservations.ts +74 -9
- package/src/db/runner-instances.test.ts +231 -0
- package/src/db/runner-instances.ts +32 -1
- package/src/db/runner-sessions.ts +26 -15
- package/src/metrics/service.test.ts +99 -0
- package/src/metrics/service.ts +24 -4
- package/src/presentation/routes/late-runner-enrollment.test.ts +230 -0
- package/src/presentation/routes/poll-demand.test.ts +23 -6
- package/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shipfox/api-runners",
|
|
3
3
|
"license": "MIT",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.3.0",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/ShipfoxHQ/shipfox.git",
|
|
@@ -25,19 +25,19 @@
|
|
|
25
25
|
"@shipfox/api-auth-context": "12.2.0",
|
|
26
26
|
"@shipfox/api-auth-dto": "12.0.0",
|
|
27
27
|
"@shipfox/api-runners-dto": "12.2.0",
|
|
28
|
-
"@shipfox/api-workflows-dto": "12.
|
|
28
|
+
"@shipfox/api-workflows-dto": "12.3.0",
|
|
29
29
|
"@shipfox/config": "1.2.4",
|
|
30
30
|
"@shipfox/inter-module": "0.2.3",
|
|
31
31
|
"@shipfox/node-drizzle": "0.3.5",
|
|
32
32
|
"@shipfox/node-error-monitoring": "0.3.0",
|
|
33
33
|
"@shipfox/node-auth-root-key": "0.2.3",
|
|
34
34
|
"@shipfox/node-fastify": "0.4.2",
|
|
35
|
+
"@shipfox/node-module": "1.0.6",
|
|
35
36
|
"@shipfox/node-opentelemetry": "0.6.4",
|
|
36
37
|
"@shipfox/node-outbox": "0.2.6",
|
|
37
38
|
"@shipfox/node-postgres": "0.5.0",
|
|
38
39
|
"@shipfox/node-rate-limit": "0.4.0",
|
|
39
40
|
"@shipfox/node-temporal": "0.4.5",
|
|
40
|
-
"@shipfox/node-module": "1.0.6",
|
|
41
41
|
"@shipfox/node-tokens": "0.3.2",
|
|
42
42
|
"@shipfox/runner-labels": "0.2.0"
|
|
43
43
|
},
|
package/src/config.test.ts
CHANGED
|
@@ -104,12 +104,12 @@ describe('reservation TTL ceiling validation', () => {
|
|
|
104
104
|
await expect(import('#config.js')).rejects.toThrow('RESERVATION_TTL_MAX_SECONDS');
|
|
105
105
|
});
|
|
106
106
|
|
|
107
|
-
it('defaults high enough for
|
|
107
|
+
it('defaults high enough for provider registration deadlines and launch headroom', async () => {
|
|
108
108
|
vi.resetModules();
|
|
109
109
|
|
|
110
110
|
const {config} = await import('#config.js');
|
|
111
111
|
|
|
112
|
-
expect(config.RESERVATION_TTL_MAX_SECONDS).toBe(
|
|
112
|
+
expect(config.RESERVATION_TTL_MAX_SECONDS).toBe(600);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
115
|
it('accepts a whole-second ceiling inside the hard maximum', async () => {
|
package/src/config.ts
CHANGED
|
@@ -62,12 +62,12 @@ export const config = createConfig({
|
|
|
62
62
|
default: 250,
|
|
63
63
|
}),
|
|
64
64
|
RESERVATION_TTL_SECONDS: num({
|
|
65
|
-
desc: 'Default lifetime of a count-based runner reservation, in seconds. Used when a provisioner does not request a provider-specific value. Expired reservations stop counting against queued demand.',
|
|
65
|
+
desc: 'Default lifetime and activation grace period of a count-based runner reservation, in seconds. A runner without a session can be rebound only after this deadline. Used when a provisioner does not request a provider-specific value. Expired reservations stop counting against queued demand.',
|
|
66
66
|
default: 60,
|
|
67
67
|
}),
|
|
68
68
|
RESERVATION_TTL_MAX_SECONDS: num({
|
|
69
|
-
desc: `Server-side ceiling for a count-based runner reservation lifetime, in seconds. Set it at least as high as every provider registration deadline so provider-specific values can cover boot and enrollment. Set this between 1 and ${RESERVATION_TTL_HARD_MAX_SECONDS}.`,
|
|
70
|
-
default:
|
|
69
|
+
desc: `Server-side ceiling for a count-based runner reservation lifetime and activation grace period, in seconds. Set it at least as high as every provider registration deadline plus its launch headroom so provider-specific values can cover the launch gap, boot, and enrollment. Set this between 1 and ${RESERVATION_TTL_HARD_MAX_SECONDS}.`,
|
|
70
|
+
default: 600,
|
|
71
71
|
}),
|
|
72
72
|
RESERVATION_LONG_POLL_MAX_WAIT_SECONDS: num({
|
|
73
73
|
desc: 'Maximum time the provisioner demand poll endpoint waits for reservable demand before returning, in seconds.',
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type {RunnerToolCapabilitiesDto} from '@shipfox/api-runners-dto';
|
|
1
2
|
import {afterEach, vi} from '@shipfox/vitest/vi';
|
|
2
|
-
import {inArray} from 'drizzle-orm';
|
|
3
|
+
import {eq, inArray} from 'drizzle-orm';
|
|
3
4
|
import {db} from '#db/db.js';
|
|
5
|
+
import {provisionerTokens} from '#db/schema/provisioner-tokens.js';
|
|
4
6
|
import {reservations} from '#db/schema/reservations.js';
|
|
5
7
|
import {runnerActivationTokens} from '#db/schema/runner-activation-tokens.js';
|
|
6
8
|
import {runnerControlSessions} from '#db/schema/runner-control-sessions.js';
|
|
@@ -12,6 +14,7 @@ import {
|
|
|
12
14
|
import {enrollRunnerControlSession} from './runner-control-sessions.js';
|
|
13
15
|
|
|
14
16
|
const createdReservationIds = new Set<string>();
|
|
17
|
+
const createdProvisionerTokenIds = new Set<string>();
|
|
15
18
|
const createdRunnerInstanceIds = new Set<string>();
|
|
16
19
|
|
|
17
20
|
afterEach(async () => {
|
|
@@ -29,9 +32,14 @@ afterEach(async () => {
|
|
|
29
32
|
}
|
|
30
33
|
if (reservationIds.length > 0)
|
|
31
34
|
await db().delete(reservations).where(inArray(reservations.id, reservationIds));
|
|
35
|
+
if (createdProvisionerTokenIds.size > 0)
|
|
36
|
+
await db()
|
|
37
|
+
.delete(provisionerTokens)
|
|
38
|
+
.where(inArray(provisionerTokens.id, [...createdProvisionerTokenIds]));
|
|
32
39
|
|
|
33
40
|
createdRunnerInstanceIds.clear();
|
|
34
41
|
createdReservationIds.clear();
|
|
42
|
+
createdProvisionerTokenIds.clear();
|
|
35
43
|
});
|
|
36
44
|
|
|
37
45
|
describe('enrollRunnerControlSession', () => {
|
|
@@ -110,6 +118,139 @@ describe('enrollRunnerControlSession', () => {
|
|
|
110
118
|
}
|
|
111
119
|
});
|
|
112
120
|
|
|
121
|
+
it('preserves committed assignment metadata during a later enrollment', async () => {
|
|
122
|
+
const provisionerId = crypto.randomUUID();
|
|
123
|
+
const reservation = await createReservation({provisionerId});
|
|
124
|
+
const committedCapabilities: RunnerToolCapabilitiesDto = {
|
|
125
|
+
harnesses: {pi: {tools: ['read']}},
|
|
126
|
+
};
|
|
127
|
+
const runnerInstanceId = await createRunner({
|
|
128
|
+
provisionerId,
|
|
129
|
+
intendedReservationId: reservation.id,
|
|
130
|
+
reservationId: reservation.id,
|
|
131
|
+
workspaceId: reservation.workspaceId,
|
|
132
|
+
assignedAt: new Date(),
|
|
133
|
+
labels: ['linux'],
|
|
134
|
+
providerKind: 'ec2',
|
|
135
|
+
protocolVersion: '1',
|
|
136
|
+
capabilities: committedCapabilities,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const activationToken = await enrollRunnerControlSession({
|
|
140
|
+
runnerInstanceId,
|
|
141
|
+
provisionerId,
|
|
142
|
+
labels: ['gpu'],
|
|
143
|
+
capabilities: {harnesses: {claude: {tools: ['bash']}}},
|
|
144
|
+
providerKind: 'docker',
|
|
145
|
+
protocolVersion: '2',
|
|
146
|
+
});
|
|
147
|
+
const [runner] = await db()
|
|
148
|
+
.select({
|
|
149
|
+
reservationId: providerRunners.reservationId,
|
|
150
|
+
labels: providerRunners.labels,
|
|
151
|
+
providerKind: providerRunners.providerKind,
|
|
152
|
+
protocolVersion: providerRunners.protocolVersion,
|
|
153
|
+
capabilities: providerRunners.capabilities,
|
|
154
|
+
state: providerRunners.state,
|
|
155
|
+
})
|
|
156
|
+
.from(providerRunners)
|
|
157
|
+
.where(eq(providerRunners.id, runnerInstanceId));
|
|
158
|
+
|
|
159
|
+
expect(activationToken).toEqual(expect.any(String));
|
|
160
|
+
expect(runner).toEqual({
|
|
161
|
+
reservationId: reservation.id,
|
|
162
|
+
labels: ['linux'],
|
|
163
|
+
providerKind: 'ec2',
|
|
164
|
+
protocolVersion: '1',
|
|
165
|
+
capabilities: committedCapabilities,
|
|
166
|
+
state: 'running',
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('updates metadata when a provider report precedes assignment commit', async () => {
|
|
171
|
+
const provisionerId = crypto.randomUUID();
|
|
172
|
+
const reservation = await createReservation({provisionerId});
|
|
173
|
+
const capabilities: RunnerToolCapabilitiesDto = {
|
|
174
|
+
harnesses: {pi: {tools: ['read']}},
|
|
175
|
+
};
|
|
176
|
+
const runnerInstanceId = await createRunner({
|
|
177
|
+
provisionerId,
|
|
178
|
+
intendedReservationId: reservation.id,
|
|
179
|
+
reservationId: reservation.id,
|
|
180
|
+
assignedAt: null,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const activationToken = await enrollRunnerControlSession({
|
|
184
|
+
runnerInstanceId,
|
|
185
|
+
provisionerId,
|
|
186
|
+
labels: ['linux'],
|
|
187
|
+
capabilities,
|
|
188
|
+
providerKind: 'ec2',
|
|
189
|
+
protocolVersion: '1',
|
|
190
|
+
});
|
|
191
|
+
const [runner] = await db()
|
|
192
|
+
.select({
|
|
193
|
+
reservationId: providerRunners.reservationId,
|
|
194
|
+
assignedAt: providerRunners.assignedAt,
|
|
195
|
+
labels: providerRunners.labels,
|
|
196
|
+
providerKind: providerRunners.providerKind,
|
|
197
|
+
protocolVersion: providerRunners.protocolVersion,
|
|
198
|
+
capabilities: providerRunners.capabilities,
|
|
199
|
+
state: providerRunners.state,
|
|
200
|
+
})
|
|
201
|
+
.from(providerRunners)
|
|
202
|
+
.where(eq(providerRunners.id, runnerInstanceId));
|
|
203
|
+
|
|
204
|
+
expect(activationToken).toBeNull();
|
|
205
|
+
expect(runner).toEqual({
|
|
206
|
+
reservationId: reservation.id,
|
|
207
|
+
assignedAt: null,
|
|
208
|
+
labels: ['linux'],
|
|
209
|
+
providerKind: 'ec2',
|
|
210
|
+
protocolVersion: '1',
|
|
211
|
+
capabilities,
|
|
212
|
+
state: 'running',
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('updates metadata during enrollment for an unassigned runner', async () => {
|
|
217
|
+
const provisionerId = crypto.randomUUID();
|
|
218
|
+
const capabilities: RunnerToolCapabilitiesDto = {
|
|
219
|
+
harnesses: {pi: {tools: ['read']}},
|
|
220
|
+
};
|
|
221
|
+
const runnerInstanceId = await createRunner({provisionerId});
|
|
222
|
+
|
|
223
|
+
const activationToken = await enrollRunnerControlSession({
|
|
224
|
+
runnerInstanceId,
|
|
225
|
+
provisionerId,
|
|
226
|
+
labels: ['linux'],
|
|
227
|
+
capabilities,
|
|
228
|
+
providerKind: 'docker',
|
|
229
|
+
protocolVersion: '1',
|
|
230
|
+
});
|
|
231
|
+
const [runner] = await db()
|
|
232
|
+
.select({
|
|
233
|
+
reservationId: providerRunners.reservationId,
|
|
234
|
+
labels: providerRunners.labels,
|
|
235
|
+
providerKind: providerRunners.providerKind,
|
|
236
|
+
protocolVersion: providerRunners.protocolVersion,
|
|
237
|
+
capabilities: providerRunners.capabilities,
|
|
238
|
+
state: providerRunners.state,
|
|
239
|
+
})
|
|
240
|
+
.from(providerRunners)
|
|
241
|
+
.where(eq(providerRunners.id, runnerInstanceId));
|
|
242
|
+
|
|
243
|
+
expect(activationToken).toBeNull();
|
|
244
|
+
expect(runner).toEqual({
|
|
245
|
+
reservationId: null,
|
|
246
|
+
labels: ['linux'],
|
|
247
|
+
providerKind: 'docker',
|
|
248
|
+
protocolVersion: '1',
|
|
249
|
+
capabilities,
|
|
250
|
+
state: 'running',
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
113
254
|
it('keeps unexpected promotion failures on the error-log path', async () => {
|
|
114
255
|
const provisionerId = crypto.randomUUID();
|
|
115
256
|
const reservation = await createReservation({provisionerId});
|
|
@@ -207,18 +348,42 @@ async function createReservation(params: {
|
|
|
207
348
|
|
|
208
349
|
async function createRunner(params: {
|
|
209
350
|
provisionerId: string;
|
|
210
|
-
intendedReservationId
|
|
211
|
-
reservationId?: string;
|
|
351
|
+
intendedReservationId?: string | null;
|
|
352
|
+
reservationId?: string | null;
|
|
353
|
+
workspaceId?: string | null;
|
|
212
354
|
labels?: string[];
|
|
355
|
+
providerKind?: string | null;
|
|
356
|
+
protocolVersion?: string | null;
|
|
357
|
+
capabilities?: RunnerToolCapabilitiesDto | null;
|
|
358
|
+
assignedAt?: Date | null;
|
|
213
359
|
}): Promise<string> {
|
|
360
|
+
const defaultAssignedAt = params.reservationId ? new Date() : null;
|
|
361
|
+
|
|
362
|
+
if (!createdProvisionerTokenIds.has(params.provisionerId)) {
|
|
363
|
+
await db().insert(provisionerTokens).values({
|
|
364
|
+
id: params.provisionerId,
|
|
365
|
+
scope: 'installation',
|
|
366
|
+
workspaceId: null,
|
|
367
|
+
hashedToken: crypto.randomUUID(),
|
|
368
|
+
prefix: 'test',
|
|
369
|
+
createdByUserId: crypto.randomUUID(),
|
|
370
|
+
});
|
|
371
|
+
createdProvisionerTokenIds.add(params.provisionerId);
|
|
372
|
+
}
|
|
373
|
+
|
|
214
374
|
const [runner] = await db()
|
|
215
375
|
.insert(providerRunners)
|
|
216
376
|
.values({
|
|
217
377
|
provisionerId: params.provisionerId,
|
|
218
|
-
intendedReservationId: params.intendedReservationId,
|
|
378
|
+
intendedReservationId: params.intendedReservationId ?? null,
|
|
219
379
|
reservationId: params.reservationId ?? null,
|
|
380
|
+
workspaceId: params.workspaceId ?? null,
|
|
220
381
|
providerRunnerId: crypto.randomUUID(),
|
|
221
382
|
labels: params.labels ?? [],
|
|
383
|
+
providerKind: params.providerKind ?? null,
|
|
384
|
+
protocolVersion: params.protocolVersion ?? null,
|
|
385
|
+
capabilities: params.capabilities ?? null,
|
|
386
|
+
assignedAt: params.assignedAt === undefined ? defaultAssignedAt : params.assignedAt,
|
|
222
387
|
state: 'starting',
|
|
223
388
|
reportedAt: new Date(),
|
|
224
389
|
})
|
|
@@ -163,7 +163,9 @@ export async function enrollRunnerControlSession(params: {
|
|
|
163
163
|
sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${current.intendedReservationId}`}))`,
|
|
164
164
|
);
|
|
165
165
|
|
|
166
|
-
|
|
166
|
+
// Provider reports may populate reservationId before the assignment commits. assignedAt is
|
|
167
|
+
// written by the assignment transaction, so keep the guard on that write boundary.
|
|
168
|
+
const [metadataUpdated] = await tx
|
|
167
169
|
.update(providerRunners)
|
|
168
170
|
.set({
|
|
169
171
|
labels: sanitizeRunnerLabels(params.labels, {
|
|
@@ -173,6 +175,25 @@ export async function enrollRunnerControlSession(params: {
|
|
|
173
175
|
providerKind: params.providerKind,
|
|
174
176
|
protocolVersion: params.protocolVersion,
|
|
175
177
|
capabilities: params.capabilities ?? null,
|
|
178
|
+
})
|
|
179
|
+
.where(
|
|
180
|
+
and(
|
|
181
|
+
eq(providerRunners.id, params.runnerInstanceId),
|
|
182
|
+
eq(providerRunners.provisionerId, params.provisionerId),
|
|
183
|
+
isNull(providerRunners.assignedAt),
|
|
184
|
+
notInArray(providerRunners.state, [...terminalStates]),
|
|
185
|
+
),
|
|
186
|
+
)
|
|
187
|
+
.returning({id: providerRunners.id});
|
|
188
|
+
if (!metadataUpdated)
|
|
189
|
+
logger().debug(
|
|
190
|
+
{runnerInstanceId: params.runnerInstanceId, provisionerId: params.provisionerId},
|
|
191
|
+
'Runner enrollment metadata update skipped for assigned or terminal runner',
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const [updated] = await tx
|
|
195
|
+
.update(providerRunners)
|
|
196
|
+
.set({
|
|
176
197
|
state: 'running',
|
|
177
198
|
reportedAt: sql`now()`,
|
|
178
199
|
updatedAt: sql`now()`,
|
package/src/db/index.ts
CHANGED