@shipfox/api-runners 12.1.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 +35 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -2
- package/dist/config.js.map +1 -1
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/rate-limit.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +37 -11
- 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.map +1 -1
- package/dist/core/runner-labels.d.ts +11 -0
- package/dist/core/runner-labels.d.ts.map +1 -0
- package/dist/core/runner-labels.js +41 -0
- package/dist/core/runner-labels.js.map +1 -0
- package/dist/core/runner-sessions.d.ts.map +1 -1
- package/dist/core/runner-sessions.js +5 -1
- package/dist/core/runner-sessions.js.map +1 -1
- package/dist/db/db.d.ts +64 -64
- package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.js +12 -2
- package/dist/db/ephemeral-registration-tokens.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 +36 -10
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-instances.d.ts +4 -0
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +16 -5
- 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 +33 -15
- package/dist/db/runner-sessions.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +1 -1
- package/dist/db/schema/ephemeral-registration-tokens.d.ts +1 -1
- package/dist/db/schema/manual-registration-tokens.d.ts +1 -1
- package/dist/db/schema/outbox.d.ts +1 -1
- package/dist/db/schema/pending-job-executions.d.ts +3 -3
- package/dist/db/schema/provisioner-capability-snapshots.d.ts +3 -3
- package/dist/db/schema/provisioner-tokens.d.ts +2 -2
- package/dist/db/schema/rate-limits.d.ts +1 -1
- package/dist/db/schema/reservations.d.ts +3 -3
- package/dist/db/schema/runner-activation-tokens.d.ts +1 -1
- package/dist/db/schema/runner-control-sessions.d.ts +2 -2
- package/dist/db/schema/runner-instances.d.ts +4 -4
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +1 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/db/schema/runner-sessions.d.ts +4 -4
- package/dist/db/schema/running-job-executions.d.ts +5 -5
- package/dist/metrics/index.d.ts +2 -1
- 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 +13 -8
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +8 -0
- package/dist/metrics/instance.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/presentation/dto/admin-provisioner-token.d.ts +1 -1
- package/dist/presentation/dto/admin-provisioner-token.d.ts.map +1 -1
- package/dist/presentation/dto/admin-runner-instances.d.ts +2 -2
- package/dist/presentation/dto/admin-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.js +12 -6
- package/dist/presentation/routes/poll-demand.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/register.d.ts.map +1 -1
- package/dist/presentation/routes/register.js +9 -1
- package/dist/presentation/routes/register.js.map +1 -1
- package/dist/presentation/routes/report-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/report-runner-instances.js +1 -0
- package/dist/presentation/routes/report-runner-instances.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0004_huge_skreet.sql +1 -0
- package/drizzle/meta/0004_snapshot.json +2194 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +9 -9
- package/src/config.test.ts +81 -0
- package/src/config.ts +39 -2
- package/src/core/errors.ts +9 -0
- package/src/core/index.ts +1 -0
- package/src/core/runner-control-sessions.test.ts +404 -0
- package/src/core/runner-control-sessions.ts +51 -11
- package/src/core/runner-instances.ts +1 -0
- package/src/core/runner-labels.test.ts +53 -0
- package/src/core/runner-labels.ts +51 -0
- package/src/core/runner-sessions.test.ts +34 -1
- package/src/core/runner-sessions.ts +8 -1
- package/src/db/ephemeral-registration-tokens.ts +15 -1
- package/src/db/index.ts +1 -0
- package/src/db/reservations.test.ts +537 -0
- package/src/db/reservations.ts +116 -17
- package/src/db/runner-instances.test.ts +276 -1
- package/src/db/runner-instances.ts +39 -3
- package/src/db/runner-sessions.ts +39 -15
- package/src/db/schema/runner-instances.ts +3 -0
- package/src/metrics/index.ts +2 -0
- package/src/metrics/instance.ts +18 -0
- 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 +116 -3
- package/src/presentation/routes/poll-demand.ts +18 -6
- package/src/presentation/routes/register.test.ts +116 -2
- package/src/presentation/routes/register.ts +7 -0
- package/src/presentation/routes/report-runner-instances.test.ts +33 -0
- package/src/presentation/routes/report-runner-instances.ts +1 -0
- package/src/presentation/routes/runner-enrollment.test.ts +65 -5
- package/src/temporal/activities/maintenance-activities.test.ts +24 -31
- package/test/env.ts +1 -0
- package/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -4,16 +4,18 @@ import {db} from '#db/db.js';
|
|
|
4
4
|
import {createRunnerSessionConsumingEphemeralToken} from '#db/ephemeral-registration-tokens.js';
|
|
5
5
|
import {
|
|
6
6
|
attachRunnerInstanceProviderId,
|
|
7
|
+
countStaleEnrolledRunnerInstances,
|
|
7
8
|
listActiveRunnerInstanceCountsByTemplateTx,
|
|
8
9
|
listActiveRunnerInstances,
|
|
9
10
|
listProvisionerTerminateIntentRowsTx,
|
|
10
11
|
listProvisionerTerminateIntents,
|
|
11
12
|
reapStaleRunnerInstances,
|
|
12
13
|
reconcileRunnerInstances,
|
|
13
|
-
reportRunnerInstances,
|
|
14
|
+
reportRunnerInstances as reportRunnerInstancesDb,
|
|
14
15
|
} from '#db/runner-instances.js';
|
|
15
16
|
import {provisionerTokens} from '#db/schema/provisioner-tokens.js';
|
|
16
17
|
import {reservations} from '#db/schema/reservations.js';
|
|
18
|
+
import {runnerControlSessions} from '#db/schema/runner-control-sessions.js';
|
|
17
19
|
import {providerRunners} from '#db/schema/runner-instances.js';
|
|
18
20
|
import {runnerSessions} from '#db/schema/runner-sessions.js';
|
|
19
21
|
import {runningJobExecutions} from '#db/schema/running-job-executions.js';
|
|
@@ -25,6 +27,12 @@ import {
|
|
|
25
27
|
runnerSessionFactory,
|
|
26
28
|
} from '#test/index.js';
|
|
27
29
|
|
|
30
|
+
type ReportRunnerInstancesTestParams = Parameters<typeof reportRunnerInstancesDb>[0];
|
|
31
|
+
|
|
32
|
+
function reportRunnerInstances(params: ReportRunnerInstancesTestParams) {
|
|
33
|
+
return reportRunnerInstancesDb(params);
|
|
34
|
+
}
|
|
35
|
+
|
|
28
36
|
function providerRunnerRowsFor(params: {workspaceId: string; provisionerId: string}) {
|
|
29
37
|
return db()
|
|
30
38
|
.select()
|
|
@@ -93,6 +101,7 @@ describe('reportRunnerInstances', () => {
|
|
|
93
101
|
const reportedAt = new Date();
|
|
94
102
|
|
|
95
103
|
const report = await reportRunnerInstances({
|
|
104
|
+
scope: 'installation',
|
|
96
105
|
workspaceId: null,
|
|
97
106
|
provisionerId,
|
|
98
107
|
events: [event({providerRunnerId: 'installation-runner', reportedAt})],
|
|
@@ -118,6 +127,7 @@ describe('reportRunnerInstances', () => {
|
|
|
118
127
|
const reportedAt = new Date();
|
|
119
128
|
|
|
120
129
|
const result = await reportRunnerInstances({
|
|
130
|
+
scope: 'workspace',
|
|
121
131
|
workspaceId,
|
|
122
132
|
provisionerId,
|
|
123
133
|
events: [
|
|
@@ -140,6 +150,7 @@ describe('reportRunnerInstances', () => {
|
|
|
140
150
|
const reportedAt = new Date();
|
|
141
151
|
|
|
142
152
|
const result = await reportRunnerInstances({
|
|
153
|
+
scope: 'workspace',
|
|
143
154
|
workspaceId,
|
|
144
155
|
provisionerId,
|
|
145
156
|
events: [
|
|
@@ -160,6 +171,7 @@ describe('reportRunnerInstances', () => {
|
|
|
160
171
|
it('accepts delayed events that move the lifecycle forward', async () => {
|
|
161
172
|
const newest = new Date();
|
|
162
173
|
await reportRunnerInstances({
|
|
174
|
+
scope: 'workspace',
|
|
163
175
|
workspaceId,
|
|
164
176
|
provisionerId,
|
|
165
177
|
events: [
|
|
@@ -168,6 +180,7 @@ describe('reportRunnerInstances', () => {
|
|
|
168
180
|
});
|
|
169
181
|
|
|
170
182
|
await reportRunnerInstances({
|
|
183
|
+
scope: 'workspace',
|
|
171
184
|
workspaceId,
|
|
172
185
|
provisionerId,
|
|
173
186
|
events: [
|
|
@@ -188,6 +201,7 @@ describe('reportRunnerInstances', () => {
|
|
|
188
201
|
it('rejects older out-of-order events in the same lifecycle state', async () => {
|
|
189
202
|
const newest = new Date();
|
|
190
203
|
await reportRunnerInstances({
|
|
204
|
+
scope: 'workspace',
|
|
191
205
|
workspaceId,
|
|
192
206
|
provisionerId,
|
|
193
207
|
events: [
|
|
@@ -201,6 +215,7 @@ describe('reportRunnerInstances', () => {
|
|
|
201
215
|
});
|
|
202
216
|
|
|
203
217
|
await reportRunnerInstances({
|
|
218
|
+
scope: 'workspace',
|
|
204
219
|
workspaceId,
|
|
205
220
|
provisionerId,
|
|
206
221
|
events: [
|
|
@@ -218,10 +233,144 @@ describe('reportRunnerInstances', () => {
|
|
|
218
233
|
expect(rows[0]?.reason).toBe('fresh');
|
|
219
234
|
});
|
|
220
235
|
|
|
236
|
+
it('preserves a rebound reservation through stale reports and releases it on termination', async () => {
|
|
237
|
+
const staleReservationId = await createReservation(1);
|
|
238
|
+
await db()
|
|
239
|
+
.update(reservations)
|
|
240
|
+
.set({expiresAt: new Date(Date.now() - 60_000)})
|
|
241
|
+
.where(eq(reservations.id, staleReservationId));
|
|
242
|
+
const reboundReservationId = await createReservation(2);
|
|
243
|
+
const initialReportedAt = new Date(Date.now() - 120_000);
|
|
244
|
+
const runningReportedAt = new Date(Date.now() - 60_000);
|
|
245
|
+
await providerRunnerFactory.create({
|
|
246
|
+
workspaceId,
|
|
247
|
+
provisionerId,
|
|
248
|
+
providerRunnerId: 'rebound-runner',
|
|
249
|
+
reservationId: reboundReservationId,
|
|
250
|
+
state: 'running',
|
|
251
|
+
reportedAt: initialReportedAt,
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const runningReport = await reportRunnerInstances({
|
|
255
|
+
scope: 'workspace',
|
|
256
|
+
workspaceId,
|
|
257
|
+
provisionerId,
|
|
258
|
+
events: [
|
|
259
|
+
event({
|
|
260
|
+
providerRunnerId: 'rebound-runner',
|
|
261
|
+
reservationId: staleReservationId,
|
|
262
|
+
reportedAt: runningReportedAt,
|
|
263
|
+
}),
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
const terminalReport = await reportRunnerInstances({
|
|
267
|
+
scope: 'workspace',
|
|
268
|
+
workspaceId,
|
|
269
|
+
provisionerId,
|
|
270
|
+
events: [
|
|
271
|
+
event({
|
|
272
|
+
providerRunnerId: 'rebound-runner',
|
|
273
|
+
reservationId: staleReservationId,
|
|
274
|
+
state: 'failed',
|
|
275
|
+
reportedAt: new Date(runningReportedAt.getTime() + 1_000),
|
|
276
|
+
}),
|
|
277
|
+
],
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
const [runner] = await db()
|
|
281
|
+
.select()
|
|
282
|
+
.from(providerRunners)
|
|
283
|
+
.where(eq(providerRunners.providerRunnerId, 'rebound-runner'));
|
|
284
|
+
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
285
|
+
expect(runningReport).toEqual({
|
|
286
|
+
accepted: 1,
|
|
287
|
+
reservationsReleased: 0,
|
|
288
|
+
terminateIntentsHonored: [],
|
|
289
|
+
});
|
|
290
|
+
expect(terminalReport).toEqual({
|
|
291
|
+
accepted: 1,
|
|
292
|
+
reservationsReleased: 1,
|
|
293
|
+
terminateIntentsHonored: [],
|
|
294
|
+
});
|
|
295
|
+
expect(runner).toMatchObject({
|
|
296
|
+
reservationId: reboundReservationId,
|
|
297
|
+
reservationReleasedAt: expect.any(Date),
|
|
298
|
+
});
|
|
299
|
+
expect(
|
|
300
|
+
reservationRows.find((reservation) => reservation.id === reboundReservationId)?.count,
|
|
301
|
+
).toBe(1);
|
|
302
|
+
expect(
|
|
303
|
+
reservationRows.find((reservation) => reservation.id === staleReservationId)?.count,
|
|
304
|
+
).toBe(1);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('keeps the stored reservation when a report carries a stale reservation id', async () => {
|
|
308
|
+
const staleReservationId = await createReservation(1);
|
|
309
|
+
const boundReservationId = await createReservation(1);
|
|
310
|
+
await db()
|
|
311
|
+
.insert(providerRunners)
|
|
312
|
+
.values({
|
|
313
|
+
workspaceId,
|
|
314
|
+
provisionerId,
|
|
315
|
+
reservationId: boundReservationId,
|
|
316
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
317
|
+
state: 'running',
|
|
318
|
+
reportedAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
await reportRunnerInstances({
|
|
322
|
+
scope: 'workspace',
|
|
323
|
+
workspaceId,
|
|
324
|
+
provisionerId,
|
|
325
|
+
events: [
|
|
326
|
+
event({
|
|
327
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
328
|
+
reservationId: staleReservationId,
|
|
329
|
+
state: 'running',
|
|
330
|
+
reportedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
331
|
+
}),
|
|
332
|
+
],
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
336
|
+
expect(rows[0]?.reservationId).toBe(boundReservationId);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
it('adopts a reported reservation id when the runner carries none', async () => {
|
|
340
|
+
const reservationId = await createReservation(1);
|
|
341
|
+
await db()
|
|
342
|
+
.insert(providerRunners)
|
|
343
|
+
.values({
|
|
344
|
+
workspaceId,
|
|
345
|
+
provisionerId,
|
|
346
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
347
|
+
state: 'starting',
|
|
348
|
+
reportedAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
await reportRunnerInstances({
|
|
352
|
+
scope: 'workspace',
|
|
353
|
+
workspaceId,
|
|
354
|
+
provisionerId,
|
|
355
|
+
events: [
|
|
356
|
+
event({
|
|
357
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
358
|
+
reservationId,
|
|
359
|
+
state: 'running',
|
|
360
|
+
reportedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
361
|
+
}),
|
|
362
|
+
],
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const rows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
366
|
+
expect(rows[0]?.reservationId).toBe(reservationId);
|
|
367
|
+
});
|
|
368
|
+
|
|
221
369
|
it('does not let equal-timestamp lower-priority reports flip terminal state', async () => {
|
|
222
370
|
const reservationId = await createReservation(1);
|
|
223
371
|
const reportedAt = new Date('2025-01-01T00:00:00.000Z');
|
|
224
372
|
await reportRunnerInstances({
|
|
373
|
+
scope: 'workspace',
|
|
225
374
|
workspaceId,
|
|
226
375
|
provisionerId,
|
|
227
376
|
events: [
|
|
@@ -235,6 +384,7 @@ describe('reportRunnerInstances', () => {
|
|
|
235
384
|
});
|
|
236
385
|
|
|
237
386
|
const terminal = await reportRunnerInstances({
|
|
387
|
+
scope: 'workspace',
|
|
238
388
|
workspaceId,
|
|
239
389
|
provisionerId,
|
|
240
390
|
events: [
|
|
@@ -247,6 +397,7 @@ describe('reportRunnerInstances', () => {
|
|
|
247
397
|
],
|
|
248
398
|
});
|
|
249
399
|
const revived = await reportRunnerInstances({
|
|
400
|
+
scope: 'workspace',
|
|
250
401
|
workspaceId,
|
|
251
402
|
provisionerId,
|
|
252
403
|
events: [
|
|
@@ -270,6 +421,7 @@ describe('reportRunnerInstances', () => {
|
|
|
270
421
|
|
|
271
422
|
it('clamps future reported times so they do not pin provisioned runner state', async () => {
|
|
272
423
|
await reportRunnerInstances({
|
|
424
|
+
scope: 'workspace',
|
|
273
425
|
workspaceId,
|
|
274
426
|
provisionerId,
|
|
275
427
|
events: [
|
|
@@ -282,6 +434,7 @@ describe('reportRunnerInstances', () => {
|
|
|
282
434
|
});
|
|
283
435
|
|
|
284
436
|
await reportRunnerInstances({
|
|
437
|
+
scope: 'workspace',
|
|
285
438
|
workspaceId,
|
|
286
439
|
provisionerId,
|
|
287
440
|
events: [
|
|
@@ -305,6 +458,7 @@ describe('reportRunnerInstances', () => {
|
|
|
305
458
|
const terminatedAt = new Date('2025-01-01T00:03:00.000Z');
|
|
306
459
|
|
|
307
460
|
const result = await reportRunnerInstances({
|
|
461
|
+
scope: 'workspace',
|
|
308
462
|
workspaceId,
|
|
309
463
|
provisionerId,
|
|
310
464
|
events: [
|
|
@@ -346,6 +500,7 @@ describe('reportRunnerInstances', () => {
|
|
|
346
500
|
const terminatedAt = new Date('2025-01-01T00:03:00.000Z');
|
|
347
501
|
const startedAt = new Date('2025-01-01T00:00:00.000Z');
|
|
348
502
|
await reportRunnerInstances({
|
|
503
|
+
scope: 'workspace',
|
|
349
504
|
workspaceId,
|
|
350
505
|
provisionerId,
|
|
351
506
|
events: [
|
|
@@ -358,6 +513,7 @@ describe('reportRunnerInstances', () => {
|
|
|
358
513
|
});
|
|
359
514
|
|
|
360
515
|
await reportRunnerInstances({
|
|
516
|
+
scope: 'workspace',
|
|
361
517
|
workspaceId,
|
|
362
518
|
provisionerId,
|
|
363
519
|
events: [
|
|
@@ -378,6 +534,7 @@ describe('reportRunnerInstances', () => {
|
|
|
378
534
|
|
|
379
535
|
it('uses server update time for active provisioned runner windows', async () => {
|
|
380
536
|
await reportRunnerInstances({
|
|
537
|
+
scope: 'workspace',
|
|
381
538
|
workspaceId,
|
|
382
539
|
provisionerId,
|
|
383
540
|
events: [
|
|
@@ -401,6 +558,7 @@ describe('reportRunnerInstances', () => {
|
|
|
401
558
|
const reservationId = await createReservation(2);
|
|
402
559
|
|
|
403
560
|
const result = await reportRunnerInstances({
|
|
561
|
+
scope: 'workspace',
|
|
404
562
|
workspaceId,
|
|
405
563
|
provisionerId,
|
|
406
564
|
events: [event({providerRunnerId: 'provisioned-runner-1', reservationId, state: 'failed'})],
|
|
@@ -429,6 +587,7 @@ describe('reportRunnerInstances', () => {
|
|
|
429
587
|
if (!runner.providerRunnerId) throw new Error('Runner instance provider id missing');
|
|
430
588
|
|
|
431
589
|
const result = await reportRunnerInstances({
|
|
590
|
+
scope: 'workspace',
|
|
432
591
|
workspaceId,
|
|
433
592
|
provisionerId,
|
|
434
593
|
events: [event({providerRunnerId: runner.providerRunnerId, state: 'failed'})],
|
|
@@ -479,6 +638,7 @@ describe('reportRunnerInstances', () => {
|
|
|
479
638
|
});
|
|
480
639
|
|
|
481
640
|
const result = await reportRunnerInstances({
|
|
641
|
+
scope: 'installation',
|
|
482
642
|
workspaceId: null,
|
|
483
643
|
provisionerId: installationProvisioner.id,
|
|
484
644
|
events: [event({providerRunnerId: 'installation-pre-enrollment-runner', state: 'failed'})],
|
|
@@ -521,6 +681,7 @@ describe('reportRunnerInstances', () => {
|
|
|
521
681
|
});
|
|
522
682
|
|
|
523
683
|
const result = await reportRunnerInstances({
|
|
684
|
+
scope: 'installation',
|
|
524
685
|
workspaceId: null,
|
|
525
686
|
provisionerId: installationProvisioner.id,
|
|
526
687
|
events: [
|
|
@@ -552,6 +713,7 @@ describe('reportRunnerInstances', () => {
|
|
|
552
713
|
});
|
|
553
714
|
|
|
554
715
|
const result = await reportRunnerInstances({
|
|
716
|
+
scope: 'workspace',
|
|
555
717
|
workspaceId,
|
|
556
718
|
provisionerId,
|
|
557
719
|
events: [
|
|
@@ -581,11 +743,13 @@ describe('reportRunnerInstances', () => {
|
|
|
581
743
|
const reservationId = await createReservation(2);
|
|
582
744
|
|
|
583
745
|
await reportRunnerInstances({
|
|
746
|
+
scope: 'workspace',
|
|
584
747
|
workspaceId,
|
|
585
748
|
provisionerId,
|
|
586
749
|
events: [event({providerRunnerId: 'provisioned-runner-1', reservationId, state: 'failed'})],
|
|
587
750
|
});
|
|
588
751
|
const result = await reportRunnerInstances({
|
|
752
|
+
scope: 'workspace',
|
|
589
753
|
workspaceId,
|
|
590
754
|
provisionerId,
|
|
591
755
|
events: [event({providerRunnerId: 'provisioned-runner-1', reservationId, state: 'failed'})],
|
|
@@ -600,6 +764,7 @@ describe('reportRunnerInstances', () => {
|
|
|
600
764
|
const reservationId = await createReservation(2);
|
|
601
765
|
const failedAt = new Date();
|
|
602
766
|
await reportRunnerInstances({
|
|
767
|
+
scope: 'workspace',
|
|
603
768
|
workspaceId,
|
|
604
769
|
provisionerId,
|
|
605
770
|
events: [
|
|
@@ -613,6 +778,7 @@ describe('reportRunnerInstances', () => {
|
|
|
613
778
|
});
|
|
614
779
|
|
|
615
780
|
await reportRunnerInstances({
|
|
781
|
+
scope: 'workspace',
|
|
616
782
|
workspaceId,
|
|
617
783
|
provisionerId,
|
|
618
784
|
events: [
|
|
@@ -636,6 +802,7 @@ describe('reportRunnerInstances', () => {
|
|
|
636
802
|
const reservationId = await createReservation(2);
|
|
637
803
|
|
|
638
804
|
const result = await reportRunnerInstances({
|
|
805
|
+
scope: 'workspace',
|
|
639
806
|
workspaceId,
|
|
640
807
|
provisionerId,
|
|
641
808
|
events: [
|
|
@@ -656,6 +823,7 @@ describe('reportRunnerInstances', () => {
|
|
|
656
823
|
const reservationId = await createReservation(3);
|
|
657
824
|
|
|
658
825
|
const result = await reportRunnerInstances({
|
|
826
|
+
scope: 'workspace',
|
|
659
827
|
workspaceId,
|
|
660
828
|
provisionerId,
|
|
661
829
|
events: [
|
|
@@ -673,6 +841,7 @@ describe('reportRunnerInstances', () => {
|
|
|
673
841
|
const reservationId = await createReservation(1);
|
|
674
842
|
|
|
675
843
|
const result = await reportRunnerInstances({
|
|
844
|
+
scope: 'workspace',
|
|
676
845
|
workspaceId,
|
|
677
846
|
provisionerId,
|
|
678
847
|
events: [event({providerRunnerId: 'provisioned-runner-1', reservationId, state: 'failed'})],
|
|
@@ -695,6 +864,7 @@ describe('reportRunnerInstances', () => {
|
|
|
695
864
|
if (!reservation) throw new Error('Expected reservation');
|
|
696
865
|
|
|
697
866
|
const result = await reportRunnerInstances({
|
|
867
|
+
scope: 'workspace',
|
|
698
868
|
workspaceId,
|
|
699
869
|
provisionerId,
|
|
700
870
|
events: [
|
|
@@ -715,6 +885,7 @@ describe('reportRunnerInstances', () => {
|
|
|
715
885
|
const reservationId = await createReservation(1);
|
|
716
886
|
|
|
717
887
|
const result = await reportRunnerInstances({
|
|
888
|
+
scope: 'workspace',
|
|
718
889
|
workspaceId,
|
|
719
890
|
provisionerId,
|
|
720
891
|
events: [
|
|
@@ -750,6 +921,7 @@ describe('reportRunnerInstances', () => {
|
|
|
750
921
|
});
|
|
751
922
|
|
|
752
923
|
const result = await reportRunnerInstances({
|
|
924
|
+
scope: 'workspace',
|
|
753
925
|
workspaceId,
|
|
754
926
|
provisionerId,
|
|
755
927
|
events: [
|
|
@@ -776,6 +948,7 @@ describe('reportRunnerInstances', () => {
|
|
|
776
948
|
const reportedAt = new Date('2025-01-01T00:00:00.000Z');
|
|
777
949
|
|
|
778
950
|
const result = await reportRunnerInstances({
|
|
951
|
+
scope: 'workspace',
|
|
779
952
|
workspaceId,
|
|
780
953
|
provisionerId,
|
|
781
954
|
events: [
|
|
@@ -819,11 +992,13 @@ describe('reportRunnerInstances', () => {
|
|
|
819
992
|
});
|
|
820
993
|
|
|
821
994
|
const first = await reportRunnerInstances({
|
|
995
|
+
scope: 'workspace',
|
|
822
996
|
workspaceId,
|
|
823
997
|
provisionerId,
|
|
824
998
|
events: [event({providerRunnerId: 'provisioned-runner-1', state: 'terminated'})],
|
|
825
999
|
});
|
|
826
1000
|
const second = await reportRunnerInstances({
|
|
1001
|
+
scope: 'workspace',
|
|
827
1002
|
workspaceId,
|
|
828
1003
|
provisionerId,
|
|
829
1004
|
events: [event({providerRunnerId: 'provisioned-runner-1', state: 'terminated'})],
|
|
@@ -1365,6 +1540,7 @@ describe('reapStaleRunnerInstances', () => {
|
|
|
1365
1540
|
|
|
1366
1541
|
await lockHolderReady.promise;
|
|
1367
1542
|
const report = reportRunnerInstances({
|
|
1543
|
+
scope: 'workspace',
|
|
1368
1544
|
workspaceId,
|
|
1369
1545
|
provisionerId,
|
|
1370
1546
|
events: [
|
|
@@ -1845,6 +2021,7 @@ describe('reconcileRunnerInstances', () => {
|
|
|
1845
2021
|
});
|
|
1846
2022
|
|
|
1847
2023
|
await reportRunnerInstances({
|
|
2024
|
+
scope: 'workspace',
|
|
1848
2025
|
workspaceId,
|
|
1849
2026
|
provisionerId,
|
|
1850
2027
|
events: [
|
|
@@ -1877,6 +2054,7 @@ describe('reconcileRunnerInstances', () => {
|
|
|
1877
2054
|
|
|
1878
2055
|
await lockHolderReady.promise;
|
|
1879
2056
|
const report = reportRunnerInstances({
|
|
2057
|
+
scope: 'workspace',
|
|
1880
2058
|
workspaceId,
|
|
1881
2059
|
provisionerId,
|
|
1882
2060
|
events: [
|
|
@@ -2003,6 +2181,102 @@ describe('reconcileRunnerInstances', () => {
|
|
|
2003
2181
|
}
|
|
2004
2182
|
});
|
|
2005
2183
|
|
|
2184
|
+
describe('countStaleEnrolledRunnerInstances', () => {
|
|
2185
|
+
let provisionerId: string;
|
|
2186
|
+
|
|
2187
|
+
beforeEach(() => {
|
|
2188
|
+
provisionerId = crypto.randomUUID();
|
|
2189
|
+
});
|
|
2190
|
+
|
|
2191
|
+
function staleAt(): Date {
|
|
2192
|
+
return new Date(Date.now() - 120_000);
|
|
2193
|
+
}
|
|
2194
|
+
|
|
2195
|
+
it('counts only stale reported runners with a live control session and no assignment', async () => {
|
|
2196
|
+
const baseline = await countStaleEnrolledRunnerInstances({graceSeconds: 60});
|
|
2197
|
+
const stale = await createRunner({updatedAt: staleAt()});
|
|
2198
|
+
await createControlSession(stale);
|
|
2199
|
+
|
|
2200
|
+
const fresh = await createRunner({updatedAt: new Date()});
|
|
2201
|
+
await createControlSession(fresh);
|
|
2202
|
+
|
|
2203
|
+
const recentlyMutated = await createRunner({reportedAt: staleAt(), updatedAt: new Date()});
|
|
2204
|
+
await createControlSession(recentlyMutated);
|
|
2205
|
+
|
|
2206
|
+
const freshReport = await createRunner({reportedAt: new Date(), updatedAt: staleAt()});
|
|
2207
|
+
await createControlSession(freshReport);
|
|
2208
|
+
|
|
2209
|
+
const assigned = await createRunner({workspaceId: crypto.randomUUID(), updatedAt: staleAt()});
|
|
2210
|
+
await createControlSession(assigned);
|
|
2211
|
+
|
|
2212
|
+
const activated = await createRunner({
|
|
2213
|
+
runnerSessionId: crypto.randomUUID(),
|
|
2214
|
+
updatedAt: staleAt(),
|
|
2215
|
+
});
|
|
2216
|
+
await createControlSession(activated);
|
|
2217
|
+
|
|
2218
|
+
await createRunner({updatedAt: staleAt()});
|
|
2219
|
+
const expiredControlSession = await createRunner({updatedAt: staleAt()});
|
|
2220
|
+
await createControlSession(expiredControlSession, {
|
|
2221
|
+
expiresAt: new Date(Date.now() - 1_000),
|
|
2222
|
+
});
|
|
2223
|
+
const closedControlSession = await createRunner({updatedAt: staleAt()});
|
|
2224
|
+
await createControlSession(closedControlSession, {
|
|
2225
|
+
closedAt: new Date(),
|
|
2226
|
+
});
|
|
2227
|
+
const stopped = await createRunner({state: 'stopped', updatedAt: staleAt()});
|
|
2228
|
+
await createControlSession(stopped);
|
|
2229
|
+
|
|
2230
|
+
const count = await countStaleEnrolledRunnerInstances({graceSeconds: 60});
|
|
2231
|
+
|
|
2232
|
+
expect(count - baseline).toBe(2);
|
|
2233
|
+
});
|
|
2234
|
+
|
|
2235
|
+
async function createRunner(params: {
|
|
2236
|
+
state?: 'running' | 'stopped';
|
|
2237
|
+
workspaceId?: string | null;
|
|
2238
|
+
runnerSessionId?: string | null;
|
|
2239
|
+
reportedAt?: Date;
|
|
2240
|
+
updatedAt?: Date;
|
|
2241
|
+
}) {
|
|
2242
|
+
const reportedAt = params.reportedAt ?? params.updatedAt ?? new Date();
|
|
2243
|
+
const [runner] = await db()
|
|
2244
|
+
.insert(providerRunners)
|
|
2245
|
+
.values({
|
|
2246
|
+
workspaceId: params.workspaceId ?? null,
|
|
2247
|
+
provisionerId,
|
|
2248
|
+
providerRunnerId: crypto.randomUUID(),
|
|
2249
|
+
templateKey: 'linux',
|
|
2250
|
+
labels: ['linux'],
|
|
2251
|
+
state: params.state ?? 'running',
|
|
2252
|
+
runnerSessionId: params.runnerSessionId ?? null,
|
|
2253
|
+
providerKind: 'docker',
|
|
2254
|
+
reportedAt,
|
|
2255
|
+
updatedAt: params.updatedAt ?? reportedAt,
|
|
2256
|
+
})
|
|
2257
|
+
.returning({id: providerRunners.id});
|
|
2258
|
+
if (!runner) throw new Error('Expected runner instance');
|
|
2259
|
+
return runner.id;
|
|
2260
|
+
}
|
|
2261
|
+
|
|
2262
|
+
async function createControlSession(
|
|
2263
|
+
runnerInstanceId: string,
|
|
2264
|
+
params: {expiresAt?: Date; closedAt?: Date} = {},
|
|
2265
|
+
) {
|
|
2266
|
+
await db()
|
|
2267
|
+
.insert(runnerControlSessions)
|
|
2268
|
+
.values({
|
|
2269
|
+
runnerInstanceId,
|
|
2270
|
+
provisionerId,
|
|
2271
|
+
hashedToken: crypto.randomUUID(),
|
|
2272
|
+
prefix: 'test',
|
|
2273
|
+
expiresAt: params.expiresAt ?? new Date(Date.now() + 60_000),
|
|
2274
|
+
closedAt: params.closedAt,
|
|
2275
|
+
closeReason: params.closedAt ? 'test' : null,
|
|
2276
|
+
});
|
|
2277
|
+
}
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2006
2280
|
describe('runner instance provider attachment', () => {
|
|
2007
2281
|
it('updates the pre-created runner instance before its first lifecycle report', async () => {
|
|
2008
2282
|
const provisionerId = crypto.randomUUID();
|
|
@@ -2026,6 +2300,7 @@ describe('runner instance provider attachment', () => {
|
|
|
2026
2300
|
providerRunnerId,
|
|
2027
2301
|
});
|
|
2028
2302
|
await reportRunnerInstances({
|
|
2303
|
+
scope: 'installation',
|
|
2029
2304
|
workspaceId: null,
|
|
2030
2305
|
provisionerId,
|
|
2031
2306
|
events: [
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {logger} from '@shipfox/node-opentelemetry';
|
|
2
|
-
import {canonicalizeLabels} from '@shipfox/runner-labels';
|
|
3
2
|
import {
|
|
4
3
|
and,
|
|
5
4
|
asc,
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
} from 'drizzle-orm';
|
|
20
19
|
import {alias} from 'drizzle-orm/pg-core';
|
|
21
20
|
import type {RunnerInstance, RunnerInstanceState} from '#core/entities/runner-instance.js';
|
|
21
|
+
import {sanitizeRunnerLabels} from '#core/runner-labels.js';
|
|
22
22
|
import type {Tx} from './db.js';
|
|
23
23
|
import {db} from './db.js';
|
|
24
24
|
import {
|
|
@@ -29,6 +29,7 @@ import {releaseReservationUnits} from './reservations.js';
|
|
|
29
29
|
import {ephemeralRegistrationTokens} from './schema/ephemeral-registration-tokens.js';
|
|
30
30
|
import {provisionerTokens} from './schema/provisioner-tokens.js';
|
|
31
31
|
import {reservations} from './schema/reservations.js';
|
|
32
|
+
import {runnerControlSessions} from './schema/runner-control-sessions.js';
|
|
32
33
|
import {providerRunners, toRunnerInstance} from './schema/runner-instances.js';
|
|
33
34
|
import {runnerSessions} from './schema/runner-sessions.js';
|
|
34
35
|
import {runningJobExecutions} from './schema/running-job-executions.js';
|
|
@@ -74,6 +75,7 @@ export interface RunnerInstanceReportEvent {
|
|
|
74
75
|
}
|
|
75
76
|
|
|
76
77
|
export interface ReportRunnerInstancesParams {
|
|
78
|
+
scope: 'installation' | 'workspace';
|
|
77
79
|
workspaceId: string | null;
|
|
78
80
|
provisionerId: string;
|
|
79
81
|
events: RunnerInstanceReportEvent[];
|
|
@@ -165,7 +167,11 @@ export async function reportRunnerInstances(params: ReportRunnerInstancesParams)
|
|
|
165
167
|
providerRunnerId: event.providerRunnerId,
|
|
166
168
|
reservationId: event.reservationId,
|
|
167
169
|
templateKey: event.templateKey,
|
|
168
|
-
labels:
|
|
170
|
+
labels: sanitizeRunnerLabels(event.labels, {
|
|
171
|
+
scope: params.scope,
|
|
172
|
+
logLevel: 'debug',
|
|
173
|
+
source: 'runner instance report',
|
|
174
|
+
}),
|
|
169
175
|
state: event.state,
|
|
170
176
|
reason: event.reason,
|
|
171
177
|
runnerSessionId: event.runnerSessionId,
|
|
@@ -185,7 +191,7 @@ export async function reportRunnerInstances(params: ReportRunnerInstancesParams)
|
|
|
185
191
|
target: [providerRunners.provisionerId, providerRunners.providerRunnerId],
|
|
186
192
|
targetWhere: isNotNull(providerRunners.providerRunnerId),
|
|
187
193
|
set: {
|
|
188
|
-
reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(
|
|
194
|
+
reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(${providerRunners.reservationId}, excluded.reservation_id) ELSE ${providerRunners.reservationId} END`,
|
|
189
195
|
templateKey: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(excluded.template_key, ${providerRunners.templateKey}) ELSE ${providerRunners.templateKey} END`,
|
|
190
196
|
labels: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.labels ELSE ${providerRunners.labels} END`,
|
|
191
197
|
state: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.state ELSE ${providerRunners.state} END`,
|
|
@@ -242,6 +248,36 @@ export async function listActiveRunnerInstanceCountsByTemplateTx(
|
|
|
242
248
|
);
|
|
243
249
|
}
|
|
244
250
|
|
|
251
|
+
export async function countStaleEnrolledRunnerInstances(params: {
|
|
252
|
+
graceSeconds: number;
|
|
253
|
+
}): Promise<number> {
|
|
254
|
+
const [row] = await db()
|
|
255
|
+
.select({count: sql<number>`count(*)::int`})
|
|
256
|
+
.from(providerRunners)
|
|
257
|
+
.where(
|
|
258
|
+
and(
|
|
259
|
+
eq(providerRunners.state, 'running'),
|
|
260
|
+
isNull(providerRunners.workspaceId),
|
|
261
|
+
isNull(providerRunners.runnerSessionId),
|
|
262
|
+
lt(providerRunners.reportedAt, staleRunnerInstanceCutoff(params.graceSeconds)),
|
|
263
|
+
exists(
|
|
264
|
+
db()
|
|
265
|
+
.select({id: runnerControlSessions.id})
|
|
266
|
+
.from(runnerControlSessions)
|
|
267
|
+
.where(
|
|
268
|
+
and(
|
|
269
|
+
eq(runnerControlSessions.runnerInstanceId, providerRunners.id),
|
|
270
|
+
isNull(runnerControlSessions.closedAt),
|
|
271
|
+
gt(runnerControlSessions.expiresAt, sql`now()`),
|
|
272
|
+
),
|
|
273
|
+
),
|
|
274
|
+
),
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
return row?.count ?? 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
245
281
|
export async function listActiveRunnerInstances(params: {
|
|
246
282
|
workspaceId: string;
|
|
247
283
|
windowSeconds: number;
|