@shipfox/api-runners 12.6.0 → 13.0.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 +20 -0
- 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/job-executions.d.ts +8 -21
- package/dist/db/job-executions.d.ts.map +1 -1
- package/dist/db/job-executions.js +63 -60
- package/dist/db/job-executions.js.map +1 -1
- package/dist/db/provisioner-tokens.d.ts.map +1 -1
- package/dist/db/provisioner-tokens.js +14 -2
- package/dist/db/provisioner-tokens.js.map +1 -1
- package/dist/db/reservations.d.ts +10 -0
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +139 -17
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-assignments.d.ts.map +1 -1
- package/dist/db/runner-assignments.js +8 -5
- package/dist/db/runner-assignments.js.map +1 -1
- package/dist/db/runner-instances.d.ts +0 -1
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +85 -100
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-states.d.ts +3 -1
- package/dist/db/runner-states.d.ts.map +1 -1
- package/dist/db/runner-states.js +6 -4
- package/dist/db/runner-states.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/presentation/index.d.ts +2 -1
- package/dist/presentation/index.d.ts.map +1 -1
- package/dist/presentation/index.js +2 -1
- package/dist/presentation/index.js.map +1 -1
- package/dist/presentation/inter-module.d.ts +0 -1
- package/dist/presentation/inter-module.d.ts.map +1 -1
- package/dist/presentation/inter-module.js +2 -25
- package/dist/presentation/inter-module.js.map +1 -1
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.d.ts +3 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.d.ts.map +1 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.js +21 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-queued.js.map +1 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-terminated.d.ts +3 -0
- package/dist/presentation/subscribers/on-workflows-job-execution-terminated.d.ts.map +1 -0
- package/dist/presentation/subscribers/{on-workflows-job-execution-timed-out.js → on-workflows-job-execution-terminated.js} +5 -4
- package/dist/presentation/subscribers/on-workflows-job-execution-terminated.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/package.json +3 -3
- package/src/db/index.ts +0 -2
- package/src/db/job-executions.test.ts +106 -217
- package/src/db/job-executions.ts +71 -70
- package/src/db/provisioner-tokens.test.ts +19 -0
- package/src/db/provisioner-tokens.ts +13 -2
- package/src/db/reservations.test.ts +138 -0
- package/src/db/reservations.ts +314 -35
- package/src/db/runner-assignments.test.ts +20 -0
- package/src/db/runner-assignments.ts +6 -7
- package/src/db/runner-instances.test.ts +350 -15
- package/src/db/runner-instances.ts +136 -198
- package/src/db/runner-states.test.ts +16 -0
- package/src/db/runner-states.ts +9 -4
- package/src/index.test.ts +1 -0
- package/src/index.ts +8 -7
- package/src/presentation/index.ts +2 -1
- package/src/presentation/inter-module.ts +2 -40
- package/src/presentation/subscribers/on-workflows-job-execution-queued.test.ts +57 -0
- package/src/presentation/subscribers/on-workflows-job-execution-queued.ts +26 -0
- package/src/presentation/subscribers/on-workflows-job-execution-terminated.test.ts +142 -0
- package/src/presentation/subscribers/{on-workflows-job-execution-timed-out.ts → on-workflows-job-execution-terminated.ts} +5 -4
- package/test/factories/pending-job.ts +3 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts +0 -3
- package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.d.ts.map +0 -1
- package/dist/presentation/subscribers/on-workflows-job-execution-timed-out.js.map +0 -1
- package/src/presentation/inter-module.test.ts +0 -15
- package/src/presentation/subscribers/on-workflows-job-execution-timed-out.test.ts +0 -163
|
@@ -2,6 +2,10 @@ import {pgClient} from '@shipfox/node-postgres';
|
|
|
2
2
|
import {and, desc, eq, inArray, or, sql} from 'drizzle-orm';
|
|
3
3
|
import {db} from '#db/db.js';
|
|
4
4
|
import {createRunnerSessionConsumingEphemeralToken} from '#db/ephemeral-registration-tokens.js';
|
|
5
|
+
import {
|
|
6
|
+
pollDemandAndReserve,
|
|
7
|
+
releaseTerminalRunnerInstanceReservationsByIds,
|
|
8
|
+
} from '#db/reservations.js';
|
|
5
9
|
import {
|
|
6
10
|
attachRunnerInstanceProviderId,
|
|
7
11
|
countStaleEnrolledRunnerInstances,
|
|
@@ -22,6 +26,7 @@ import {runnerSessions} from '#db/schema/runner-sessions.js';
|
|
|
22
26
|
import {runningJobExecutions} from '#db/schema/running-job-executions.js';
|
|
23
27
|
import {
|
|
24
28
|
ephemeralRegistrationTokenFactory,
|
|
29
|
+
pendingJobFactory,
|
|
25
30
|
providerRunnerFactory,
|
|
26
31
|
provisionerTokenFactory,
|
|
27
32
|
reservationFactory,
|
|
@@ -221,6 +226,39 @@ describe('reportRunnerInstances', () => {
|
|
|
221
226
|
expect(rows[0]?.reason).toBe('late stale failure');
|
|
222
227
|
});
|
|
223
228
|
|
|
229
|
+
it('does not release a reservation for a terminal report older than the projection', async () => {
|
|
230
|
+
const reservationId = await createReservation(1);
|
|
231
|
+
const currentReportedAt = new Date('2025-01-01T00:01:00.000Z');
|
|
232
|
+
await providerRunnerFactory.create({
|
|
233
|
+
workspaceId,
|
|
234
|
+
provisionerId,
|
|
235
|
+
providerRunnerId: 'stale-terminal-runner',
|
|
236
|
+
reservationId,
|
|
237
|
+
state: 'running',
|
|
238
|
+
reportedAt: currentReportedAt,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
const result = await reportRunnerInstances({
|
|
242
|
+
scope: 'workspace',
|
|
243
|
+
workspaceId,
|
|
244
|
+
provisionerId,
|
|
245
|
+
events: [
|
|
246
|
+
event({
|
|
247
|
+
providerRunnerId: 'stale-terminal-runner',
|
|
248
|
+
reservationId,
|
|
249
|
+
state: 'failed',
|
|
250
|
+
reportedAt: new Date(currentReportedAt.getTime() - 1_000),
|
|
251
|
+
}),
|
|
252
|
+
],
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const [providerRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
256
|
+
const [reservation] = await reservationRowsFor({workspaceId, provisionerId});
|
|
257
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 0, terminateIntentsHonored: []});
|
|
258
|
+
expect(providerRunner).toMatchObject({state: 'failed', reservationReleasedAt: null});
|
|
259
|
+
expect(reservation?.count).toBe(1);
|
|
260
|
+
});
|
|
261
|
+
|
|
224
262
|
it('rejects older out-of-order events in the same lifecycle state', async () => {
|
|
225
263
|
const newest = new Date();
|
|
226
264
|
await reportRunnerInstances({
|
|
@@ -1036,8 +1074,16 @@ describe('reportRunnerInstances', () => {
|
|
|
1036
1074
|
expect(providerRunnerRows[0]?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1037
1075
|
});
|
|
1038
1076
|
|
|
1039
|
-
it('
|
|
1077
|
+
it('releases a reservation for a provisioned runner that already has a runner session', async () => {
|
|
1040
1078
|
const reservationId = await createReservation(1);
|
|
1079
|
+
const runnerSession = await runnerSessionFactory.create({workspaceId});
|
|
1080
|
+
await providerRunnerFactory.create({
|
|
1081
|
+
workspaceId,
|
|
1082
|
+
provisionerId,
|
|
1083
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
1084
|
+
reservationId,
|
|
1085
|
+
state: 'running',
|
|
1086
|
+
});
|
|
1041
1087
|
|
|
1042
1088
|
const result = await reportRunnerInstances({
|
|
1043
1089
|
scope: 'workspace',
|
|
@@ -1048,16 +1094,155 @@ describe('reportRunnerInstances', () => {
|
|
|
1048
1094
|
providerRunnerId: 'provisioned-runner-1',
|
|
1049
1095
|
reservationId,
|
|
1050
1096
|
state: 'failed',
|
|
1051
|
-
runnerSessionId:
|
|
1097
|
+
runnerSessionId: runnerSession.id,
|
|
1052
1098
|
}),
|
|
1053
1099
|
],
|
|
1054
1100
|
});
|
|
1055
1101
|
|
|
1056
1102
|
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
1057
1103
|
const providerRunnerRows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1104
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 1, terminateIntentsHonored: []});
|
|
1105
|
+
expect(reservationRows).toHaveLength(0);
|
|
1106
|
+
expect(providerRunnerRows[0]?.runnerSessionId).toBe(runnerSession.id);
|
|
1107
|
+
expect(providerRunnerRows[0]?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1108
|
+
});
|
|
1109
|
+
|
|
1110
|
+
it('keeps a reservation while a terminal runner still has a running job', async () => {
|
|
1111
|
+
const reservationId = await createReservation(1);
|
|
1112
|
+
const runnerSession = await runnerSessionFactory.create({workspaceId});
|
|
1113
|
+
await providerRunnerFactory.create({
|
|
1114
|
+
workspaceId,
|
|
1115
|
+
provisionerId,
|
|
1116
|
+
providerRunnerId: 'busy-terminal-runner',
|
|
1117
|
+
reservationId,
|
|
1118
|
+
runnerSessionId: runnerSession.id,
|
|
1119
|
+
state: 'running',
|
|
1120
|
+
});
|
|
1121
|
+
await insertRunningJobRow({
|
|
1122
|
+
workspaceId,
|
|
1123
|
+
provisionerId,
|
|
1124
|
+
providerRunnerId: 'busy-terminal-runner',
|
|
1125
|
+
});
|
|
1126
|
+
|
|
1127
|
+
const result = await reportRunnerInstances({
|
|
1128
|
+
scope: 'workspace',
|
|
1129
|
+
workspaceId,
|
|
1130
|
+
provisionerId,
|
|
1131
|
+
events: [
|
|
1132
|
+
event({
|
|
1133
|
+
providerRunnerId: 'busy-terminal-runner',
|
|
1134
|
+
reservationId,
|
|
1135
|
+
state: 'failed',
|
|
1136
|
+
runnerSessionId: runnerSession.id,
|
|
1137
|
+
}),
|
|
1138
|
+
],
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
const [providerRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1142
|
+
const [reservation] = await reservationRowsFor({workspaceId, provisionerId});
|
|
1058
1143
|
expect(result).toEqual({accepted: 1, reservationsReleased: 0, terminateIntentsHonored: []});
|
|
1059
|
-
expect(
|
|
1060
|
-
expect(
|
|
1144
|
+
expect(providerRunner).toMatchObject({state: 'failed', reservationReleasedAt: null});
|
|
1145
|
+
expect(reservation?.count).toBe(1);
|
|
1146
|
+
});
|
|
1147
|
+
|
|
1148
|
+
it('keeps a reservation when a terminal runner has cancelled and uncancelled jobs', async () => {
|
|
1149
|
+
const reservationId = await createReservation(1);
|
|
1150
|
+
const runnerSession = await runnerSessionFactory.create({workspaceId});
|
|
1151
|
+
await providerRunnerFactory.create({
|
|
1152
|
+
workspaceId,
|
|
1153
|
+
provisionerId,
|
|
1154
|
+
providerRunnerId: 'mixed-terminal-runner',
|
|
1155
|
+
reservationId,
|
|
1156
|
+
runnerSessionId: runnerSession.id,
|
|
1157
|
+
state: 'running',
|
|
1158
|
+
});
|
|
1159
|
+
await insertRunningJobRow({
|
|
1160
|
+
workspaceId,
|
|
1161
|
+
provisionerId,
|
|
1162
|
+
providerRunnerId: 'mixed-terminal-runner',
|
|
1163
|
+
startedAt: new Date('2025-01-01T00:00:00.000Z'),
|
|
1164
|
+
cancellationRequestedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
1165
|
+
});
|
|
1166
|
+
await insertRunningJobRow({
|
|
1167
|
+
workspaceId,
|
|
1168
|
+
provisionerId,
|
|
1169
|
+
providerRunnerId: 'mixed-terminal-runner',
|
|
1170
|
+
startedAt: new Date('2025-01-01T00:02:00.000Z'),
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
const result = await reportRunnerInstances({
|
|
1174
|
+
scope: 'workspace',
|
|
1175
|
+
workspaceId,
|
|
1176
|
+
provisionerId,
|
|
1177
|
+
events: [
|
|
1178
|
+
event({
|
|
1179
|
+
providerRunnerId: 'mixed-terminal-runner',
|
|
1180
|
+
reservationId,
|
|
1181
|
+
state: 'terminated',
|
|
1182
|
+
runnerSessionId: runnerSession.id,
|
|
1183
|
+
}),
|
|
1184
|
+
],
|
|
1185
|
+
});
|
|
1186
|
+
|
|
1187
|
+
const [providerRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1188
|
+
const [reservation] = await reservationRowsFor({workspaceId, provisionerId});
|
|
1189
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 0, terminateIntentsHonored: []});
|
|
1190
|
+
expect(providerRunner).toMatchObject({state: 'terminated', reservationReleasedAt: null});
|
|
1191
|
+
expect(reservation?.count).toBe(1);
|
|
1192
|
+
});
|
|
1193
|
+
|
|
1194
|
+
it('releases a reservation when a terminal runner only has a cancelled job', async () => {
|
|
1195
|
+
const reservationId = await createReservation(1);
|
|
1196
|
+
const runnerSession = await runnerSessionFactory.create({workspaceId});
|
|
1197
|
+
await providerRunnerFactory.create({
|
|
1198
|
+
workspaceId,
|
|
1199
|
+
provisionerId,
|
|
1200
|
+
providerRunnerId: 'cancelled-terminal-runner',
|
|
1201
|
+
reservationId,
|
|
1202
|
+
runnerSessionId: runnerSession.id,
|
|
1203
|
+
state: 'running',
|
|
1204
|
+
});
|
|
1205
|
+
await insertRunningJobRow({
|
|
1206
|
+
workspaceId,
|
|
1207
|
+
provisionerId,
|
|
1208
|
+
providerRunnerId: 'cancelled-terminal-runner',
|
|
1209
|
+
cancellationRequestedAt: new Date('2025-01-01T00:01:00.000Z'),
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
const result = await reportRunnerInstances({
|
|
1213
|
+
scope: 'workspace',
|
|
1214
|
+
workspaceId,
|
|
1215
|
+
provisionerId,
|
|
1216
|
+
events: [
|
|
1217
|
+
event({
|
|
1218
|
+
providerRunnerId: 'cancelled-terminal-runner',
|
|
1219
|
+
reservationId,
|
|
1220
|
+
state: 'terminated',
|
|
1221
|
+
runnerSessionId: runnerSession.id,
|
|
1222
|
+
}),
|
|
1223
|
+
],
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
const [providerRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1227
|
+
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
1228
|
+
const runningJobRows = await db()
|
|
1229
|
+
.select()
|
|
1230
|
+
.from(runningJobExecutions)
|
|
1231
|
+
.where(eq(runningJobExecutions.providerRunnerId, 'cancelled-terminal-runner'));
|
|
1232
|
+
expect(result).toEqual({
|
|
1233
|
+
accepted: 1,
|
|
1234
|
+
reservationsReleased: 1,
|
|
1235
|
+
terminateIntentsHonored: [
|
|
1236
|
+
{providerRunnerId: 'cancelled-terminal-runner', reason: 'job-cancelled'},
|
|
1237
|
+
],
|
|
1238
|
+
});
|
|
1239
|
+
expect(providerRunner).toMatchObject({
|
|
1240
|
+
state: 'terminated',
|
|
1241
|
+
reservationReleasedAt: expect.any(Date),
|
|
1242
|
+
});
|
|
1243
|
+
expect(reservationRows).toHaveLength(0);
|
|
1244
|
+
expect(runningJobRows).toHaveLength(1);
|
|
1245
|
+
expect(runningJobRows[0]?.cancellationRequestedAt).toBeInstanceOf(Date);
|
|
1061
1246
|
});
|
|
1062
1247
|
|
|
1063
1248
|
it('uses the consumed ephemeral token session before releasing a terminal report', async () => {
|
|
@@ -1068,6 +1253,13 @@ describe('reportRunnerInstances', () => {
|
|
|
1068
1253
|
reservationId,
|
|
1069
1254
|
providerRunnerId: 'provisioned-runner-1',
|
|
1070
1255
|
});
|
|
1256
|
+
await providerRunnerFactory.create({
|
|
1257
|
+
workspaceId,
|
|
1258
|
+
provisionerId,
|
|
1259
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
1260
|
+
reservationId,
|
|
1261
|
+
state: 'running',
|
|
1262
|
+
});
|
|
1071
1263
|
const session = await createRunnerSessionConsumingEphemeralToken({
|
|
1072
1264
|
ephemeralTokenId: token.id,
|
|
1073
1265
|
workspaceId,
|
|
@@ -1091,16 +1283,24 @@ describe('reportRunnerInstances', () => {
|
|
|
1091
1283
|
|
|
1092
1284
|
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
1093
1285
|
const providerRunnerRows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1094
|
-
expect(result).toEqual({accepted: 1, reservationsReleased:
|
|
1095
|
-
expect(reservationRows
|
|
1286
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 1, terminateIntentsHonored: []});
|
|
1287
|
+
expect(reservationRows).toHaveLength(0);
|
|
1096
1288
|
expect(providerRunnerRows[0]?.runnerSessionId).toBe(session.id);
|
|
1097
|
-
expect(providerRunnerRows[0]?.reservationReleasedAt).
|
|
1289
|
+
expect(providerRunnerRows[0]?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1098
1290
|
});
|
|
1099
1291
|
|
|
1100
1292
|
it('preserves claimed runner session metadata when a terminal state wins the batch', async () => {
|
|
1101
1293
|
const reservationId = await createReservation(1);
|
|
1102
1294
|
const runnerSessionId = crypto.randomUUID();
|
|
1103
1295
|
const reportedAt = new Date('2025-01-01T00:00:00.000Z');
|
|
1296
|
+
await providerRunnerFactory.create({
|
|
1297
|
+
workspaceId,
|
|
1298
|
+
provisionerId,
|
|
1299
|
+
providerRunnerId: 'provisioned-runner-1',
|
|
1300
|
+
reservationId,
|
|
1301
|
+
state: 'running',
|
|
1302
|
+
reportedAt: new Date(reportedAt.getTime() - 1_000),
|
|
1303
|
+
});
|
|
1104
1304
|
|
|
1105
1305
|
const result = await reportRunnerInstances({
|
|
1106
1306
|
scope: 'workspace',
|
|
@@ -1125,11 +1325,11 @@ describe('reportRunnerInstances', () => {
|
|
|
1125
1325
|
|
|
1126
1326
|
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
1127
1327
|
const providerRunnerRows = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1128
|
-
expect(result).toEqual({accepted: 1, reservationsReleased:
|
|
1129
|
-
expect(reservationRows
|
|
1328
|
+
expect(result).toEqual({accepted: 1, reservationsReleased: 1, terminateIntentsHonored: []});
|
|
1329
|
+
expect(reservationRows).toHaveLength(0);
|
|
1130
1330
|
expect(providerRunnerRows[0]?.state).toBe('failed');
|
|
1131
1331
|
expect(providerRunnerRows[0]?.runnerSessionId).toBe(runnerSessionId);
|
|
1132
|
-
expect(providerRunnerRows[0]?.reservationReleasedAt).
|
|
1332
|
+
expect(providerRunnerRows[0]?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1133
1333
|
});
|
|
1134
1334
|
|
|
1135
1335
|
it('returns honored terminate intents only for the first active-to-terminated transition', async () => {
|
|
@@ -1642,7 +1842,6 @@ describe('reapStaleRunnerInstances', () => {
|
|
|
1642
1842
|
.values({
|
|
1643
1843
|
provisionerId: installationProvisioner.id,
|
|
1644
1844
|
intendedReservationId: reservation.id,
|
|
1645
|
-
providerRunnerId: 'stale-installation-runner-with-reservation',
|
|
1646
1845
|
state: 'starting',
|
|
1647
1846
|
reportedAt: staleAt(),
|
|
1648
1847
|
updatedAt: staleAt(),
|
|
@@ -1746,6 +1945,30 @@ describe('reapStaleRunnerInstances', () => {
|
|
|
1746
1945
|
expect(reservationRows.find((row) => row.id === reservationId)).toBeUndefined();
|
|
1747
1946
|
});
|
|
1748
1947
|
|
|
1948
|
+
it('drains stale terminal rows with unreleased session-linked reservations', async () => {
|
|
1949
|
+
const reservationId = await createReservation(1);
|
|
1950
|
+
const session = await createLinkedSession({
|
|
1951
|
+
providerRunnerId: 'legacy-terminal-runner',
|
|
1952
|
+
updatedAt: staleAt(),
|
|
1953
|
+
});
|
|
1954
|
+
await createRunnerInstance({
|
|
1955
|
+
providerRunnerId: 'legacy-terminal-runner',
|
|
1956
|
+
reservationId,
|
|
1957
|
+
runnerSessionId: session.id,
|
|
1958
|
+
state: 'failed',
|
|
1959
|
+
reportedAt: staleAt(),
|
|
1960
|
+
updatedAt: staleAt(),
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
const result = await reapStaleRunnerInstances({thresholdSeconds: 60, limit: 100});
|
|
1964
|
+
|
|
1965
|
+
const [providerRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
1966
|
+
const reservationRows = await reservationRowsFor({workspaceId, provisionerId});
|
|
1967
|
+
expect(result).toEqual({reaped: 0, reservationsReleased: 1});
|
|
1968
|
+
expect(providerRunner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
1969
|
+
expect(reservationRows).toHaveLength(0);
|
|
1970
|
+
});
|
|
1971
|
+
|
|
1749
1972
|
it('flags expired releases and drains only the configured batch size', async () => {
|
|
1750
1973
|
const activeReservationId = await createReservation(2);
|
|
1751
1974
|
const expiredReservationId = await createReservation(1, {
|
|
@@ -1786,6 +2009,52 @@ describe('reapStaleRunnerInstances', () => {
|
|
|
1786
2009
|
).toBe(true);
|
|
1787
2010
|
});
|
|
1788
2011
|
|
|
2012
|
+
it('waits for reservation assignment locks before releasing terminal runners', async () => {
|
|
2013
|
+
const reservationId = await createReservation(1);
|
|
2014
|
+
const runner = await createRunnerInstance({
|
|
2015
|
+
providerRunnerId: 'terminal-assignment-race',
|
|
2016
|
+
reservationId,
|
|
2017
|
+
state: 'failed',
|
|
2018
|
+
reportedAt: staleAt(),
|
|
2019
|
+
updatedAt: staleAt(),
|
|
2020
|
+
});
|
|
2021
|
+
const releaseLock = deferred<void>();
|
|
2022
|
+
const lockHolderReady = deferred<void>();
|
|
2023
|
+
const lockHolder = db().transaction(async (tx) => {
|
|
2024
|
+
await tx.execute(
|
|
2025
|
+
sql`select pg_advisory_xact_lock(hashtext(${`runners_assignment:${provisionerId}:${reservationId}`}))`,
|
|
2026
|
+
);
|
|
2027
|
+
lockHolderReady.resolve();
|
|
2028
|
+
await releaseLock.promise;
|
|
2029
|
+
});
|
|
2030
|
+
|
|
2031
|
+
const cleanup = (async () => {
|
|
2032
|
+
await lockHolderReady.promise;
|
|
2033
|
+
return await db().transaction((tx) =>
|
|
2034
|
+
releaseTerminalRunnerInstanceReservationsByIds(tx, {
|
|
2035
|
+
workspaceId,
|
|
2036
|
+
provisionerId,
|
|
2037
|
+
runnerInstanceIds: [runner.id],
|
|
2038
|
+
requireUnlinkedSession: false,
|
|
2039
|
+
}),
|
|
2040
|
+
);
|
|
2041
|
+
})();
|
|
2042
|
+
|
|
2043
|
+
try {
|
|
2044
|
+
await waitForLockWait({queryLike: '%pg_advisory_xact_lock%'});
|
|
2045
|
+
const [beforeRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
2046
|
+
const [beforeReservation] = await reservationRowsFor({workspaceId, provisionerId});
|
|
2047
|
+
expect(beforeRunner?.reservationReleasedAt).toBeNull();
|
|
2048
|
+
expect(beforeReservation?.count).toBe(1);
|
|
2049
|
+
} finally {
|
|
2050
|
+
releaseLock.resolve();
|
|
2051
|
+
await Promise.all([cleanup, lockHolder]);
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
const [providerRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
2055
|
+
expect(providerRunner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
2056
|
+
});
|
|
2057
|
+
|
|
1789
2058
|
it('does not double-release reservations when terminal report and reaper queue on the workspace lock', async () => {
|
|
1790
2059
|
const reservationId = await createReservation(2);
|
|
1791
2060
|
await createRunnerInstance({
|
|
@@ -1973,6 +2242,58 @@ describe('reconcileRunnerInstances', () => {
|
|
|
1973
2242
|
expect(reservation?.count).toBe(1);
|
|
1974
2243
|
});
|
|
1975
2244
|
|
|
2245
|
+
it('releases reservations when reconciling stale installation runners', async () => {
|
|
2246
|
+
const reservationWorkspaceId = crypto.randomUUID();
|
|
2247
|
+
const [reservation] = await db()
|
|
2248
|
+
.insert(reservations)
|
|
2249
|
+
.values({
|
|
2250
|
+
workspaceId: reservationWorkspaceId,
|
|
2251
|
+
provisionerId,
|
|
2252
|
+
requiredLabels: ['linux'],
|
|
2253
|
+
count: 1,
|
|
2254
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
2255
|
+
})
|
|
2256
|
+
.returning();
|
|
2257
|
+
if (!reservation) throw new Error('Expected reservation');
|
|
2258
|
+
const [runner] = await db()
|
|
2259
|
+
.insert(providerRunners)
|
|
2260
|
+
.values({
|
|
2261
|
+
workspaceId: null,
|
|
2262
|
+
provisionerId,
|
|
2263
|
+
providerRunnerId: 'stale-installation-runner',
|
|
2264
|
+
reservationId: reservation.id,
|
|
2265
|
+
runnerSessionId: crypto.randomUUID(),
|
|
2266
|
+
state: 'running',
|
|
2267
|
+
reportedAt: staleReportedAt(),
|
|
2268
|
+
updatedAt: staleReportedAt(),
|
|
2269
|
+
})
|
|
2270
|
+
.returning({id: providerRunners.id});
|
|
2271
|
+
if (!runner) throw new Error('Expected runner');
|
|
2272
|
+
|
|
2273
|
+
const result = await reconcileRunnerInstances({
|
|
2274
|
+
workspaceId: null,
|
|
2275
|
+
provisionerId,
|
|
2276
|
+
observedRunnerInstanceIds: ['observed-runner'],
|
|
2277
|
+
terminateGraceSeconds: 60,
|
|
2278
|
+
});
|
|
2279
|
+
|
|
2280
|
+
const [providerRunner] = await db()
|
|
2281
|
+
.select()
|
|
2282
|
+
.from(providerRunners)
|
|
2283
|
+
.where(eq(providerRunners.id, runner.id));
|
|
2284
|
+
const [remainingReservation] = await db()
|
|
2285
|
+
.select()
|
|
2286
|
+
.from(reservations)
|
|
2287
|
+
.where(eq(reservations.id, reservation.id));
|
|
2288
|
+
expect(result).toMatchObject({
|
|
2289
|
+
absentIds: ['stale-installation-runner'],
|
|
2290
|
+
reservationsReleased: 1,
|
|
2291
|
+
});
|
|
2292
|
+
expect(providerRunner).toMatchObject({state: 'terminated'});
|
|
2293
|
+
expect(providerRunner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
2294
|
+
expect(remainingReservation).toBeUndefined();
|
|
2295
|
+
});
|
|
2296
|
+
|
|
1976
2297
|
it('treats an empty observed set as read-only', async () => {
|
|
1977
2298
|
const reservationId = await createReservation(2);
|
|
1978
2299
|
await createRunnerInstance({
|
|
@@ -2219,8 +2540,9 @@ describe('reconcileRunnerInstances', () => {
|
|
|
2219
2540
|
]);
|
|
2220
2541
|
});
|
|
2221
2542
|
|
|
2222
|
-
it('terminates session-bound absent runners
|
|
2543
|
+
it('terminates session-bound absent runners and releases their reservation', async () => {
|
|
2223
2544
|
const reservationId = await createReservation(1);
|
|
2545
|
+
await pendingJobFactory.create({workspaceId, requiredLabels: ['linux']});
|
|
2224
2546
|
await createRunnerInstance({
|
|
2225
2547
|
providerRunnerId: 'provisioned-runner-1',
|
|
2226
2548
|
reservationId,
|
|
@@ -2237,10 +2559,23 @@ describe('reconcileRunnerInstances', () => {
|
|
|
2237
2559
|
|
|
2238
2560
|
const [providerRunner] = await providerRunnerRowsFor({workspaceId, provisionerId});
|
|
2239
2561
|
const [reservation] = await reservationRowsFor({workspaceId, provisionerId});
|
|
2240
|
-
expect(result.reservationsReleased).toBe(
|
|
2562
|
+
expect(result.reservationsReleased).toBe(1);
|
|
2241
2563
|
expect(providerRunner?.state).toBe('terminated');
|
|
2242
|
-
expect(providerRunner?.reservationReleasedAt).
|
|
2243
|
-
expect(reservation
|
|
2564
|
+
expect(providerRunner?.reservationReleasedAt).toBeInstanceOf(Date);
|
|
2565
|
+
expect(reservation).toBeUndefined();
|
|
2566
|
+
|
|
2567
|
+
const poll = await pollDemandAndReserve({
|
|
2568
|
+
workspaceId,
|
|
2569
|
+
provisionerId,
|
|
2570
|
+
maxReservations: 1,
|
|
2571
|
+
ttlSeconds: 60,
|
|
2572
|
+
templates: [
|
|
2573
|
+
{templateKey: 'linux', labels: ['linux'], availableSlots: 1, starting: 0, running: 0},
|
|
2574
|
+
],
|
|
2575
|
+
});
|
|
2576
|
+
expect(poll.reservations).toHaveLength(1);
|
|
2577
|
+
expect(poll.reservations[0]?.count).toBe(1);
|
|
2578
|
+
expect(poll.stats[0]).toMatchObject({labels: ['linux'], queued: 1, reserved: 1});
|
|
2244
2579
|
});
|
|
2245
2580
|
|
|
2246
2581
|
it('returns a deterministic newest running job execution bound to an observed provisioned runner', async () => {
|