@shipfox/api-runners 12.2.0 → 12.4.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 +34 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +10 -3
- 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/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 +69 -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 +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/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/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 +256 -45
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts +17 -0
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +103 -8
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +5 -2
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +134 -25
- 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/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 +24 -2
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +23 -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/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 +3 -3
- 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 +4 -4
- package/src/config.test.ts +24 -2
- package/src/config.ts +16 -3
- 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/runner-activation.ts +48 -2
- package/src/core/runner-control-sessions.test.ts +230 -4
- package/src/core/runner-control-sessions.ts +84 -10
- 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/index.ts +1 -0
- package/src/db/reservation-locks.ts +22 -0
- package/src/db/reservations.test.ts +1110 -31
- package/src/db/reservations.ts +434 -105
- package/src/db/runner-assignments.test.ts +105 -1
- package/src/db/runner-assignments.ts +178 -9
- package/src/db/runner-instances.test.ts +496 -2
- package/src/db/runner-instances.ts +247 -56
- package/src/db/runner-sessions.ts +26 -15
- 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 +10 -1
- package/src/metrics/instance.ts +59 -2
- package/src/metrics/service.test.ts +106 -0
- package/src/metrics/service.ts +24 -4
- 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 +6 -2
- package/src/presentation/routes/late-runner-enrollment.test.ts +447 -0
- package/src/presentation/routes/poll-demand.test.ts +79 -6
- 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/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -10,4 +10,21 @@ export declare function assignRunnerInstancesTx(tx: Tx, params: {
|
|
|
10
10
|
reservationId: string;
|
|
11
11
|
runnerInstanceIds: string[];
|
|
12
12
|
}): Promise<string[]>;
|
|
13
|
+
export type RunnerReservationCapacityFailureReason = 'reservation-not-found' | 'reservation-kind-mismatch' | 'reservation-expired' | 'capacity-exhausted';
|
|
14
|
+
export interface RunnerReservationCapacityValidation {
|
|
15
|
+
acceptedByReservation: Map<string, number>;
|
|
16
|
+
unavailableByReservation: Map<string, {
|
|
17
|
+
reason: RunnerReservationCapacityFailureReason;
|
|
18
|
+
count: number;
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
export declare function validateRunnerReservationCapacityTx(tx: Tx, params: {
|
|
22
|
+
provisionerId: string;
|
|
23
|
+
requests: readonly {
|
|
24
|
+
reservationId: string;
|
|
25
|
+
count: number;
|
|
26
|
+
}[];
|
|
27
|
+
}, options?: {
|
|
28
|
+
advisoryLocksHeld?: boolean;
|
|
29
|
+
}): Promise<RunnerReservationCapacityValidation>;
|
|
13
30
|
//# sourceMappingURL=runner-assignments.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner-assignments.d.ts","sourceRoot":"","sources":["../../src/db/runner-assignments.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"runner-assignments.d.ts","sourceRoot":"","sources":["../../src/db/runner-assignments.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAC,EAAE,EAAC,MAAM,SAAS,CAAC;AAQhC,gGAAgG;AAChG,wBAAsB,qBAAqB,CAAC,MAAM,EAAE;IAClD,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAEpB;AAED,wBAAsB,uBAAuB,CAC3C,EAAE,EAAE,EAAE,EACN,MAAM,EAAE;IACN,aAAa,EAAE,MAAM,CAAC;IACtB,aAAa,EAAE,MAAM,CAAC;IACtB,iBAAiB,EAAE,MAAM,EAAE,CAAC;CAC7B,GACA,OAAO,CAAC,MAAM,EAAE,CAAC,CAkJnB;AAED,MAAM,MAAM,sCAAsC,GAC9C,uBAAuB,GACvB,2BAA2B,GAC3B,qBAAqB,GACrB,oBAAoB,CAAC;AAEzB,MAAM,WAAW,mCAAmC;IAClD,qBAAqB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC3C,wBAAwB,EAAE,GAAG,CAC3B,MAAM,EACN;QAAC,MAAM,EAAE,sCAAsC,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,CAChE,CAAC;CACH;AAED,wBAAsB,mCAAmC,CACvD,EAAE,EAAE,EAAE,EACN,MAAM,EAAE;IACN,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,SAAS;QAAC,aAAa,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAC,EAAE,CAAC;CAC7D,EACD,OAAO,GAAE;IAAC,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAM,GAC1C,OAAO,CAAC,mCAAmC,CAAC,CAmI9C"}
|
|
@@ -1,7 +1,8 @@
|
|
|
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 { ReservationExpiredError, ReservationNotFoundError, RunnerInstanceAlreadyAssignedError, RunnerInstanceNotAssignableError } from '#core/errors.js';
|
|
3
3
|
import { db } from './db.js';
|
|
4
|
-
import {
|
|
4
|
+
import { lockRunnerReservationAdvisoryKeysTx } from './reservation-locks.js';
|
|
5
|
+
import { terminalStates } from './runner-states.js';
|
|
5
6
|
import { reservations } from './schema/reservations.js';
|
|
6
7
|
import { runnerControlSessions } from './schema/runner-control-sessions.js';
|
|
7
8
|
import { providerRunners } from './schema/runner-instances.js';
|
|
@@ -12,26 +13,32 @@ export async function assignRunnerInstancesTx(tx, params) {
|
|
|
12
13
|
const runnerInstanceIds = [
|
|
13
14
|
...params.runnerInstanceIds
|
|
14
15
|
].sort();
|
|
15
|
-
await tx
|
|
16
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
17
|
+
provisionerId: params.provisionerId,
|
|
18
|
+
reservationIds: [
|
|
19
|
+
params.reservationId
|
|
20
|
+
]
|
|
21
|
+
});
|
|
16
22
|
const runnerRows = await tx.select({
|
|
17
23
|
id: providerRunners.id,
|
|
18
24
|
reservationId: providerRunners.reservationId,
|
|
19
25
|
intendedReservationId: providerRunners.intendedReservationId,
|
|
20
26
|
workspaceId: providerRunners.workspaceId,
|
|
27
|
+
assignedAt: providerRunners.assignedAt,
|
|
21
28
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
22
29
|
labels: providerRunners.labels,
|
|
23
30
|
state: providerRunners.state
|
|
24
31
|
}).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.id, runnerInstanceIds))).for('update');
|
|
25
32
|
// The assignment outlives its short reservation row. A retry after maintenance
|
|
26
|
-
// cleanup is complete when every owned runner already carries the
|
|
27
|
-
if (runnerInstanceIds.length > 0 && runnerRows.length === runnerInstanceIds.length && runnerRows.every((runner)=>runner.reservationId === params.reservationId)) {
|
|
33
|
+
// cleanup is complete when every owned runner already carries the committed assignment.
|
|
34
|
+
if (runnerInstanceIds.length > 0 && runnerRows.length === runnerInstanceIds.length && runnerRows.every((runner)=>runner.reservationId === params.reservationId && runner.assignedAt !== null && runner.workspaceId !== null)) {
|
|
28
35
|
await tx.update(providerRunners).set({
|
|
29
36
|
intendedReservationId: null,
|
|
30
37
|
updatedAt: sql`now()`
|
|
31
38
|
}).where(and(eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.id, runnerInstanceIds)));
|
|
32
39
|
return params.runnerInstanceIds;
|
|
33
40
|
}
|
|
34
|
-
const [reservation] = await tx.select().from(reservations).where(and(eq(reservations.id, params.reservationId), eq(reservations.provisionerId, params.provisionerId))).limit(1).for('update');
|
|
41
|
+
const [reservation] = await tx.select().from(reservations).where(and(eq(reservations.id, params.reservationId), eq(reservations.provisionerId, params.provisionerId), eq(reservations.kind, 'launch'))).limit(1).for('update');
|
|
35
42
|
if (!reservation) throw new ReservationNotFoundError(params.reservationId);
|
|
36
43
|
if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);
|
|
37
44
|
if (runnerRows.length !== runnerInstanceIds.length) throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');
|
|
@@ -45,10 +52,11 @@ export async function assignRunnerInstancesTx(tx, params) {
|
|
|
45
52
|
}));
|
|
46
53
|
const alreadyAssigned = runners.filter((runner)=>runner.reservationId !== null);
|
|
47
54
|
if (alreadyAssigned.some((runner)=>runner.reservationId !== reservation.id)) throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');
|
|
48
|
-
const newRunners = runners.filter((runner)=>runner.reservationId === null);
|
|
55
|
+
const newRunners = runners.filter((runner)=>runner.reservationId === null || runner.reservationId === reservation.id && (runner.assignedAt === null || runner.workspaceId === null));
|
|
56
|
+
const newRunnerIds = newRunners.map((runner)=>runner.id);
|
|
49
57
|
const assignedCount = await tx.select({
|
|
50
58
|
count: sql`count(*)::int`
|
|
51
|
-
}).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), or(and(eq(providerRunners.reservationId, reservation.id), isNull(providerRunners.reservationReleasedAt)), and(eq(providerRunners.intendedReservationId, reservation.id),
|
|
59
|
+
}).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), notInArray(providerRunners.id, newRunnerIds), or(and(eq(providerRunners.reservationId, reservation.id), isNull(providerRunners.reservationReleasedAt)), and(eq(providerRunners.intendedReservationId, reservation.id), isNull(providerRunners.reservationReleasedAt), notInArray(providerRunners.state, [
|
|
52
60
|
...terminalStates
|
|
53
61
|
])))));
|
|
54
62
|
if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count) throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');
|
|
@@ -66,5 +74,92 @@ export async function assignRunnerInstancesTx(tx, params) {
|
|
|
66
74
|
}
|
|
67
75
|
return params.runnerInstanceIds;
|
|
68
76
|
}
|
|
77
|
+
export async function validateRunnerReservationCapacityTx(tx, params, options = {}) {
|
|
78
|
+
const requestedByReservation = new Map();
|
|
79
|
+
for (const request of params.requests){
|
|
80
|
+
if (request.count <= 0) continue;
|
|
81
|
+
requestedByReservation.set(request.reservationId, (requestedByReservation.get(request.reservationId) ?? 0) + request.count);
|
|
82
|
+
}
|
|
83
|
+
const reservationIds = [
|
|
84
|
+
...requestedByReservation.keys()
|
|
85
|
+
].sort();
|
|
86
|
+
if (reservationIds.length === 0) return {
|
|
87
|
+
acceptedByReservation: new Map(),
|
|
88
|
+
unavailableByReservation: new Map()
|
|
89
|
+
};
|
|
90
|
+
if (!options.advisoryLocksHeld) await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
91
|
+
provisionerId: params.provisionerId,
|
|
92
|
+
reservationIds
|
|
93
|
+
});
|
|
94
|
+
const reservationRows = await tx.select({
|
|
95
|
+
id: reservations.id,
|
|
96
|
+
count: reservations.count,
|
|
97
|
+
expiresAt: reservations.expiresAt,
|
|
98
|
+
isExpired: sql`${reservations.expiresAt} <= now()`
|
|
99
|
+
}).from(reservations).where(and(eq(reservations.provisionerId, params.provisionerId), inArray(reservations.id, reservationIds), eq(reservations.kind, 'launch'))).for('update');
|
|
100
|
+
const nonLaunchReservationRows = await tx.select({
|
|
101
|
+
id: reservations.id
|
|
102
|
+
}).from(reservations).where(and(eq(reservations.provisionerId, params.provisionerId), inArray(reservations.id, reservationIds), ne(reservations.kind, 'launch'))).for('update');
|
|
103
|
+
const reservationsById = new Map(reservationRows.map((reservation)=>[
|
|
104
|
+
reservation.id,
|
|
105
|
+
reservation
|
|
106
|
+
]));
|
|
107
|
+
const nonLaunchReservationIds = new Set(nonLaunchReservationRows.map((reservation)=>reservation.id));
|
|
108
|
+
const requestedReservationIds = new Set(reservationIds);
|
|
109
|
+
const usedRunnerIdsByReservation = new Map();
|
|
110
|
+
const usedRunnerRows = await tx.select({
|
|
111
|
+
id: providerRunners.id,
|
|
112
|
+
reservationId: providerRunners.reservationId,
|
|
113
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
114
|
+
state: providerRunners.state
|
|
115
|
+
}).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), isNull(providerRunners.reservationReleasedAt), or(inArray(providerRunners.reservationId, reservationIds), and(inArray(providerRunners.intendedReservationId, reservationIds), notInArray(providerRunners.state, [
|
|
116
|
+
...terminalStates
|
|
117
|
+
])))));
|
|
118
|
+
for (const runner of usedRunnerRows){
|
|
119
|
+
if (runner.reservationId && requestedReservationIds.has(runner.reservationId)) {
|
|
120
|
+
const usedRunnerIds = usedRunnerIdsByReservation.get(runner.reservationId) ?? new Set();
|
|
121
|
+
usedRunnerIds.add(runner.id);
|
|
122
|
+
usedRunnerIdsByReservation.set(runner.reservationId, usedRunnerIds);
|
|
123
|
+
}
|
|
124
|
+
if (runner.intendedReservationId && requestedReservationIds.has(runner.intendedReservationId) && !terminalStates.includes(runner.state)) {
|
|
125
|
+
const usedRunnerIds = usedRunnerIdsByReservation.get(runner.intendedReservationId) ?? new Set();
|
|
126
|
+
usedRunnerIds.add(runner.id);
|
|
127
|
+
usedRunnerIdsByReservation.set(runner.intendedReservationId, usedRunnerIds);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const acceptedByReservation = new Map();
|
|
131
|
+
const unavailableByReservation = new Map();
|
|
132
|
+
for (const reservationId of reservationIds){
|
|
133
|
+
const requested = requestedByReservation.get(reservationId) ?? 0;
|
|
134
|
+
const reservation = reservationsById.get(reservationId);
|
|
135
|
+
if (!reservation) {
|
|
136
|
+
unavailableByReservation.set(reservationId, {
|
|
137
|
+
reason: nonLaunchReservationIds.has(reservationId) ? 'reservation-kind-mismatch' : 'reservation-not-found',
|
|
138
|
+
count: requested
|
|
139
|
+
});
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (reservation.isExpired) {
|
|
143
|
+
unavailableByReservation.set(reservationId, {
|
|
144
|
+
reason: 'reservation-expired',
|
|
145
|
+
count: requested
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const used = usedRunnerIdsByReservation.get(reservationId)?.size ?? 0;
|
|
150
|
+
const accepted = Math.min(requested, Math.max(0, reservation.count - used));
|
|
151
|
+
acceptedByReservation.set(reservationId, accepted);
|
|
152
|
+
if (accepted < requested) {
|
|
153
|
+
unavailableByReservation.set(reservationId, {
|
|
154
|
+
reason: 'capacity-exhausted',
|
|
155
|
+
count: requested - accepted
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
acceptedByReservation,
|
|
161
|
+
unavailableByReservation
|
|
162
|
+
};
|
|
163
|
+
}
|
|
69
164
|
|
|
70
165
|
//# sourceMappingURL=runner-assignments.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/db/runner-assignments.ts"],"sourcesContent":["import {and, eq, inArray, isNull, notInArray, or, sql} from 'drizzle-orm';\nimport {\n ReservationExpiredError,\n ReservationNotFoundError,\n RunnerInstanceAlreadyAssignedError,\n RunnerInstanceNotAssignableError,\n} from '#core/errors.js';\nimport type {Tx} from './db.js';\nimport {db} from './db.js';\nimport {terminalStates} from './runner-instances.js';\nimport {reservations} from './schema/reservations.js';\nimport {runnerControlSessions} from './schema/runner-control-sessions.js';\nimport {providerRunners} from './schema/runner-instances.js';\n\n/** Atomically consumes reservation units by writing the immutable assignment on each runner. */\nexport async function assignRunnerInstances(params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n}): Promise<string[]> {\n return await db().transaction(async (tx) => assignRunnerInstancesTx(tx, params));\n}\n\nexport async function assignRunnerInstancesTx(\n tx: Tx,\n params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n },\n): Promise<string[]> {\n const runnerInstanceIds = [...params.runnerInstanceIds].sort();\n await tx.execute(\n sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${params.provisionerId}:${params.reservationId}`}))`,\n );\n const runnerRows = await tx\n .select({\n id: providerRunners.id,\n reservationId: providerRunners.reservationId,\n intendedReservationId: providerRunners.intendedReservationId,\n workspaceId: providerRunners.workspaceId,\n providerRunnerId: providerRunners.providerRunnerId,\n labels: providerRunners.labels,\n state: providerRunners.state,\n })\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n )\n .for('update');\n\n // The assignment outlives its short reservation row. A retry after maintenance\n // cleanup is complete when every owned runner already carries the requested assignment.\n if (\n runnerInstanceIds.length > 0 &&\n runnerRows.length === runnerInstanceIds.length &&\n runnerRows.every((runner) => runner.reservationId === params.reservationId)\n ) {\n await tx\n .update(providerRunners)\n .set({intendedReservationId: null, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n );\n return params.runnerInstanceIds;\n }\n\n const [reservation] = await tx\n .select()\n .from(reservations)\n .where(\n and(\n eq(reservations.id, params.reservationId),\n eq(reservations.provisionerId, params.provisionerId),\n ),\n )\n .limit(1)\n .for('update');\n if (!reservation) throw new ReservationNotFoundError(params.reservationId);\n if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);\n if (runnerRows.length !== runnerInstanceIds.length)\n throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');\n\n const activeControlSessions = await tx\n .select({runnerInstanceId: runnerControlSessions.runnerInstanceId})\n .from(runnerControlSessions)\n .where(\n and(\n inArray(\n runnerControlSessions.runnerInstanceId,\n runnerRows.map((runner) => runner.id),\n ),\n isNull(runnerControlSessions.closedAt),\n ),\n );\n const runnerInstanceIdsWithControlSession = new Set(\n activeControlSessions.map((session) => session.runnerInstanceId),\n );\n const runners = runnerRows.map((runner) => ({\n ...runner,\n controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,\n }));\n\n const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);\n if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))\n throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');\n const newRunners = runners.filter((runner) => runner.reservationId === null);\n const assignedCount = await tx\n .select({count: sql<number>`count(*)::int`})\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n or(\n and(\n eq(providerRunners.reservationId, reservation.id),\n isNull(providerRunners.reservationReleasedAt),\n ),\n and(\n eq(providerRunners.intendedReservationId, reservation.id),\n notInArray(providerRunners.id, runnerInstanceIds),\n isNull(providerRunners.reservationReleasedAt),\n notInArray(providerRunners.state, [...terminalStates]),\n ),\n ),\n ),\n );\n if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)\n throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');\n for (const runner of newRunners) {\n if (\n runner.state !== 'running' ||\n !runner.providerRunnerId ||\n !runner.controlSessionId ||\n !reservation.requiredLabels.every((label) => runner.labels.includes(label))\n )\n throw new RunnerInstanceNotAssignableError(runner.id);\n }\n if (newRunners.length > 0) {\n await tx\n .update(providerRunners)\n .set({\n workspaceId: reservation.workspaceId,\n reservationId: reservation.id,\n intendedReservationId: null,\n assignedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n inArray(\n providerRunners.id,\n newRunners.map((runner) => runner.id),\n ),\n );\n }\n return params.runnerInstanceIds;\n}\n"],"names":["and","eq","inArray","isNull","notInArray","or","sql","ReservationExpiredError","ReservationNotFoundError","RunnerInstanceAlreadyAssignedError","RunnerInstanceNotAssignableError","db","terminalStates","reservations","runnerControlSessions","providerRunners","assignRunnerInstances","params","transaction","tx","assignRunnerInstancesTx","runnerInstanceIds","sort","execute","provisionerId","reservationId","runnerRows","select","id","intendedReservationId","workspaceId","providerRunnerId","labels","state","from","where","for","length","every","runner","update","set","updatedAt","reservation","limit","expiresAt","Date","activeControlSessions","runnerInstanceId","map","closedAt","runnerInstanceIdsWithControlSession","Set","session","runners","controlSessionId","has","alreadyAssigned","filter","some","newRunners","assignedCount","count","reservationReleasedAt","requiredLabels","label","includes","assignedAt"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../src/db/runner-assignments.ts"],"sourcesContent":["import {and, eq, inArray, isNull, ne, notInArray, or, sql} from 'drizzle-orm';\nimport {\n ReservationExpiredError,\n ReservationNotFoundError,\n RunnerInstanceAlreadyAssignedError,\n RunnerInstanceNotAssignableError,\n} from '#core/errors.js';\nimport type {Tx} from './db.js';\nimport {db} from './db.js';\nimport {lockRunnerReservationAdvisoryKeysTx} from './reservation-locks.js';\nimport {terminalStates} from './runner-states.js';\nimport {reservations} from './schema/reservations.js';\nimport {runnerControlSessions} from './schema/runner-control-sessions.js';\nimport {providerRunners} from './schema/runner-instances.js';\n\n/** Atomically consumes reservation units by writing the immutable assignment on each runner. */\nexport async function assignRunnerInstances(params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n}): Promise<string[]> {\n return await db().transaction(async (tx) => assignRunnerInstancesTx(tx, params));\n}\n\nexport async function assignRunnerInstancesTx(\n tx: Tx,\n params: {\n provisionerId: string;\n reservationId: string;\n runnerInstanceIds: string[];\n },\n): Promise<string[]> {\n const runnerInstanceIds = [...params.runnerInstanceIds].sort();\n await lockRunnerReservationAdvisoryKeysTx(tx, {\n provisionerId: params.provisionerId,\n reservationIds: [params.reservationId],\n });\n const runnerRows = await tx\n .select({\n id: providerRunners.id,\n reservationId: providerRunners.reservationId,\n intendedReservationId: providerRunners.intendedReservationId,\n workspaceId: providerRunners.workspaceId,\n assignedAt: providerRunners.assignedAt,\n providerRunnerId: providerRunners.providerRunnerId,\n labels: providerRunners.labels,\n state: providerRunners.state,\n })\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n )\n .for('update');\n\n // The assignment outlives its short reservation row. A retry after maintenance\n // cleanup is complete when every owned runner already carries the committed assignment.\n if (\n runnerInstanceIds.length > 0 &&\n runnerRows.length === runnerInstanceIds.length &&\n runnerRows.every(\n (runner) =>\n runner.reservationId === params.reservationId &&\n runner.assignedAt !== null &&\n runner.workspaceId !== null,\n )\n ) {\n await tx\n .update(providerRunners)\n .set({intendedReservationId: null, updatedAt: sql`now()`})\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n inArray(providerRunners.id, runnerInstanceIds),\n ),\n );\n return params.runnerInstanceIds;\n }\n\n const [reservation] = await tx\n .select()\n .from(reservations)\n .where(\n and(\n eq(reservations.id, params.reservationId),\n eq(reservations.provisionerId, params.provisionerId),\n eq(reservations.kind, 'launch'),\n ),\n )\n .limit(1)\n .for('update');\n if (!reservation) throw new ReservationNotFoundError(params.reservationId);\n if (reservation.expiresAt <= new Date()) throw new ReservationExpiredError(params.reservationId);\n if (runnerRows.length !== runnerInstanceIds.length)\n throw new RunnerInstanceNotAssignableError(runnerInstanceIds[0] ?? '');\n\n const activeControlSessions = await tx\n .select({runnerInstanceId: runnerControlSessions.runnerInstanceId})\n .from(runnerControlSessions)\n .where(\n and(\n inArray(\n runnerControlSessions.runnerInstanceId,\n runnerRows.map((runner) => runner.id),\n ),\n isNull(runnerControlSessions.closedAt),\n ),\n );\n const runnerInstanceIdsWithControlSession = new Set(\n activeControlSessions.map((session) => session.runnerInstanceId),\n );\n const runners = runnerRows.map((runner) => ({\n ...runner,\n controlSessionId: runnerInstanceIdsWithControlSession.has(runner.id) ? runner.id : null,\n }));\n\n const alreadyAssigned = runners.filter((runner) => runner.reservationId !== null);\n if (alreadyAssigned.some((runner) => runner.reservationId !== reservation.id))\n throw new RunnerInstanceAlreadyAssignedError(alreadyAssigned[0]?.id ?? '');\n const newRunners = runners.filter(\n (runner) =>\n runner.reservationId === null ||\n (runner.reservationId === reservation.id &&\n (runner.assignedAt === null || runner.workspaceId === null)),\n );\n const newRunnerIds = newRunners.map((runner) => runner.id);\n const assignedCount = await tx\n .select({count: sql<number>`count(*)::int`})\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n notInArray(providerRunners.id, newRunnerIds),\n or(\n and(\n eq(providerRunners.reservationId, reservation.id),\n isNull(providerRunners.reservationReleasedAt),\n ),\n and(\n eq(providerRunners.intendedReservationId, reservation.id),\n isNull(providerRunners.reservationReleasedAt),\n notInArray(providerRunners.state, [...terminalStates]),\n ),\n ),\n ),\n );\n if ((assignedCount[0]?.count ?? 0) + newRunners.length > reservation.count)\n throw new RunnerInstanceNotAssignableError(newRunners[0]?.id ?? '');\n for (const runner of newRunners) {\n if (\n runner.state !== 'running' ||\n !runner.providerRunnerId ||\n !runner.controlSessionId ||\n !reservation.requiredLabels.every((label) => runner.labels.includes(label))\n )\n throw new RunnerInstanceNotAssignableError(runner.id);\n }\n if (newRunners.length > 0) {\n await tx\n .update(providerRunners)\n .set({\n workspaceId: reservation.workspaceId,\n reservationId: reservation.id,\n intendedReservationId: null,\n assignedAt: sql`now()`,\n updatedAt: sql`now()`,\n })\n .where(\n inArray(\n providerRunners.id,\n newRunners.map((runner) => runner.id),\n ),\n );\n }\n return params.runnerInstanceIds;\n}\n\nexport type RunnerReservationCapacityFailureReason =\n | 'reservation-not-found'\n | 'reservation-kind-mismatch'\n | 'reservation-expired'\n | 'capacity-exhausted';\n\nexport interface RunnerReservationCapacityValidation {\n acceptedByReservation: Map<string, number>;\n unavailableByReservation: Map<\n string,\n {reason: RunnerReservationCapacityFailureReason; count: number}\n >;\n}\n\nexport async function validateRunnerReservationCapacityTx(\n tx: Tx,\n params: {\n provisionerId: string;\n requests: readonly {reservationId: string; count: number}[];\n },\n options: {advisoryLocksHeld?: boolean} = {},\n): Promise<RunnerReservationCapacityValidation> {\n const requestedByReservation = new Map<string, number>();\n for (const request of params.requests) {\n if (request.count <= 0) continue;\n requestedByReservation.set(\n request.reservationId,\n (requestedByReservation.get(request.reservationId) ?? 0) + request.count,\n );\n }\n const reservationIds = [...requestedByReservation.keys()].sort();\n if (reservationIds.length === 0)\n return {acceptedByReservation: new Map(), unavailableByReservation: new Map()};\n\n if (!options.advisoryLocksHeld)\n await lockRunnerReservationAdvisoryKeysTx(tx, {\n provisionerId: params.provisionerId,\n reservationIds,\n });\n\n const reservationRows = await tx\n .select({\n id: reservations.id,\n count: reservations.count,\n expiresAt: reservations.expiresAt,\n isExpired: sql<boolean>`${reservations.expiresAt} <= now()`,\n })\n .from(reservations)\n .where(\n and(\n eq(reservations.provisionerId, params.provisionerId),\n inArray(reservations.id, reservationIds),\n eq(reservations.kind, 'launch'),\n ),\n )\n .for('update');\n const nonLaunchReservationRows = await tx\n .select({id: reservations.id})\n .from(reservations)\n .where(\n and(\n eq(reservations.provisionerId, params.provisionerId),\n inArray(reservations.id, reservationIds),\n ne(reservations.kind, 'launch'),\n ),\n )\n .for('update');\n const reservationsById = new Map(\n reservationRows.map((reservation) => [reservation.id, reservation]),\n );\n const nonLaunchReservationIds = new Set(\n nonLaunchReservationRows.map((reservation) => reservation.id),\n );\n const requestedReservationIds = new Set(reservationIds);\n const usedRunnerIdsByReservation = new Map<string, Set<string>>();\n const usedRunnerRows = await tx\n .select({\n id: providerRunners.id,\n reservationId: providerRunners.reservationId,\n intendedReservationId: providerRunners.intendedReservationId,\n state: providerRunners.state,\n })\n .from(providerRunners)\n .where(\n and(\n eq(providerRunners.provisionerId, params.provisionerId),\n isNull(providerRunners.reservationReleasedAt),\n or(\n inArray(providerRunners.reservationId, reservationIds),\n and(\n inArray(providerRunners.intendedReservationId, reservationIds),\n notInArray(providerRunners.state, [...terminalStates]),\n ),\n ),\n ),\n );\n for (const runner of usedRunnerRows) {\n if (runner.reservationId && requestedReservationIds.has(runner.reservationId)) {\n const usedRunnerIds =\n usedRunnerIdsByReservation.get(runner.reservationId) ?? new Set<string>();\n usedRunnerIds.add(runner.id);\n usedRunnerIdsByReservation.set(runner.reservationId, usedRunnerIds);\n }\n if (\n runner.intendedReservationId &&\n requestedReservationIds.has(runner.intendedReservationId) &&\n !terminalStates.includes(runner.state as (typeof terminalStates)[number])\n ) {\n const usedRunnerIds =\n usedRunnerIdsByReservation.get(runner.intendedReservationId) ?? new Set<string>();\n usedRunnerIds.add(runner.id);\n usedRunnerIdsByReservation.set(runner.intendedReservationId, usedRunnerIds);\n }\n }\n\n const acceptedByReservation = new Map<string, number>();\n const unavailableByReservation = new Map<\n string,\n {reason: RunnerReservationCapacityFailureReason; count: number}\n >();\n for (const reservationId of reservationIds) {\n const requested = requestedByReservation.get(reservationId) ?? 0;\n const reservation = reservationsById.get(reservationId);\n if (!reservation) {\n unavailableByReservation.set(reservationId, {\n reason: nonLaunchReservationIds.has(reservationId)\n ? 'reservation-kind-mismatch'\n : 'reservation-not-found',\n count: requested,\n });\n continue;\n }\n if (reservation.isExpired) {\n unavailableByReservation.set(reservationId, {\n reason: 'reservation-expired',\n count: requested,\n });\n continue;\n }\n\n const used = usedRunnerIdsByReservation.get(reservationId)?.size ?? 0;\n const accepted = Math.min(requested, Math.max(0, reservation.count - used));\n acceptedByReservation.set(reservationId, accepted);\n if (accepted < requested) {\n unavailableByReservation.set(reservationId, {\n reason: 'capacity-exhausted',\n count: requested - accepted,\n });\n }\n }\n\n return {acceptedByReservation, unavailableByReservation};\n}\n"],"names":["and","eq","inArray","isNull","ne","notInArray","or","sql","ReservationExpiredError","ReservationNotFoundError","RunnerInstanceAlreadyAssignedError","RunnerInstanceNotAssignableError","db","lockRunnerReservationAdvisoryKeysTx","terminalStates","reservations","runnerControlSessions","providerRunners","assignRunnerInstances","params","transaction","tx","assignRunnerInstancesTx","runnerInstanceIds","sort","provisionerId","reservationIds","reservationId","runnerRows","select","id","intendedReservationId","workspaceId","assignedAt","providerRunnerId","labels","state","from","where","for","length","every","runner","update","set","updatedAt","reservation","kind","limit","expiresAt","Date","activeControlSessions","runnerInstanceId","map","closedAt","runnerInstanceIdsWithControlSession","Set","session","runners","controlSessionId","has","alreadyAssigned","filter","some","newRunners","newRunnerIds","assignedCount","count","reservationReleasedAt","requiredLabels","label","includes","validateRunnerReservationCapacityTx","options","requestedByReservation","Map","request","requests","get","keys","acceptedByReservation","unavailableByReservation","advisoryLocksHeld","reservationRows","isExpired","nonLaunchReservationRows","reservationsById","nonLaunchReservationIds","requestedReservationIds","usedRunnerIdsByReservation","usedRunnerRows","usedRunnerIds","add","requested","reason","used","size","accepted","Math","min","max"],"mappings":"AAAA,SAAQA,GAAG,EAAEC,EAAE,EAAEC,OAAO,EAAEC,MAAM,EAAEC,EAAE,EAAEC,UAAU,EAAEC,EAAE,EAAEC,GAAG,QAAO,cAAc;AAC9E,SACEC,uBAAuB,EACvBC,wBAAwB,EACxBC,kCAAkC,EAClCC,gCAAgC,QAC3B,kBAAkB;AAEzB,SAAQC,EAAE,QAAO,UAAU;AAC3B,SAAQC,mCAAmC,QAAO,yBAAyB;AAC3E,SAAQC,cAAc,QAAO,qBAAqB;AAClD,SAAQC,YAAY,QAAO,2BAA2B;AACtD,SAAQC,qBAAqB,QAAO,sCAAsC;AAC1E,SAAQC,eAAe,QAAO,+BAA+B;AAE7D,8FAA8F,GAC9F,OAAO,eAAeC,sBAAsBC,MAI3C;IACC,OAAO,MAAMP,KAAKQ,WAAW,CAAC,OAAOC,KAAOC,wBAAwBD,IAAIF;AAC1E;AAEA,OAAO,eAAeG,wBACpBD,EAAM,EACNF,MAIC;IAED,MAAMI,oBAAoB;WAAIJ,OAAOI,iBAAiB;KAAC,CAACC,IAAI;IAC5D,MAAMX,oCAAoCQ,IAAI;QAC5CI,eAAeN,OAAOM,aAAa;QACnCC,gBAAgB;YAACP,OAAOQ,aAAa;SAAC;IACxC;IACA,MAAMC,aAAa,MAAMP,GACtBQ,MAAM,CAAC;QACNC,IAAIb,gBAAgBa,EAAE;QACtBH,eAAeV,gBAAgBU,aAAa;QAC5CI,uBAAuBd,gBAAgBc,qBAAqB;QAC5DC,aAAaf,gBAAgBe,WAAW;QACxCC,YAAYhB,gBAAgBgB,UAAU;QACtCC,kBAAkBjB,gBAAgBiB,gBAAgB;QAClDC,QAAQlB,gBAAgBkB,MAAM;QAC9BC,OAAOnB,gBAAgBmB,KAAK;IAC9B,GACCC,IAAI,CAACpB,iBACLqB,KAAK,CACJtC,IACEC,GAAGgB,gBAAgBQ,aAAa,EAAEN,OAAOM,aAAa,GACtDvB,QAAQe,gBAAgBa,EAAE,EAAEP,qBAG/BgB,GAAG,CAAC;IAEP,+EAA+E;IAC/E,wFAAwF;IACxF,IACEhB,kBAAkBiB,MAAM,GAAG,KAC3BZ,WAAWY,MAAM,KAAKjB,kBAAkBiB,MAAM,IAC9CZ,WAAWa,KAAK,CACd,CAACC,SACCA,OAAOf,aAAa,KAAKR,OAAOQ,aAAa,IAC7Ce,OAAOT,UAAU,KAAK,QACtBS,OAAOV,WAAW,KAAK,OAE3B;QACA,MAAMX,GACHsB,MAAM,CAAC1B,iBACP2B,GAAG,CAAC;YAACb,uBAAuB;YAAMc,WAAWtC,GAAG,CAAC,KAAK,CAAC;QAAA,GACvD+B,KAAK,CACJtC,IACEC,GAAGgB,gBAAgBQ,aAAa,EAAEN,OAAOM,aAAa,GACtDvB,QAAQe,gBAAgBa,EAAE,EAAEP;QAGlC,OAAOJ,OAAOI,iBAAiB;IACjC;IAEA,MAAM,CAACuB,YAAY,GAAG,MAAMzB,GACzBQ,MAAM,GACNQ,IAAI,CAACtB,cACLuB,KAAK,CACJtC,IACEC,GAAGc,aAAae,EAAE,EAAEX,OAAOQ,aAAa,GACxC1B,GAAGc,aAAaU,aAAa,EAAEN,OAAOM,aAAa,GACnDxB,GAAGc,aAAagC,IAAI,EAAE,YAGzBC,KAAK,CAAC,GACNT,GAAG,CAAC;IACP,IAAI,CAACO,aAAa,MAAM,IAAIrC,yBAAyBU,OAAOQ,aAAa;IACzE,IAAImB,YAAYG,SAAS,IAAI,IAAIC,QAAQ,MAAM,IAAI1C,wBAAwBW,OAAOQ,aAAa;IAC/F,IAAIC,WAAWY,MAAM,KAAKjB,kBAAkBiB,MAAM,EAChD,MAAM,IAAI7B,iCAAiCY,iBAAiB,CAAC,EAAE,IAAI;IAErE,MAAM4B,wBAAwB,MAAM9B,GACjCQ,MAAM,CAAC;QAACuB,kBAAkBpC,sBAAsBoC,gBAAgB;IAAA,GAChEf,IAAI,CAACrB,uBACLsB,KAAK,CACJtC,IACEE,QACEc,sBAAsBoC,gBAAgB,EACtCxB,WAAWyB,GAAG,CAAC,CAACX,SAAWA,OAAOZ,EAAE,IAEtC3B,OAAOa,sBAAsBsC,QAAQ;IAG3C,MAAMC,sCAAsC,IAAIC,IAC9CL,sBAAsBE,GAAG,CAAC,CAACI,UAAYA,QAAQL,gBAAgB;IAEjE,MAAMM,UAAU9B,WAAWyB,GAAG,CAAC,CAACX,SAAY,CAAA;YAC1C,GAAGA,MAAM;YACTiB,kBAAkBJ,oCAAoCK,GAAG,CAAClB,OAAOZ,EAAE,IAAIY,OAAOZ,EAAE,GAAG;QACrF,CAAA;IAEA,MAAM+B,kBAAkBH,QAAQI,MAAM,CAAC,CAACpB,SAAWA,OAAOf,aAAa,KAAK;IAC5E,IAAIkC,gBAAgBE,IAAI,CAAC,CAACrB,SAAWA,OAAOf,aAAa,KAAKmB,YAAYhB,EAAE,GAC1E,MAAM,IAAIpB,mCAAmCmD,eAAe,CAAC,EAAE,EAAE/B,MAAM;IACzE,MAAMkC,aAAaN,QAAQI,MAAM,CAC/B,CAACpB,SACCA,OAAOf,aAAa,KAAK,QACxBe,OAAOf,aAAa,KAAKmB,YAAYhB,EAAE,IACrCY,CAAAA,OAAOT,UAAU,KAAK,QAAQS,OAAOV,WAAW,KAAK,IAAG;IAE/D,MAAMiC,eAAeD,WAAWX,GAAG,CAAC,CAACX,SAAWA,OAAOZ,EAAE;IACzD,MAAMoC,gBAAgB,MAAM7C,GACzBQ,MAAM,CAAC;QAACsC,OAAO5D,GAAW,CAAC,aAAa,CAAC;IAAA,GACzC8B,IAAI,CAACpB,iBACLqB,KAAK,CACJtC,IACEC,GAAGgB,gBAAgBQ,aAAa,EAAEN,OAAOM,aAAa,GACtDpB,WAAWY,gBAAgBa,EAAE,EAAEmC,eAC/B3D,GACEN,IACEC,GAAGgB,gBAAgBU,aAAa,EAAEmB,YAAYhB,EAAE,GAChD3B,OAAOc,gBAAgBmD,qBAAqB,IAE9CpE,IACEC,GAAGgB,gBAAgBc,qBAAqB,EAAEe,YAAYhB,EAAE,GACxD3B,OAAOc,gBAAgBmD,qBAAqB,GAC5C/D,WAAWY,gBAAgBmB,KAAK,EAAE;WAAItB;KAAe;IAK/D,IAAI,AAACoD,CAAAA,aAAa,CAAC,EAAE,EAAEC,SAAS,CAAA,IAAKH,WAAWxB,MAAM,GAAGM,YAAYqB,KAAK,EACxE,MAAM,IAAIxD,iCAAiCqD,UAAU,CAAC,EAAE,EAAElC,MAAM;IAClE,KAAK,MAAMY,UAAUsB,WAAY;QAC/B,IACEtB,OAAON,KAAK,KAAK,aACjB,CAACM,OAAOR,gBAAgB,IACxB,CAACQ,OAAOiB,gBAAgB,IACxB,CAACb,YAAYuB,cAAc,CAAC5B,KAAK,CAAC,CAAC6B,QAAU5B,OAAOP,MAAM,CAACoC,QAAQ,CAACD,SAEpE,MAAM,IAAI3D,iCAAiC+B,OAAOZ,EAAE;IACxD;IACA,IAAIkC,WAAWxB,MAAM,GAAG,GAAG;QACzB,MAAMnB,GACHsB,MAAM,CAAC1B,iBACP2B,GAAG,CAAC;YACHZ,aAAac,YAAYd,WAAW;YACpCL,eAAemB,YAAYhB,EAAE;YAC7BC,uBAAuB;YACvBE,YAAY1B,GAAG,CAAC,KAAK,CAAC;YACtBsC,WAAWtC,GAAG,CAAC,KAAK,CAAC;QACvB,GACC+B,KAAK,CACJpC,QACEe,gBAAgBa,EAAE,EAClBkC,WAAWX,GAAG,CAAC,CAACX,SAAWA,OAAOZ,EAAE;IAG5C;IACA,OAAOX,OAAOI,iBAAiB;AACjC;AAgBA,OAAO,eAAeiD,oCACpBnD,EAAM,EACNF,MAGC,EACDsD,UAAyC,CAAC,CAAC;IAE3C,MAAMC,yBAAyB,IAAIC;IACnC,KAAK,MAAMC,WAAWzD,OAAO0D,QAAQ,CAAE;QACrC,IAAID,QAAQT,KAAK,IAAI,GAAG;QACxBO,uBAAuB9B,GAAG,CACxBgC,QAAQjD,aAAa,EACrB,AAAC+C,CAAAA,uBAAuBI,GAAG,CAACF,QAAQjD,aAAa,KAAK,CAAA,IAAKiD,QAAQT,KAAK;IAE5E;IACA,MAAMzC,iBAAiB;WAAIgD,uBAAuBK,IAAI;KAAG,CAACvD,IAAI;IAC9D,IAAIE,eAAec,MAAM,KAAK,GAC5B,OAAO;QAACwC,uBAAuB,IAAIL;QAAOM,0BAA0B,IAAIN;IAAK;IAE/E,IAAI,CAACF,QAAQS,iBAAiB,EAC5B,MAAMrE,oCAAoCQ,IAAI;QAC5CI,eAAeN,OAAOM,aAAa;QACnCC;IACF;IAEF,MAAMyD,kBAAkB,MAAM9D,GAC3BQ,MAAM,CAAC;QACNC,IAAIf,aAAae,EAAE;QACnBqC,OAAOpD,aAAaoD,KAAK;QACzBlB,WAAWlC,aAAakC,SAAS;QACjCmC,WAAW7E,GAAY,CAAC,EAAEQ,aAAakC,SAAS,CAAC,SAAS,CAAC;IAC7D,GACCZ,IAAI,CAACtB,cACLuB,KAAK,CACJtC,IACEC,GAAGc,aAAaU,aAAa,EAAEN,OAAOM,aAAa,GACnDvB,QAAQa,aAAae,EAAE,EAAEJ,iBACzBzB,GAAGc,aAAagC,IAAI,EAAE,YAGzBR,GAAG,CAAC;IACP,MAAM8C,2BAA2B,MAAMhE,GACpCQ,MAAM,CAAC;QAACC,IAAIf,aAAae,EAAE;IAAA,GAC3BO,IAAI,CAACtB,cACLuB,KAAK,CACJtC,IACEC,GAAGc,aAAaU,aAAa,EAAEN,OAAOM,aAAa,GACnDvB,QAAQa,aAAae,EAAE,EAAEJ,iBACzBtB,GAAGW,aAAagC,IAAI,EAAE,YAGzBR,GAAG,CAAC;IACP,MAAM+C,mBAAmB,IAAIX,IAC3BQ,gBAAgB9B,GAAG,CAAC,CAACP,cAAgB;YAACA,YAAYhB,EAAE;YAAEgB;SAAY;IAEpE,MAAMyC,0BAA0B,IAAI/B,IAClC6B,yBAAyBhC,GAAG,CAAC,CAACP,cAAgBA,YAAYhB,EAAE;IAE9D,MAAM0D,0BAA0B,IAAIhC,IAAI9B;IACxC,MAAM+D,6BAA6B,IAAId;IACvC,MAAMe,iBAAiB,MAAMrE,GAC1BQ,MAAM,CAAC;QACNC,IAAIb,gBAAgBa,EAAE;QACtBH,eAAeV,gBAAgBU,aAAa;QAC5CI,uBAAuBd,gBAAgBc,qBAAqB;QAC5DK,OAAOnB,gBAAgBmB,KAAK;IAC9B,GACCC,IAAI,CAACpB,iBACLqB,KAAK,CACJtC,IACEC,GAAGgB,gBAAgBQ,aAAa,EAAEN,OAAOM,aAAa,GACtDtB,OAAOc,gBAAgBmD,qBAAqB,GAC5C9D,GACEJ,QAAQe,gBAAgBU,aAAa,EAAED,iBACvC1B,IACEE,QAAQe,gBAAgBc,qBAAqB,EAAEL,iBAC/CrB,WAAWY,gBAAgBmB,KAAK,EAAE;WAAItB;KAAe;IAK/D,KAAK,MAAM4B,UAAUgD,eAAgB;QACnC,IAAIhD,OAAOf,aAAa,IAAI6D,wBAAwB5B,GAAG,CAAClB,OAAOf,aAAa,GAAG;YAC7E,MAAMgE,gBACJF,2BAA2BX,GAAG,CAACpC,OAAOf,aAAa,KAAK,IAAI6B;YAC9DmC,cAAcC,GAAG,CAAClD,OAAOZ,EAAE;YAC3B2D,2BAA2B7C,GAAG,CAACF,OAAOf,aAAa,EAAEgE;QACvD;QACA,IACEjD,OAAOX,qBAAqB,IAC5ByD,wBAAwB5B,GAAG,CAAClB,OAAOX,qBAAqB,KACxD,CAACjB,eAAeyD,QAAQ,CAAC7B,OAAON,KAAK,GACrC;YACA,MAAMuD,gBACJF,2BAA2BX,GAAG,CAACpC,OAAOX,qBAAqB,KAAK,IAAIyB;YACtEmC,cAAcC,GAAG,CAAClD,OAAOZ,EAAE;YAC3B2D,2BAA2B7C,GAAG,CAACF,OAAOX,qBAAqB,EAAE4D;QAC/D;IACF;IAEA,MAAMX,wBAAwB,IAAIL;IAClC,MAAMM,2BAA2B,IAAIN;IAIrC,KAAK,MAAMhD,iBAAiBD,eAAgB;QAC1C,MAAMmE,YAAYnB,uBAAuBI,GAAG,CAACnD,kBAAkB;QAC/D,MAAMmB,cAAcwC,iBAAiBR,GAAG,CAACnD;QACzC,IAAI,CAACmB,aAAa;YAChBmC,yBAAyBrC,GAAG,CAACjB,eAAe;gBAC1CmE,QAAQP,wBAAwB3B,GAAG,CAACjC,iBAChC,8BACA;gBACJwC,OAAO0B;YACT;YACA;QACF;QACA,IAAI/C,YAAYsC,SAAS,EAAE;YACzBH,yBAAyBrC,GAAG,CAACjB,eAAe;gBAC1CmE,QAAQ;gBACR3B,OAAO0B;YACT;YACA;QACF;QAEA,MAAME,OAAON,2BAA2BX,GAAG,CAACnD,gBAAgBqE,QAAQ;QACpE,MAAMC,WAAWC,KAAKC,GAAG,CAACN,WAAWK,KAAKE,GAAG,CAAC,GAAGtD,YAAYqB,KAAK,GAAG4B;QACrEf,sBAAsBpC,GAAG,CAACjB,eAAesE;QACzC,IAAIA,WAAWJ,WAAW;YACxBZ,yBAAyBrC,GAAG,CAACjB,eAAe;gBAC1CmE,QAAQ;gBACR3B,OAAO0B,YAAYI;YACrB;QACF;IACF;IAEA,OAAO;QAACjB;QAAuBC;IAAwB;AACzD"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { RunnerInstance, RunnerInstanceState } from '#core/entities/runner-instance.js';
|
|
2
2
|
import type { Tx } from './db.js';
|
|
3
3
|
import { type RunnerInstanceBoundJobExecution } from './job-executions.js';
|
|
4
|
-
export declare const terminalStates: readonly ["stopped", "failed", "terminated"];
|
|
5
4
|
export declare const activeStates: readonly ["starting", "running", "stopping"];
|
|
6
5
|
export declare const divergenceCountStates: readonly ["starting", "running"];
|
|
7
|
-
export type RunnerInstanceTerminateIntentReason = 'job-cancelled';
|
|
6
|
+
export type RunnerInstanceTerminateIntentReason = 'activation-timeout' | 'job-cancelled';
|
|
8
7
|
export interface RunnerInstanceTerminateIntent {
|
|
9
8
|
providerRunnerId: string;
|
|
10
9
|
reason: RunnerInstanceTerminateIntentReason;
|
|
10
|
+
activationTimeoutRetry?: boolean;
|
|
11
11
|
}
|
|
12
12
|
export interface ActiveRunnerInstanceTemplateCount {
|
|
13
13
|
templateKey: string;
|
|
@@ -61,6 +61,9 @@ export declare function listActiveRunnerInstanceCountsByTemplateTx(tx: Tx, param
|
|
|
61
61
|
workspaceId: string;
|
|
62
62
|
provisionerId: string;
|
|
63
63
|
}): Promise<ActiveRunnerInstanceTemplateCount[]>;
|
|
64
|
+
export declare function countStaleEnrolledRunnerInstances(params: {
|
|
65
|
+
graceSeconds: number;
|
|
66
|
+
}): Promise<number>;
|
|
64
67
|
export declare function listActiveRunnerInstances(params: {
|
|
65
68
|
workspaceId: string;
|
|
66
69
|
windowSeconds: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runner-instances.d.ts","sourceRoot":"","sources":["../../src/db/runner-instances.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"runner-instances.d.ts","sourceRoot":"","sources":["../../src/db/runner-instances.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { logger } from '@shipfox/node-opentelemetry';
|
|
2
2
|
import { and, asc, desc, eq, exists, gt, inArray, isNotNull, isNull, lt, notExists, notInArray, or, sql } from 'drizzle-orm';
|
|
3
3
|
import { alias } from 'drizzle-orm/pg-core';
|
|
4
|
+
import { config } from '#config.js';
|
|
4
5
|
import { sanitizeRunnerLabels } from '#core/runner-labels.js';
|
|
6
|
+
import { recordRunnerReservationCapacityFailure } from '#metrics/index.js';
|
|
5
7
|
import { db } from './db.js';
|
|
6
8
|
import { listRunningJobExecutionsByRunnerInstanceTx } from './job-executions.js';
|
|
9
|
+
import { lockRunnerReservationAdvisoryKeysTx } from './reservation-locks.js';
|
|
7
10
|
import { releaseReservationUnits } from './reservations.js';
|
|
11
|
+
import { validateRunnerReservationCapacityTx } from './runner-assignments.js';
|
|
12
|
+
import { terminalStates } from './runner-states.js';
|
|
8
13
|
import { ephemeralRegistrationTokens } from './schema/ephemeral-registration-tokens.js';
|
|
9
14
|
import { provisionerTokens } from './schema/provisioner-tokens.js';
|
|
10
15
|
import { reservations } from './schema/reservations.js';
|
|
16
|
+
import { runnerControlSessions } from './schema/runner-control-sessions.js';
|
|
11
17
|
import { providerRunners, toRunnerInstance } from './schema/runner-instances.js';
|
|
12
18
|
import { runnerSessions } from './schema/runner-sessions.js';
|
|
13
19
|
import { runningJobExecutions } from './schema/running-job-executions.js';
|
|
14
|
-
export const terminalStates = [
|
|
15
|
-
'stopped',
|
|
16
|
-
'failed',
|
|
17
|
-
'terminated'
|
|
18
|
-
];
|
|
19
20
|
export const activeStates = [
|
|
20
21
|
'starting',
|
|
21
22
|
'running',
|
|
@@ -50,8 +51,9 @@ export async function reportRunnerInstances(params) {
|
|
|
50
51
|
await tx.execute(sql`select pg_advisory_xact_lock(hashtext(${params.workspaceId ?? params.provisionerId}))`);
|
|
51
52
|
}
|
|
52
53
|
const events = await hydrateRunnerSessionIdsFromConsumedTokens(tx, params, aggregatedEvents);
|
|
53
|
-
const
|
|
54
|
-
const
|
|
54
|
+
const reservationSafeEvents = await guardReportedReservationIdsTx(tx, params, events);
|
|
55
|
+
const terminateIntentsHonored = await listTerminateIntentsHonoredByTerminatedReportsTx(tx, params, reservationSafeEvents);
|
|
56
|
+
const values = reservationSafeEvents.map((event)=>({
|
|
55
57
|
workspaceId: params.workspaceId,
|
|
56
58
|
provisionerId: params.provisionerId,
|
|
57
59
|
providerRunnerId: event.providerRunnerId,
|
|
@@ -80,7 +82,7 @@ export async function reportRunnerInstances(params) {
|
|
|
80
82
|
],
|
|
81
83
|
targetWhere: isNotNull(providerRunners.providerRunnerId),
|
|
82
84
|
set: {
|
|
83
|
-
reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(
|
|
85
|
+
reservationId: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(${providerRunners.reservationId}, excluded.reservation_id) ELSE ${providerRunners.reservationId} END`,
|
|
84
86
|
templateKey: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN coalesce(excluded.template_key, ${providerRunners.templateKey}) ELSE ${providerRunners.templateKey} END`,
|
|
85
87
|
labels: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.labels ELSE ${providerRunners.labels} END`,
|
|
86
88
|
state: sql`CASE WHEN ${providerRunnerProjectionUpdateCondition()} THEN excluded.state ELSE ${providerRunners.state} END`,
|
|
@@ -100,14 +102,92 @@ export async function reportRunnerInstances(params) {
|
|
|
100
102
|
OR ${providerRunnerMilestoneUpdateCondition()}
|
|
101
103
|
`
|
|
102
104
|
});
|
|
103
|
-
const reservationsReleased = hasTerminalEvent ? await releaseTerminalRunnerInstanceReservations(tx, params,
|
|
105
|
+
const reservationsReleased = hasTerminalEvent ? await releaseTerminalRunnerInstanceReservations(tx, params, reservationSafeEvents) : 0;
|
|
104
106
|
return {
|
|
105
|
-
accepted:
|
|
107
|
+
accepted: reservationSafeEvents.length,
|
|
106
108
|
reservationsReleased,
|
|
107
109
|
terminateIntentsHonored
|
|
108
110
|
};
|
|
109
111
|
});
|
|
110
112
|
}
|
|
113
|
+
async function guardReportedReservationIdsTx(tx, params, events) {
|
|
114
|
+
const reservationIds = [
|
|
115
|
+
...new Set(events.flatMap((event)=>event.reservationId ? [
|
|
116
|
+
event.reservationId
|
|
117
|
+
] : []))
|
|
118
|
+
].sort();
|
|
119
|
+
if (reservationIds.length === 0) return events;
|
|
120
|
+
await lockRunnerReservationAdvisoryKeysTx(tx, {
|
|
121
|
+
provisionerId: params.provisionerId,
|
|
122
|
+
reservationIds
|
|
123
|
+
});
|
|
124
|
+
const providerRunnerIds = events.map((event)=>event.providerRunnerId);
|
|
125
|
+
const existingRows = await tx.select({
|
|
126
|
+
providerRunnerId: providerRunners.providerRunnerId,
|
|
127
|
+
reservationId: providerRunners.reservationId,
|
|
128
|
+
intendedReservationId: providerRunners.intendedReservationId,
|
|
129
|
+
reservationReleasedAt: providerRunners.reservationReleasedAt
|
|
130
|
+
}).from(providerRunners).where(and(eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.providerRunnerId, providerRunnerIds))).for('update');
|
|
131
|
+
const existingByProviderRunnerId = new Map(existingRows.flatMap((row)=>row.providerRunnerId ? [
|
|
132
|
+
[
|
|
133
|
+
row.providerRunnerId,
|
|
134
|
+
row
|
|
135
|
+
]
|
|
136
|
+
] : []));
|
|
137
|
+
const candidateReservationByProviderRunnerId = new Map();
|
|
138
|
+
for (const event of events){
|
|
139
|
+
if (!event.reservationId) continue;
|
|
140
|
+
if (isTerminalState(event.state)) continue;
|
|
141
|
+
const existing = existingByProviderRunnerId.get(event.providerRunnerId);
|
|
142
|
+
if (existing?.reservationId || existing?.intendedReservationId === event.reservationId) continue;
|
|
143
|
+
if (existing?.intendedReservationId || existing?.reservationReleasedAt) continue;
|
|
144
|
+
candidateReservationByProviderRunnerId.set(event.providerRunnerId, event.reservationId);
|
|
145
|
+
}
|
|
146
|
+
const validation = await validateRunnerReservationCapacityTx(tx, {
|
|
147
|
+
provisionerId: params.provisionerId,
|
|
148
|
+
requests: [
|
|
149
|
+
...candidateReservationByProviderRunnerId.values()
|
|
150
|
+
].map((reservationId)=>({
|
|
151
|
+
reservationId,
|
|
152
|
+
count: 1
|
|
153
|
+
}))
|
|
154
|
+
}, {
|
|
155
|
+
advisoryLocksHeld: true
|
|
156
|
+
});
|
|
157
|
+
for (const { reason, count } of validation.unavailableByReservation.values())recordRunnerReservationCapacityFailure(reason, count);
|
|
158
|
+
const remainingAcceptedByReservation = new Map(validation.acceptedByReservation);
|
|
159
|
+
return events.map((event)=>{
|
|
160
|
+
const existing = existingByProviderRunnerId.get(event.providerRunnerId);
|
|
161
|
+
const reservationId = candidateReservationByProviderRunnerId.get(event.providerRunnerId);
|
|
162
|
+
if (!reservationId) {
|
|
163
|
+
if (event.reservationId && isTerminalState(event.state)) {
|
|
164
|
+
// Terminal cleanup releases the IDs stored on the projection row. An arbitrary
|
|
165
|
+
// terminal report must not consume reservation capacity during that cleanup.
|
|
166
|
+
const matchesExistingReservation = existing !== undefined && (existing.reservationId === event.reservationId || existing.intendedReservationId === event.reservationId);
|
|
167
|
+
if (!matchesExistingReservation) return {
|
|
168
|
+
...event,
|
|
169
|
+
reservationId: null
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (event.reservationId && existing?.intendedReservationId && existing.intendedReservationId !== event.reservationId) return {
|
|
173
|
+
...event,
|
|
174
|
+
reservationId: null
|
|
175
|
+
};
|
|
176
|
+
if (event.reservationId && existing?.reservationReleasedAt) return {
|
|
177
|
+
...event,
|
|
178
|
+
reservationId: null
|
|
179
|
+
};
|
|
180
|
+
return event;
|
|
181
|
+
}
|
|
182
|
+
const remaining = remainingAcceptedByReservation.get(reservationId) ?? 0;
|
|
183
|
+
if (remaining === 0) return {
|
|
184
|
+
...event,
|
|
185
|
+
reservationId: null
|
|
186
|
+
};
|
|
187
|
+
remainingAcceptedByReservation.set(reservationId, remaining - 1);
|
|
188
|
+
return event;
|
|
189
|
+
});
|
|
190
|
+
}
|
|
111
191
|
export async function listActiveRunnerInstanceCountsByTemplateTx(tx, params) {
|
|
112
192
|
const rows = await tx.select({
|
|
113
193
|
templateKey: providerRunners.templateKey,
|
|
@@ -122,6 +202,14 @@ export async function listActiveRunnerInstanceCountsByTemplateTx(tx, params) {
|
|
|
122
202
|
}
|
|
123
203
|
] : []);
|
|
124
204
|
}
|
|
205
|
+
export async function countStaleEnrolledRunnerInstances(params) {
|
|
206
|
+
const [row] = await db().select({
|
|
207
|
+
count: sql`count(*)::int`
|
|
208
|
+
}).from(providerRunners).where(and(eq(providerRunners.state, 'running'), isNull(providerRunners.workspaceId), isNull(providerRunners.runnerSessionId), lt(providerRunners.reportedAt, staleRunnerInstanceCutoff(params.graceSeconds)), exists(db().select({
|
|
209
|
+
id: runnerControlSessions.id
|
|
210
|
+
}).from(runnerControlSessions).where(and(eq(runnerControlSessions.runnerInstanceId, providerRunners.id), isNull(runnerControlSessions.closedAt), gt(runnerControlSessions.expiresAt, sql`now()`))))));
|
|
211
|
+
return row?.count ?? 0;
|
|
212
|
+
}
|
|
125
213
|
export async function listActiveRunnerInstances(params) {
|
|
126
214
|
const rows = await db().select().from(providerRunners).where(and(eq(providerRunners.workspaceId, params.workspaceId), inArray(providerRunners.state, activeStates), sql`${providerRunners.updatedAt} > now() - (${params.windowSeconds} || ' seconds')::interval`)).orderBy(desc(providerRunners.updatedAt), desc(providerRunners.id)).limit(params.limit ?? 1000);
|
|
127
215
|
return rows.map(toRunnerInstance);
|
|
@@ -144,21 +232,47 @@ export async function listProvisionerTerminateIntentRowsTx(tx, params) {
|
|
|
144
232
|
returnedCount: returnedRows.length
|
|
145
233
|
}, 'provisioner terminate intents truncated by poll-demand limit');
|
|
146
234
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
...row,
|
|
150
|
-
providerRunnerId: row.providerRunnerId
|
|
151
|
-
}
|
|
235
|
+
const activationTimeoutRunnerIds = returnedRows.flatMap((row)=>row.reason === 'activation-timeout' && row.providerRunnerId ? [
|
|
236
|
+
row.providerRunnerId
|
|
152
237
|
] : []);
|
|
238
|
+
if (activationTimeoutRunnerIds.length > 0) {
|
|
239
|
+
await tx.update(providerRunners).set({
|
|
240
|
+
reservationReleasedAt: sql`now()`,
|
|
241
|
+
updatedAt: sql`now()`
|
|
242
|
+
}).where(and(eq(providerRunners.workspaceId, params.workspaceId), eq(providerRunners.provisionerId, params.provisionerId), inArray(providerRunners.providerRunnerId, activationTimeoutRunnerIds), isNull(providerRunners.reservationReleasedAt)));
|
|
243
|
+
}
|
|
244
|
+
return returnedRows.flatMap((row)=>toRunnerInstanceTerminateIntent(row));
|
|
245
|
+
}
|
|
246
|
+
function toRunnerInstanceTerminateIntent(row) {
|
|
247
|
+
if (!row.providerRunnerId) return [];
|
|
248
|
+
return [
|
|
249
|
+
{
|
|
250
|
+
providerRunnerId: row.providerRunnerId,
|
|
251
|
+
reason: row.reason,
|
|
252
|
+
...row.reason === 'activation-timeout' && row.activationTimeoutRetry ? {
|
|
253
|
+
activationTimeoutRetry: true
|
|
254
|
+
} : {}
|
|
255
|
+
}
|
|
256
|
+
];
|
|
153
257
|
}
|
|
154
258
|
function provisionerTerminateIntentsQuery(tx, params) {
|
|
155
259
|
const newerRunningJobExecutions = alias(runningJobExecutions, 'newer_running_jobs');
|
|
260
|
+
const latestCancelledJob = exists(tx.select({
|
|
261
|
+
id: runningJobExecutions.id
|
|
262
|
+
}).from(runningJobExecutions).where(and(eq(runningJobExecutions.workspaceId, params.workspaceId), eq(runningJobExecutions.provisionerId, params.provisionerId), eq(runningJobExecutions.providerRunnerId, providerRunners.providerRunnerId), isNotNull(runningJobExecutions.cancellationRequestedAt), notExists(tx.select({
|
|
263
|
+
id: newerRunningJobExecutions.id
|
|
264
|
+
}).from(newerRunningJobExecutions).where(and(eq(newerRunningJobExecutions.workspaceId, runningJobExecutions.workspaceId), eq(newerRunningJobExecutions.provisionerId, runningJobExecutions.provisionerId), eq(newerRunningJobExecutions.providerRunnerId, runningJobExecutions.providerRunnerId), or(gt(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt), and(eq(newerRunningJobExecutions.startedAt, runningJobExecutions.startedAt), gt(newerRunningJobExecutions.jobExecutionId, runningJobExecutions.jobExecutionId)))))))));
|
|
265
|
+
const activationTimeout = and(eq(providerRunners.launchKind, 'demand'), isNull(providerRunners.runnerSessionId), lt(providerRunners.createdAt, sql`now() - (${config.RUNNER_DEMAND_ACTIVATION_TIMEOUT_SECONDS} || ' seconds')::interval`), notExists(tx.select({
|
|
266
|
+
id: reservations.id
|
|
267
|
+
}).from(reservations).where(and(or(eq(reservations.id, providerRunners.reservationId), eq(reservations.id, providerRunners.intendedReservationId)), eq(reservations.workspaceId, params.workspaceId), eq(reservations.provisionerId, params.provisionerId), gt(reservations.expiresAt, sql`now()`)))));
|
|
156
268
|
return tx.select({
|
|
157
269
|
providerRunnerId: providerRunners.providerRunnerId,
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
270
|
+
activationTimeoutRetry: sql`${providerRunners.reservationReleasedAt} is not null`,
|
|
271
|
+
reason: sql`case
|
|
272
|
+
when ${activationTimeout} then 'activation-timeout'
|
|
273
|
+
else 'job-cancelled'
|
|
274
|
+
end`
|
|
275
|
+
}).from(providerRunners).where(and(eq(providerRunners.workspaceId, params.workspaceId), eq(providerRunners.provisionerId, params.provisionerId), isNotNull(providerRunners.providerRunnerId), inArray(providerRunners.state, activeStates), params.providerRunnerIds && params.providerRunnerIds.length > 0 ? inArray(providerRunners.providerRunnerId, params.providerRunnerIds) : undefined, or(latestCancelledJob, activationTimeout)));
|
|
162
276
|
}
|
|
163
277
|
async function listTerminateIntentsHonoredByTerminatedReportsTx(tx, params, events) {
|
|
164
278
|
if (!params.workspaceId) return [];
|
|
@@ -171,12 +285,7 @@ async function listTerminateIntentsHonoredByTerminatedReportsTx(tx, params, even
|
|
|
171
285
|
provisionerId: params.provisionerId,
|
|
172
286
|
providerRunnerIds: terminatedRunnerInstanceIds
|
|
173
287
|
}).orderBy(asc(providerRunners.providerRunnerId));
|
|
174
|
-
return rows.flatMap((row)=>row
|
|
175
|
-
{
|
|
176
|
-
...row,
|
|
177
|
-
providerRunnerId: row.providerRunnerId
|
|
178
|
-
}
|
|
179
|
-
] : []);
|
|
288
|
+
return rows.flatMap((row)=>toRunnerInstanceTerminateIntent(row));
|
|
180
289
|
}
|
|
181
290
|
export async function reconcileRunnerInstances(params) {
|
|
182
291
|
const observedRunnerInstanceIds = [
|