@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
|
@@ -3,6 +3,7 @@ import {and, eq, inArray} from 'drizzle-orm';
|
|
|
3
3
|
import {
|
|
4
4
|
ActiveEphemeralRegistrationTokensExistError,
|
|
5
5
|
type RegistrationTokenBatchExceedsReservationError,
|
|
6
|
+
ReservationNotFoundError,
|
|
6
7
|
} from '#core/errors.js';
|
|
7
8
|
import {db} from '#db/db.js';
|
|
8
9
|
import {
|
|
@@ -56,6 +57,20 @@ describe('createEphemeralRegistrationTokensBatch', () => {
|
|
|
56
57
|
).toEqual(expect.objectContaining({providerRunnerId: 'provisioned-runner-a'}));
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
it('rejects bound reservations', async () => {
|
|
61
|
+
const boundReservationId = await createReservation({count: 1, kind: 'bound'});
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
createEphemeralRegistrationTokensBatch({
|
|
65
|
+
workspaceId,
|
|
66
|
+
provisionerId,
|
|
67
|
+
reservationId: boundReservationId,
|
|
68
|
+
expiresAt: new Date(Date.now() + 300_000),
|
|
69
|
+
rows: [row('bound-runner', generateOpaqueToken('ephemeralRegistrationToken'))],
|
|
70
|
+
}),
|
|
71
|
+
).rejects.toBeInstanceOf(ReservationNotFoundError);
|
|
72
|
+
});
|
|
73
|
+
|
|
59
74
|
it('rejects the batch and inserts no rows when any provisioned runner already has an active token', async () => {
|
|
60
75
|
await ephemeralRegistrationTokenFactory.create({
|
|
61
76
|
workspaceId,
|
|
@@ -186,7 +201,10 @@ describe('createEphemeralRegistrationTokensBatch', () => {
|
|
|
186
201
|
} satisfies Partial<RegistrationTokenBatchExceedsReservationError>);
|
|
187
202
|
});
|
|
188
203
|
|
|
189
|
-
async function createReservation(params: {
|
|
204
|
+
async function createReservation(params: {
|
|
205
|
+
count: number;
|
|
206
|
+
kind?: 'bound' | 'launch';
|
|
207
|
+
}): Promise<string> {
|
|
190
208
|
const [reservation] = await db()
|
|
191
209
|
.insert(reservations)
|
|
192
210
|
.values({
|
|
@@ -194,6 +212,7 @@ describe('createEphemeralRegistrationTokensBatch', () => {
|
|
|
194
212
|
provisionerId,
|
|
195
213
|
requiredLabels: ['linux'],
|
|
196
214
|
count: params.count,
|
|
215
|
+
kind: params.kind ?? 'launch',
|
|
197
216
|
expiresAt: new Date(Date.now() + 60_000),
|
|
198
217
|
})
|
|
199
218
|
.returning({id: reservations.id});
|
|
@@ -133,6 +133,7 @@ export async function createEphemeralRegistrationTokensBatch(
|
|
|
133
133
|
eq(reservations.id, params.reservationId),
|
|
134
134
|
eq(reservations.workspaceId, params.workspaceId),
|
|
135
135
|
eq(reservations.provisionerId, params.provisionerId),
|
|
136
|
+
eq(reservations.kind, 'launch'),
|
|
136
137
|
),
|
|
137
138
|
)
|
|
138
139
|
.limit(1);
|
|
@@ -8,6 +8,7 @@ import {eq, sql} from 'drizzle-orm';
|
|
|
8
8
|
import {EmptyRequiredLabelsError, RunnerSessionExhaustedError} from '#core/errors.js';
|
|
9
9
|
import {claimJobExecution} from '#core/job-executions.js';
|
|
10
10
|
import {detectAndExpireStuckJobs} from '#core/maintenance.js';
|
|
11
|
+
import * as runnerMetrics from '#metrics/instance.js';
|
|
11
12
|
import {
|
|
12
13
|
getLeaseTokenClaims,
|
|
13
14
|
pendingJobFactory,
|
|
@@ -242,20 +243,95 @@ describe('claimPendingJobExecution', () => {
|
|
|
242
243
|
expect(new Date(payload.claimedAt).getTime()).toBe(running?.startedAt.getTime());
|
|
243
244
|
});
|
|
244
245
|
|
|
246
|
+
it('records queue time from the pending row creation to the runner claim', async () => {
|
|
247
|
+
const created = await pendingJobFactory.create({workspaceId});
|
|
248
|
+
await db()
|
|
249
|
+
.update(pendingJobExecutions)
|
|
250
|
+
.set({createdAt: sql`now() - interval '60 seconds'`})
|
|
251
|
+
.where(eq(pendingJobExecutions.jobExecutionId, created.jobExecutionId));
|
|
252
|
+
const [pending] = await db()
|
|
253
|
+
.select({createdAt: pendingJobExecutions.createdAt})
|
|
254
|
+
.from(pendingJobExecutions)
|
|
255
|
+
.where(eq(pendingJobExecutions.jobExecutionId, created.jobExecutionId));
|
|
256
|
+
const recordQueueTime = vi.spyOn(runnerMetrics, 'recordJobExecutionQueueTime');
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
await claimPendingJobExecution({workspaceId, runnerSessionId, maxClaims: null});
|
|
260
|
+
|
|
261
|
+
const [running] = await db()
|
|
262
|
+
.select({startedAt: runningJobExecutions.startedAt})
|
|
263
|
+
.from(runningJobExecutions)
|
|
264
|
+
.where(eq(runningJobExecutions.jobExecutionId, created.jobExecutionId));
|
|
265
|
+
const observation = recordQueueTime.mock.calls[0]?.[0];
|
|
266
|
+
if (!pending || !running) throw new Error('Expected pending and running job rows');
|
|
267
|
+
expect(recordQueueTime).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(observation).toMatchObject({provider: null, launchKind: 'manual'});
|
|
269
|
+
expect(observation?.durationMilliseconds).toBe(
|
|
270
|
+
running.startedAt.getTime() - pending.createdAt.getTime(),
|
|
271
|
+
);
|
|
272
|
+
expect(observation?.durationMilliseconds).toBeGreaterThanOrEqual(60_000);
|
|
273
|
+
expect(observation?.durationMilliseconds).toBeLessThan(120_000);
|
|
274
|
+
} finally {
|
|
275
|
+
recordQueueTime.mockRestore();
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('records bounded queue labels for a provisioned runner claim', async () => {
|
|
280
|
+
const provisionerId = crypto.randomUUID();
|
|
281
|
+
const providerRunnerId = `provisioned-runner-${crypto.randomUUID()}`;
|
|
282
|
+
await db()
|
|
283
|
+
.update(runnerSessions)
|
|
284
|
+
.set({registrationTokenKind: 'ephemeral', maxClaims: 1, provisionerId, providerRunnerId})
|
|
285
|
+
.where(eq(runnerSessions.id, runnerSessionId));
|
|
286
|
+
await db().insert(providerRunners).values({
|
|
287
|
+
workspaceId,
|
|
288
|
+
provisionerId,
|
|
289
|
+
providerRunnerId,
|
|
290
|
+
providerKind: 'ec2',
|
|
291
|
+
launchKind: 'demand',
|
|
292
|
+
labels: sessionLabels,
|
|
293
|
+
state: 'running',
|
|
294
|
+
reportedAt: new Date(),
|
|
295
|
+
});
|
|
296
|
+
await pendingJobFactory.create({workspaceId});
|
|
297
|
+
const recordQueueTime = vi.spyOn(runnerMetrics, 'recordJobExecutionQueueTime');
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
await claimPendingJobExecution({workspaceId, runnerSessionId, maxClaims: 1});
|
|
301
|
+
|
|
302
|
+
expect(recordQueueTime).toHaveBeenCalledWith(
|
|
303
|
+
expect.objectContaining({provider: 'ec2', launchKind: 'demand'}),
|
|
304
|
+
);
|
|
305
|
+
expect(recordQueueTime.mock.calls[0]?.[0].durationMilliseconds).toBeGreaterThanOrEqual(0);
|
|
306
|
+
} finally {
|
|
307
|
+
recordQueueTime.mockRestore();
|
|
308
|
+
}
|
|
309
|
+
});
|
|
310
|
+
|
|
245
311
|
it('emits no claimed event when there is nothing to claim', async () => {
|
|
312
|
+
const recordQueueTime = vi.spyOn(runnerMetrics, 'recordJobExecutionQueueTime');
|
|
246
313
|
const before = await db()
|
|
247
314
|
.select()
|
|
248
315
|
.from(runnersOutbox)
|
|
249
316
|
.where(eq(runnersOutbox.eventType, RUNNER_JOB_CLAIMED));
|
|
250
317
|
|
|
251
|
-
|
|
318
|
+
try {
|
|
319
|
+
const claimed = await claimPendingJobExecution({
|
|
320
|
+
workspaceId,
|
|
321
|
+
runnerSessionId,
|
|
322
|
+
maxClaims: null,
|
|
323
|
+
});
|
|
252
324
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
325
|
+
const after = await db()
|
|
326
|
+
.select()
|
|
327
|
+
.from(runnersOutbox)
|
|
328
|
+
.where(eq(runnersOutbox.eventType, RUNNER_JOB_CLAIMED));
|
|
329
|
+
expect(claimed).toBeNull();
|
|
330
|
+
expect(after).toHaveLength(before.length);
|
|
331
|
+
expect(recordQueueTime).not.toHaveBeenCalled();
|
|
332
|
+
} finally {
|
|
333
|
+
recordQueueTime.mockRestore();
|
|
334
|
+
}
|
|
259
335
|
});
|
|
260
336
|
|
|
261
337
|
it('emits no claimed event when dropping an orphan pending row', async () => {
|
|
@@ -273,13 +349,23 @@ describe('claimPendingJobExecution', () => {
|
|
|
273
349
|
});
|
|
274
350
|
// Clear the initial claim's events so this assertion only covers the orphan claim.
|
|
275
351
|
const beforeOrphanClaim = await outboxEventsForJob(RUNNER_JOB_CLAIMED, created.jobId);
|
|
352
|
+
const recordQueueTime = vi.spyOn(runnerMetrics, 'recordJobExecutionQueueTime');
|
|
276
353
|
|
|
277
|
-
|
|
354
|
+
try {
|
|
355
|
+
const second = await claimPendingJobExecution({
|
|
356
|
+
workspaceId,
|
|
357
|
+
runnerSessionId,
|
|
358
|
+
maxClaims: null,
|
|
359
|
+
});
|
|
278
360
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
361
|
+
expect(second).toBeNull();
|
|
362
|
+
expect(await outboxEventsForJob(RUNNER_JOB_CLAIMED, created.jobId)).toHaveLength(
|
|
363
|
+
beforeOrphanClaim.length,
|
|
364
|
+
);
|
|
365
|
+
expect(recordQueueTime).not.toHaveBeenCalled();
|
|
366
|
+
} finally {
|
|
367
|
+
recordQueueTime.mockRestore();
|
|
368
|
+
}
|
|
283
369
|
});
|
|
284
370
|
|
|
285
371
|
it('returns the job ids when a job is available', async () => {
|
package/src/db/job-executions.ts
CHANGED
|
@@ -29,7 +29,14 @@ import {
|
|
|
29
29
|
RunnerSessionExhaustedError,
|
|
30
30
|
RunningJobExecutionNotFoundError,
|
|
31
31
|
} from '#core/errors.js';
|
|
32
|
-
import {
|
|
32
|
+
import {
|
|
33
|
+
type JobExecutionQueueTimeObservation,
|
|
34
|
+
jobExecutionEnqueuedCount,
|
|
35
|
+
jobExecutionLeaseExpiredCount,
|
|
36
|
+
type ProviderRunnerLifecycleObservation,
|
|
37
|
+
recordJobExecutionQueueTime,
|
|
38
|
+
recordProviderRunnerActivationToFirstClaim,
|
|
39
|
+
} from '#metrics/instance.js';
|
|
33
40
|
import type {Tx} from './db.js';
|
|
34
41
|
import {db} from './db.js';
|
|
35
42
|
import {runnersOutbox} from './schema/outbox.js';
|
|
@@ -192,7 +199,9 @@ export async function claimPendingJobExecution(params: {
|
|
|
192
199
|
|
|
193
200
|
if (params.sessionLabels.length === 0) return null;
|
|
194
201
|
|
|
195
|
-
|
|
202
|
+
let activationToFirstClaimObservation: ProviderRunnerLifecycleObservation | null = null;
|
|
203
|
+
let queueTimeObservation: JobExecutionQueueTimeObservation | null = null;
|
|
204
|
+
const result = await db().transaction(async (tx) => {
|
|
196
205
|
let provisionerId: string | null = null;
|
|
197
206
|
let providerRunnerId: string | null = null;
|
|
198
207
|
let runnerInstanceId: string | null = null;
|
|
@@ -288,6 +297,12 @@ export async function claimPendingJobExecution(params: {
|
|
|
288
297
|
const claimed = inserted[0];
|
|
289
298
|
if (!claimed) return null;
|
|
290
299
|
|
|
300
|
+
queueTimeObservation = {
|
|
301
|
+
durationMilliseconds: claimed.claimedAt.getTime() - row.createdAt.getTime(),
|
|
302
|
+
provider: null,
|
|
303
|
+
launchKind: params.maxClaims === null ? 'manual' : 'unknown',
|
|
304
|
+
};
|
|
305
|
+
|
|
291
306
|
const runnerInstanceCondition = runnerInstanceId
|
|
292
307
|
? eq(providerRunners.id, runnerInstanceId)
|
|
293
308
|
: provisionerId && providerRunnerId
|
|
@@ -297,13 +312,42 @@ export async function claimPendingJobExecution(params: {
|
|
|
297
312
|
)
|
|
298
313
|
: null;
|
|
299
314
|
if (runnerInstanceCondition) {
|
|
300
|
-
await tx
|
|
315
|
+
const [claimedRunner] = await tx
|
|
301
316
|
.update(providerRunners)
|
|
302
317
|
.set({
|
|
303
318
|
firstClaimedAt: sql`coalesce(${providerRunners.firstClaimedAt}, ${claimed.claimedAt})`,
|
|
304
319
|
updatedAt: sql`now()`,
|
|
305
320
|
})
|
|
306
|
-
.where(runnerInstanceCondition)
|
|
321
|
+
.where(runnerInstanceCondition)
|
|
322
|
+
.returning({
|
|
323
|
+
firstClaimedAt: providerRunners.firstClaimedAt,
|
|
324
|
+
isFirstClaim: sql<boolean>`${providerRunners.firstClaimedAt} = ${claimed.claimedAt}`,
|
|
325
|
+
provider: providerRunners.providerKind,
|
|
326
|
+
launchKind: providerRunners.launchKind,
|
|
327
|
+
runnerInstanceId: providerRunners.id,
|
|
328
|
+
sessionCreatedAtEpochMs: sql<number | null>`(
|
|
329
|
+
select extract(epoch from ${runnerSessions.createdAt})::double precision * 1000
|
|
330
|
+
from ${runnerSessions}
|
|
331
|
+
where ${runnerSessions.id} = ${params.runnerSessionId}
|
|
332
|
+
)`,
|
|
333
|
+
});
|
|
334
|
+
if (claimedRunner && queueTimeObservation) {
|
|
335
|
+
queueTimeObservation.provider = claimedRunner.provider;
|
|
336
|
+
queueTimeObservation.launchKind = claimedRunner.launchKind;
|
|
337
|
+
}
|
|
338
|
+
if (
|
|
339
|
+
claimedRunner?.isFirstClaim &&
|
|
340
|
+
claimedRunner.firstClaimedAt !== null &&
|
|
341
|
+
claimedRunner.sessionCreatedAtEpochMs !== null &&
|
|
342
|
+
claimedRunner.sessionCreatedAtEpochMs !== undefined
|
|
343
|
+
)
|
|
344
|
+
activationToFirstClaimObservation = {
|
|
345
|
+
durationMilliseconds:
|
|
346
|
+
claimedRunner.firstClaimedAt.getTime() - claimedRunner.sessionCreatedAtEpochMs,
|
|
347
|
+
provider: claimedRunner.provider,
|
|
348
|
+
launchKind: claimedRunner.launchKind,
|
|
349
|
+
runnerInstanceId: claimedRunner.runnerInstanceId,
|
|
350
|
+
};
|
|
307
351
|
}
|
|
308
352
|
|
|
309
353
|
if (params.maxClaims !== null) {
|
|
@@ -335,6 +379,10 @@ export async function claimPendingJobExecution(params: {
|
|
|
335
379
|
projectId: row.projectId,
|
|
336
380
|
};
|
|
337
381
|
});
|
|
382
|
+
if (queueTimeObservation) recordJobExecutionQueueTime(queueTimeObservation);
|
|
383
|
+
if (activationToFirstClaimObservation)
|
|
384
|
+
recordProviderRunnerActivationToFirstClaim(activationToFirstClaimObservation);
|
|
385
|
+
return result;
|
|
338
386
|
}
|
|
339
387
|
|
|
340
388
|
async function touchRunnerSessionLiveness(params: {
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import {sql} from 'drizzle-orm';
|
|
2
|
+
import type {Tx} from './db.js';
|
|
3
|
+
|
|
4
|
+
export async function lockRunnerReservationAdvisoryKeysTx(
|
|
5
|
+
tx: Tx,
|
|
6
|
+
params: {provisionerId: string; reservationIds: readonly string[]},
|
|
7
|
+
): Promise<void> {
|
|
8
|
+
const reservationIds = [...new Set(params.reservationIds)].sort();
|
|
9
|
+
if (reservationIds.length === 0) return;
|
|
10
|
+
|
|
11
|
+
const lockKeys = sql.join(
|
|
12
|
+
reservationIds.map(
|
|
13
|
+
(reservationId) => sql`(${`runners_assignment:${params.provisionerId}:${reservationId}`})`,
|
|
14
|
+
),
|
|
15
|
+
sql`, `,
|
|
16
|
+
);
|
|
17
|
+
await tx.execute(sql`
|
|
18
|
+
select pg_advisory_xact_lock(hashtext(lock_key))
|
|
19
|
+
from (values ${lockKeys}) as reservation_locks(lock_key)
|
|
20
|
+
order by lock_key
|
|
21
|
+
`);
|
|
22
|
+
}
|