@shipfox/api-runners 12.1.0 → 12.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -4
- package/CHANGELOG.md +35 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -2
- package/dist/config.js.map +1 -1
- package/dist/core/errors.d.ts +4 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +6 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +1 -1
- package/dist/core/index.js.map +1 -1
- package/dist/core/rate-limit.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.d.ts.map +1 -1
- package/dist/core/runner-control-sessions.js +37 -11
- package/dist/core/runner-control-sessions.js.map +1 -1
- package/dist/core/runner-instances.d.ts +1 -0
- package/dist/core/runner-instances.d.ts.map +1 -1
- package/dist/core/runner-instances.js.map +1 -1
- package/dist/core/runner-labels.d.ts +11 -0
- package/dist/core/runner-labels.d.ts.map +1 -0
- package/dist/core/runner-labels.js +41 -0
- package/dist/core/runner-labels.js.map +1 -0
- package/dist/core/runner-sessions.d.ts.map +1 -1
- package/dist/core/runner-sessions.js +5 -1
- package/dist/core/runner-sessions.js.map +1 -1
- package/dist/db/db.d.ts +64 -64
- package/dist/db/ephemeral-registration-tokens.d.ts.map +1 -1
- package/dist/db/ephemeral-registration-tokens.js +12 -2
- package/dist/db/ephemeral-registration-tokens.js.map +1 -1
- package/dist/db/index.d.ts +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +1 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/reservations.d.ts.map +1 -1
- package/dist/db/reservations.js +36 -10
- package/dist/db/reservations.js.map +1 -1
- package/dist/db/runner-instances.d.ts +4 -0
- package/dist/db/runner-instances.d.ts.map +1 -1
- package/dist/db/runner-instances.js +16 -5
- package/dist/db/runner-instances.js.map +1 -1
- package/dist/db/runner-sessions.d.ts.map +1 -1
- package/dist/db/runner-sessions.js +33 -15
- package/dist/db/runner-sessions.js.map +1 -1
- package/dist/db/schema/admin-command-results.d.ts +1 -1
- package/dist/db/schema/ephemeral-registration-tokens.d.ts +1 -1
- package/dist/db/schema/manual-registration-tokens.d.ts +1 -1
- package/dist/db/schema/outbox.d.ts +1 -1
- package/dist/db/schema/pending-job-executions.d.ts +3 -3
- package/dist/db/schema/provisioner-capability-snapshots.d.ts +3 -3
- package/dist/db/schema/provisioner-tokens.d.ts +2 -2
- package/dist/db/schema/rate-limits.d.ts +1 -1
- package/dist/db/schema/reservations.d.ts +3 -3
- package/dist/db/schema/runner-activation-tokens.d.ts +1 -1
- package/dist/db/schema/runner-control-sessions.d.ts +2 -2
- package/dist/db/schema/runner-instances.d.ts +4 -4
- package/dist/db/schema/runner-instances.d.ts.map +1 -1
- package/dist/db/schema/runner-instances.js +1 -0
- package/dist/db/schema/runner-instances.js.map +1 -1
- package/dist/db/schema/runner-sessions.d.ts +4 -4
- package/dist/db/schema/running-job-executions.d.ts +5 -5
- package/dist/metrics/index.d.ts +2 -1
- package/dist/metrics/index.d.ts.map +1 -1
- package/dist/metrics/index.js +1 -1
- package/dist/metrics/index.js.map +1 -1
- package/dist/metrics/instance.d.ts +13 -8
- package/dist/metrics/instance.d.ts.map +1 -1
- package/dist/metrics/instance.js +8 -0
- package/dist/metrics/instance.js.map +1 -1
- package/dist/metrics/service.d.ts.map +1 -1
- package/dist/metrics/service.js +20 -4
- package/dist/metrics/service.js.map +1 -1
- package/dist/presentation/dto/admin-provisioner-token.d.ts +1 -1
- package/dist/presentation/dto/admin-provisioner-token.d.ts.map +1 -1
- package/dist/presentation/dto/admin-runner-instances.d.ts +2 -2
- package/dist/presentation/dto/admin-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.d.ts.map +1 -1
- package/dist/presentation/routes/poll-demand.js +12 -6
- package/dist/presentation/routes/poll-demand.js.map +1 -1
- package/dist/presentation/routes/rate-limit.d.ts.map +1 -1
- package/dist/presentation/routes/register.d.ts.map +1 -1
- package/dist/presentation/routes/register.js +9 -1
- package/dist/presentation/routes/register.js.map +1 -1
- package/dist/presentation/routes/report-runner-instances.d.ts.map +1 -1
- package/dist/presentation/routes/report-runner-instances.js +1 -0
- package/dist/presentation/routes/report-runner-instances.js.map +1 -1
- package/dist/tsconfig.test.tsbuildinfo +1 -1
- package/drizzle/0004_huge_skreet.sql +1 -0
- package/drizzle/meta/0004_snapshot.json +2194 -0
- package/drizzle/meta/_journal.json +7 -0
- package/package.json +9 -9
- package/src/config.test.ts +81 -0
- package/src/config.ts +39 -2
- package/src/core/errors.ts +9 -0
- package/src/core/index.ts +1 -0
- package/src/core/runner-control-sessions.test.ts +404 -0
- package/src/core/runner-control-sessions.ts +51 -11
- package/src/core/runner-instances.ts +1 -0
- package/src/core/runner-labels.test.ts +53 -0
- package/src/core/runner-labels.ts +51 -0
- package/src/core/runner-sessions.test.ts +34 -1
- package/src/core/runner-sessions.ts +8 -1
- package/src/db/ephemeral-registration-tokens.ts +15 -1
- package/src/db/index.ts +1 -0
- package/src/db/reservations.test.ts +537 -0
- package/src/db/reservations.ts +116 -17
- package/src/db/runner-instances.test.ts +276 -1
- package/src/db/runner-instances.ts +39 -3
- package/src/db/runner-sessions.ts +39 -15
- package/src/db/schema/runner-instances.ts +3 -0
- package/src/metrics/index.ts +2 -0
- package/src/metrics/instance.ts +18 -0
- package/src/metrics/service.test.ts +99 -0
- package/src/metrics/service.ts +24 -4
- package/src/presentation/routes/late-runner-enrollment.test.ts +230 -0
- package/src/presentation/routes/poll-demand.test.ts +116 -3
- package/src/presentation/routes/poll-demand.ts +18 -6
- package/src/presentation/routes/register.test.ts +116 -2
- package/src/presentation/routes/register.ts +7 -0
- package/src/presentation/routes/report-runner-instances.test.ts +33 -0
- package/src/presentation/routes/report-runner-instances.ts +1 -0
- package/src/presentation/routes/runner-enrollment.test.ts +65 -5
- package/src/temporal/activities/maintenance-activities.test.ts +24 -31
- package/test/env.ts +1 -0
- package/test/fixtures/late-runner-enrollment.ts +103 -0
- package/test/index.ts +5 -0
- package/tsconfig.build.tsbuildinfo +1 -1
|
@@ -86,6 +86,350 @@ describe('pollDemandAndReserve', () => {
|
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
+
it('binds a runner whose intended reservation has passed its activation grace period', async () => {
|
|
90
|
+
const intendedReservation = await createIntendedReservation({
|
|
91
|
+
workspaceId,
|
|
92
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
93
|
+
});
|
|
94
|
+
const runner = await createIdleRunner({
|
|
95
|
+
labels: ['linux'],
|
|
96
|
+
intendedReservationId: intendedReservation.id,
|
|
97
|
+
});
|
|
98
|
+
await createPendingJobs(1, ['linux']);
|
|
99
|
+
|
|
100
|
+
const result = await pollDemandAndReserve({
|
|
101
|
+
workspaceId,
|
|
102
|
+
provisionerId,
|
|
103
|
+
maxReservations: 1,
|
|
104
|
+
ttlSeconds: 60,
|
|
105
|
+
templates: [template('linux', ['linux'], 1)],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const [storedRunner] = await db()
|
|
109
|
+
.select()
|
|
110
|
+
.from(providerRunners)
|
|
111
|
+
.where(eq(providerRunners.id, runner.id));
|
|
112
|
+
expect(storedRunner).toMatchObject({
|
|
113
|
+
workspaceId,
|
|
114
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
115
|
+
intendedReservationId: null,
|
|
116
|
+
assignedAt: expect.any(Date),
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('binds a runner whose intended reservation was deleted after its grace period', async () => {
|
|
121
|
+
const intendedReservation = await createIntendedReservation({
|
|
122
|
+
workspaceId,
|
|
123
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
124
|
+
});
|
|
125
|
+
await db().delete(reservations).where(eq(reservations.id, intendedReservation.id));
|
|
126
|
+
const runner = await createIdleRunner({
|
|
127
|
+
labels: ['linux'],
|
|
128
|
+
intendedReservationId: intendedReservation.id,
|
|
129
|
+
});
|
|
130
|
+
await createPendingJobs(1, ['linux']);
|
|
131
|
+
|
|
132
|
+
const result = await pollDemandAndReserve({
|
|
133
|
+
workspaceId,
|
|
134
|
+
provisionerId,
|
|
135
|
+
maxReservations: 1,
|
|
136
|
+
ttlSeconds: 60,
|
|
137
|
+
templates: [template('linux', ['linux'], 1)],
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const [storedRunner] = await db()
|
|
141
|
+
.select()
|
|
142
|
+
.from(providerRunners)
|
|
143
|
+
.where(eq(providerRunners.id, runner.id));
|
|
144
|
+
expect(storedRunner).toMatchObject({
|
|
145
|
+
workspaceId,
|
|
146
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
147
|
+
intendedReservationId: null,
|
|
148
|
+
assignedAt: expect.any(Date),
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('binds an assigned runner after its reservation grace period expires', async () => {
|
|
153
|
+
const staleReservation = await createIntendedReservation({
|
|
154
|
+
workspaceId,
|
|
155
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
156
|
+
});
|
|
157
|
+
const runner = await createIdleRunner({
|
|
158
|
+
labels: ['linux'],
|
|
159
|
+
workspaceId,
|
|
160
|
+
reservationId: staleReservation.id,
|
|
161
|
+
intendedReservationId: staleReservation.id,
|
|
162
|
+
});
|
|
163
|
+
const [activationToken] = await db()
|
|
164
|
+
.insert(runnerActivationTokens)
|
|
165
|
+
.values({
|
|
166
|
+
runnerInstanceId: runner.id,
|
|
167
|
+
hashedToken: crypto.randomUUID(),
|
|
168
|
+
prefix: 'test',
|
|
169
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
170
|
+
})
|
|
171
|
+
.returning();
|
|
172
|
+
if (!activationToken) throw new Error('Expected activation token');
|
|
173
|
+
await createPendingJobs(1, ['linux']);
|
|
174
|
+
|
|
175
|
+
const result = await pollDemandAndReserve({
|
|
176
|
+
workspaceId,
|
|
177
|
+
provisionerId,
|
|
178
|
+
maxReservations: 1,
|
|
179
|
+
ttlSeconds: 60,
|
|
180
|
+
templates: [template('linux', ['linux'], 1)],
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
const [storedRunner] = await db()
|
|
184
|
+
.select()
|
|
185
|
+
.from(providerRunners)
|
|
186
|
+
.where(eq(providerRunners.id, runner.id));
|
|
187
|
+
const [storedToken] = await db()
|
|
188
|
+
.select()
|
|
189
|
+
.from(runnerActivationTokens)
|
|
190
|
+
.where(eq(runnerActivationTokens.id, activationToken.id));
|
|
191
|
+
expect(storedRunner).toMatchObject({
|
|
192
|
+
workspaceId,
|
|
193
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
194
|
+
intendedReservationId: null,
|
|
195
|
+
assignedAt: expect.any(Date),
|
|
196
|
+
});
|
|
197
|
+
expect(storedToken?.revokedAt).toBeInstanceOf(Date);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('binds an assigned runner whose reservation was deleted', async () => {
|
|
201
|
+
const staleReservation = await createIntendedReservation({
|
|
202
|
+
workspaceId,
|
|
203
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
204
|
+
});
|
|
205
|
+
await db().delete(reservations).where(eq(reservations.id, staleReservation.id));
|
|
206
|
+
const runner = await createIdleRunner({
|
|
207
|
+
labels: ['linux'],
|
|
208
|
+
workspaceId,
|
|
209
|
+
reservationId: staleReservation.id,
|
|
210
|
+
intendedReservationId: staleReservation.id,
|
|
211
|
+
});
|
|
212
|
+
await createPendingJobs(1, ['linux']);
|
|
213
|
+
|
|
214
|
+
const result = await pollDemandAndReserve({
|
|
215
|
+
workspaceId,
|
|
216
|
+
provisionerId,
|
|
217
|
+
maxReservations: 1,
|
|
218
|
+
ttlSeconds: 60,
|
|
219
|
+
templates: [template('linux', ['linux'], 1)],
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const [storedRunner] = await db()
|
|
223
|
+
.select()
|
|
224
|
+
.from(providerRunners)
|
|
225
|
+
.where(eq(providerRunners.id, runner.id));
|
|
226
|
+
expect(storedRunner).toMatchObject({
|
|
227
|
+
workspaceId,
|
|
228
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
229
|
+
intendedReservationId: null,
|
|
230
|
+
assignedAt: expect.any(Date),
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('does not bind an assigned runner within its activation grace period', async () => {
|
|
235
|
+
const liveReservation = await createIntendedReservation({
|
|
236
|
+
workspaceId,
|
|
237
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
238
|
+
});
|
|
239
|
+
const runner = await createIdleRunner({
|
|
240
|
+
labels: ['linux'],
|
|
241
|
+
workspaceId,
|
|
242
|
+
reservationId: liveReservation.id,
|
|
243
|
+
});
|
|
244
|
+
await createPendingJobs(2, ['linux']);
|
|
245
|
+
|
|
246
|
+
const result = await pollDemandAndReserve({
|
|
247
|
+
workspaceId,
|
|
248
|
+
provisionerId,
|
|
249
|
+
maxReservations: 2,
|
|
250
|
+
ttlSeconds: 60,
|
|
251
|
+
templates: [template('linux', ['linux'], 2)],
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const [storedRunner] = await db()
|
|
255
|
+
.select()
|
|
256
|
+
.from(providerRunners)
|
|
257
|
+
.where(eq(providerRunners.id, runner.id));
|
|
258
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
259
|
+
expect(storedRunner).toMatchObject({
|
|
260
|
+
workspaceId,
|
|
261
|
+
reservationId: liveReservation.id,
|
|
262
|
+
intendedReservationId: null,
|
|
263
|
+
assignedAt: null,
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('does not bind a runner whose intended reservation is within its activation grace period', async () => {
|
|
268
|
+
const intendedReservation = await createIntendedReservation({
|
|
269
|
+
workspaceId: crypto.randomUUID(),
|
|
270
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
271
|
+
});
|
|
272
|
+
const runner = await createIdleRunner({
|
|
273
|
+
labels: ['linux'],
|
|
274
|
+
intendedReservationId: intendedReservation.id,
|
|
275
|
+
});
|
|
276
|
+
await createPendingJobs(1, ['linux']);
|
|
277
|
+
|
|
278
|
+
const result = await pollDemandAndReserve({
|
|
279
|
+
workspaceId,
|
|
280
|
+
provisionerId,
|
|
281
|
+
maxReservations: 1,
|
|
282
|
+
ttlSeconds: 60,
|
|
283
|
+
templates: [template('linux', ['linux'], 1)],
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
const [storedRunner] = await db()
|
|
287
|
+
.select()
|
|
288
|
+
.from(providerRunners)
|
|
289
|
+
.where(eq(providerRunners.id, runner.id));
|
|
290
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
291
|
+
expect(storedRunner).toMatchObject({
|
|
292
|
+
workspaceId: null,
|
|
293
|
+
reservationId: null,
|
|
294
|
+
intendedReservationId: intendedReservation.id,
|
|
295
|
+
assignedAt: null,
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('does not rebind a stale runner from another workspace in a workspace-scoped poll', async () => {
|
|
300
|
+
const previousWorkspaceId = crypto.randomUUID();
|
|
301
|
+
const staleReservation = await createIntendedReservation({
|
|
302
|
+
workspaceId: previousWorkspaceId,
|
|
303
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
304
|
+
});
|
|
305
|
+
const runner = await createIdleRunner({
|
|
306
|
+
labels: ['linux'],
|
|
307
|
+
workspaceId: previousWorkspaceId,
|
|
308
|
+
reservationId: staleReservation.id,
|
|
309
|
+
intendedReservationId: staleReservation.id,
|
|
310
|
+
});
|
|
311
|
+
await createPendingJobs(1, ['linux']);
|
|
312
|
+
|
|
313
|
+
const result = await pollDemandAndReserve({
|
|
314
|
+
workspaceId,
|
|
315
|
+
provisionerId,
|
|
316
|
+
maxReservations: 1,
|
|
317
|
+
ttlSeconds: 60,
|
|
318
|
+
templates: [template('linux', ['linux'], 1)],
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
const [storedRunner] = await db()
|
|
322
|
+
.select()
|
|
323
|
+
.from(providerRunners)
|
|
324
|
+
.where(eq(providerRunners.id, runner.id));
|
|
325
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
326
|
+
expect(storedRunner).toMatchObject({
|
|
327
|
+
workspaceId: previousWorkspaceId,
|
|
328
|
+
reservationId: staleReservation.id,
|
|
329
|
+
intendedReservationId: staleReservation.id,
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('rebinds an unowned runner whose reservation was deleted', async () => {
|
|
334
|
+
const runner = await createIdleRunner({
|
|
335
|
+
labels: ['linux'],
|
|
336
|
+
reservationId: crypto.randomUUID(),
|
|
337
|
+
});
|
|
338
|
+
await createPendingJobs(1, ['linux']);
|
|
339
|
+
|
|
340
|
+
const result = await pollDemandAndReserve({
|
|
341
|
+
workspaceId,
|
|
342
|
+
provisionerId,
|
|
343
|
+
maxReservations: 1,
|
|
344
|
+
ttlSeconds: 60,
|
|
345
|
+
templates: [template('linux', ['linux'], 1)],
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const [storedRunner] = await db()
|
|
349
|
+
.select()
|
|
350
|
+
.from(providerRunners)
|
|
351
|
+
.where(eq(providerRunners.id, runner.id));
|
|
352
|
+
expect(storedRunner).toMatchObject({
|
|
353
|
+
workspaceId,
|
|
354
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
355
|
+
intendedReservationId: null,
|
|
356
|
+
assignedAt: expect.any(Date),
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('rebinds a stale runner across workspaces for an installation-scoped poll', async () => {
|
|
361
|
+
const previousWorkspaceId = crypto.randomUUID();
|
|
362
|
+
const targetWorkspaceId = crypto.randomUUID();
|
|
363
|
+
const staleReservation = await createIntendedReservation({
|
|
364
|
+
workspaceId: previousWorkspaceId,
|
|
365
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
366
|
+
});
|
|
367
|
+
const runner = await createIdleRunner({
|
|
368
|
+
labels: ['linux'],
|
|
369
|
+
workspaceId: previousWorkspaceId,
|
|
370
|
+
reservationId: staleReservation.id,
|
|
371
|
+
intendedReservationId: staleReservation.id,
|
|
372
|
+
});
|
|
373
|
+
await pendingJobFactory.create({workspaceId: targetWorkspaceId, requiredLabels: ['linux']});
|
|
374
|
+
|
|
375
|
+
const result = await pollInstallationDemandAndReserve({
|
|
376
|
+
provisionerId,
|
|
377
|
+
maxReservations: 1,
|
|
378
|
+
ttlSeconds: 60,
|
|
379
|
+
templates: [template('linux', ['linux'], 1)],
|
|
380
|
+
capabilityWindowSeconds: 60,
|
|
381
|
+
eligibleWorkspaceIds: new Set([targetWorkspaceId]),
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
const [storedRunner] = await db()
|
|
385
|
+
.select()
|
|
386
|
+
.from(providerRunners)
|
|
387
|
+
.where(eq(providerRunners.id, runner.id));
|
|
388
|
+
expect(result.reservations[0]).toMatchObject({workspaceId: targetWorkspaceId, count: 1});
|
|
389
|
+
expect(storedRunner).toMatchObject({
|
|
390
|
+
workspaceId: targetWorkspaceId,
|
|
391
|
+
reservationId: result.reservations[0]?.reservationId,
|
|
392
|
+
intendedReservationId: null,
|
|
393
|
+
assignedAt: expect.any(Date),
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('does not rebind a released runner after its reservation expires', async () => {
|
|
398
|
+
const staleReservation = await createIntendedReservation({
|
|
399
|
+
workspaceId,
|
|
400
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
401
|
+
});
|
|
402
|
+
const releasedAt = new Date(Date.now() - 30_000);
|
|
403
|
+
const runner = await createIdleRunner({
|
|
404
|
+
labels: ['linux'],
|
|
405
|
+
workspaceId,
|
|
406
|
+
reservationId: staleReservation.id,
|
|
407
|
+
intendedReservationId: staleReservation.id,
|
|
408
|
+
reservationReleasedAt: releasedAt,
|
|
409
|
+
});
|
|
410
|
+
await createPendingJobs(1, ['linux']);
|
|
411
|
+
|
|
412
|
+
const result = await pollDemandAndReserve({
|
|
413
|
+
workspaceId,
|
|
414
|
+
provisionerId,
|
|
415
|
+
maxReservations: 1,
|
|
416
|
+
ttlSeconds: 60,
|
|
417
|
+
templates: [template('linux', ['linux'], 1)],
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const [storedRunner] = await db()
|
|
421
|
+
.select()
|
|
422
|
+
.from(providerRunners)
|
|
423
|
+
.where(eq(providerRunners.id, runner.id));
|
|
424
|
+
expect(result.reservations[0]?.count).toBe(1);
|
|
425
|
+
expect(storedRunner).toMatchObject({
|
|
426
|
+
workspaceId,
|
|
427
|
+
reservationId: staleReservation.id,
|
|
428
|
+
intendedReservationId: staleReservation.id,
|
|
429
|
+
reservationReleasedAt: releasedAt,
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
89
433
|
it('does not bind a running runner without an active control session', async () => {
|
|
90
434
|
const [runner] = await db()
|
|
91
435
|
.insert(providerRunners)
|
|
@@ -461,6 +805,64 @@ describe('pollDemandAndReserve', () => {
|
|
|
461
805
|
expect(storedToken?.revokedAt).toBeInstanceOf(Date);
|
|
462
806
|
});
|
|
463
807
|
|
|
808
|
+
it('preserves activation tokens for active runners with an intended reservation', async () => {
|
|
809
|
+
const [reservation] = await db()
|
|
810
|
+
.insert(reservations)
|
|
811
|
+
.values({
|
|
812
|
+
workspaceId,
|
|
813
|
+
provisionerId,
|
|
814
|
+
requiredLabels: ['linux'],
|
|
815
|
+
count: 1,
|
|
816
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
817
|
+
})
|
|
818
|
+
.returning();
|
|
819
|
+
if (!reservation) throw new Error('Expected reservation');
|
|
820
|
+
const [runner] = await db()
|
|
821
|
+
.insert(providerRunners)
|
|
822
|
+
.values({
|
|
823
|
+
workspaceId,
|
|
824
|
+
provisionerId,
|
|
825
|
+
providerRunnerId: crypto.randomUUID(),
|
|
826
|
+
reservationId: reservation.id,
|
|
827
|
+
intendedReservationId: reservation.id,
|
|
828
|
+
runnerSessionId: crypto.randomUUID(),
|
|
829
|
+
labels: ['linux'],
|
|
830
|
+
state: 'running',
|
|
831
|
+
reportedAt: new Date(),
|
|
832
|
+
})
|
|
833
|
+
.returning();
|
|
834
|
+
if (!runner) throw new Error('Expected runner instance');
|
|
835
|
+
const [activationToken] = await db()
|
|
836
|
+
.insert(runnerActivationTokens)
|
|
837
|
+
.values({
|
|
838
|
+
runnerInstanceId: runner.id,
|
|
839
|
+
hashedToken: crypto.randomUUID(),
|
|
840
|
+
prefix: 'test',
|
|
841
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
842
|
+
})
|
|
843
|
+
.returning();
|
|
844
|
+
if (!activationToken) throw new Error('Expected activation token');
|
|
845
|
+
|
|
846
|
+
const deleted = await deleteReservationsByIds([reservation.id]);
|
|
847
|
+
|
|
848
|
+
const [storedRunner] = await db()
|
|
849
|
+
.select()
|
|
850
|
+
.from(providerRunners)
|
|
851
|
+
.where(eq(providerRunners.id, runner.id));
|
|
852
|
+
const [storedToken] = await db()
|
|
853
|
+
.select()
|
|
854
|
+
.from(runnerActivationTokens)
|
|
855
|
+
.where(eq(runnerActivationTokens.id, activationToken.id));
|
|
856
|
+
expect(deleted).toBe(1);
|
|
857
|
+
expect(storedRunner).toMatchObject({
|
|
858
|
+
reservationId: reservation.id,
|
|
859
|
+
intendedReservationId: null,
|
|
860
|
+
runnerSessionId: expect.any(String),
|
|
861
|
+
reservationReleasedAt: null,
|
|
862
|
+
});
|
|
863
|
+
expect(storedToken?.revokedAt).toBeNull();
|
|
864
|
+
});
|
|
865
|
+
|
|
464
866
|
it('unbinds prewarmed runners and revokes activation tokens when deleting expired reservations', async () => {
|
|
465
867
|
const runner = await createIdleRunner({labels: ['linux']});
|
|
466
868
|
await createPendingJobs(1, ['linux']);
|
|
@@ -509,6 +911,115 @@ describe('pollDemandAndReserve', () => {
|
|
|
509
911
|
expect(storedToken?.revokedAt).toBeInstanceOf(Date);
|
|
510
912
|
});
|
|
511
913
|
|
|
914
|
+
it('clears intended reservations and rebinds runners when deleting a reservation by id', async () => {
|
|
915
|
+
const [reservation, survivingReservation] = await db()
|
|
916
|
+
.insert(reservations)
|
|
917
|
+
.values([
|
|
918
|
+
{
|
|
919
|
+
workspaceId,
|
|
920
|
+
provisionerId,
|
|
921
|
+
requiredLabels: ['linux'],
|
|
922
|
+
count: 1,
|
|
923
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
924
|
+
},
|
|
925
|
+
{
|
|
926
|
+
workspaceId: crypto.randomUUID(),
|
|
927
|
+
provisionerId,
|
|
928
|
+
requiredLabels: ['linux'],
|
|
929
|
+
count: 1,
|
|
930
|
+
expiresAt: new Date(Date.now() + 60_000),
|
|
931
|
+
},
|
|
932
|
+
])
|
|
933
|
+
.returning();
|
|
934
|
+
if (!reservation || !survivingReservation) throw new Error('Expected reservations');
|
|
935
|
+
const runner = await createIdleRunner({
|
|
936
|
+
labels: ['linux'],
|
|
937
|
+
intendedReservationId: reservation.id,
|
|
938
|
+
});
|
|
939
|
+
const survivingRunner = await createIdleRunner({
|
|
940
|
+
labels: ['linux'],
|
|
941
|
+
intendedReservationId: survivingReservation.id,
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
const deleted = await deleteReservationsByIds([reservation.id]);
|
|
945
|
+
|
|
946
|
+
const [storedRunner] = await db()
|
|
947
|
+
.select()
|
|
948
|
+
.from(providerRunners)
|
|
949
|
+
.where(eq(providerRunners.id, runner.id));
|
|
950
|
+
expect(deleted).toBe(1);
|
|
951
|
+
expect(storedRunner).toMatchObject({
|
|
952
|
+
intendedReservationId: null,
|
|
953
|
+
workspaceId: null,
|
|
954
|
+
reservationId: null,
|
|
955
|
+
assignedAt: null,
|
|
956
|
+
state: 'running',
|
|
957
|
+
reservationReleasedAt: null,
|
|
958
|
+
});
|
|
959
|
+
const [storedSurvivingRunner] = await db()
|
|
960
|
+
.select()
|
|
961
|
+
.from(providerRunners)
|
|
962
|
+
.where(eq(providerRunners.id, survivingRunner.id));
|
|
963
|
+
expect(storedSurvivingRunner?.intendedReservationId).toBe(survivingReservation.id);
|
|
964
|
+
|
|
965
|
+
await createPendingJobs(1, ['linux']);
|
|
966
|
+
const result = await pollDemandAndReserve({
|
|
967
|
+
workspaceId,
|
|
968
|
+
provisionerId,
|
|
969
|
+
maxReservations: 1,
|
|
970
|
+
ttlSeconds: 60,
|
|
971
|
+
templates: [template('linux', ['linux'], 1)],
|
|
972
|
+
});
|
|
973
|
+
const reboundReservationId = result.reservations[0]?.reservationId;
|
|
974
|
+
const [reboundRunner] = await db()
|
|
975
|
+
.select()
|
|
976
|
+
.from(providerRunners)
|
|
977
|
+
.where(eq(providerRunners.id, runner.id));
|
|
978
|
+
expect(reboundReservationId).toEqual(expect.any(String));
|
|
979
|
+
expect(reboundRunner).toMatchObject({
|
|
980
|
+
intendedReservationId: null,
|
|
981
|
+
workspaceId,
|
|
982
|
+
reservationId: reboundReservationId,
|
|
983
|
+
assignedAt: expect.any(Date),
|
|
984
|
+
state: 'running',
|
|
985
|
+
reservationReleasedAt: null,
|
|
986
|
+
});
|
|
987
|
+
});
|
|
988
|
+
|
|
989
|
+
it('clears intended reservations when deleting expired reservations', async () => {
|
|
990
|
+
const [reservation] = await db()
|
|
991
|
+
.insert(reservations)
|
|
992
|
+
.values({
|
|
993
|
+
workspaceId,
|
|
994
|
+
provisionerId,
|
|
995
|
+
requiredLabels: ['linux'],
|
|
996
|
+
count: 1,
|
|
997
|
+
expiresAt: new Date(Date.now() - 60_000),
|
|
998
|
+
})
|
|
999
|
+
.returning();
|
|
1000
|
+
if (!reservation) throw new Error('Expected reservation');
|
|
1001
|
+
const runner = await createIdleRunner({
|
|
1002
|
+
labels: ['linux'],
|
|
1003
|
+
intendedReservationId: reservation.id,
|
|
1004
|
+
});
|
|
1005
|
+
|
|
1006
|
+
const deleted = await deleteExpiredReservations();
|
|
1007
|
+
|
|
1008
|
+
const [storedRunner] = await db()
|
|
1009
|
+
.select()
|
|
1010
|
+
.from(providerRunners)
|
|
1011
|
+
.where(eq(providerRunners.id, runner.id));
|
|
1012
|
+
expect(deleted).toBeGreaterThanOrEqual(1);
|
|
1013
|
+
expect(storedRunner).toMatchObject({
|
|
1014
|
+
intendedReservationId: null,
|
|
1015
|
+
workspaceId: null,
|
|
1016
|
+
reservationId: null,
|
|
1017
|
+
assignedAt: null,
|
|
1018
|
+
state: 'running',
|
|
1019
|
+
reservationReleasedAt: null,
|
|
1020
|
+
});
|
|
1021
|
+
});
|
|
1022
|
+
|
|
512
1023
|
it('decrements reservation units inside a caller transaction', async () => {
|
|
513
1024
|
await reservationFactory.create({
|
|
514
1025
|
workspaceId,
|
|
@@ -641,17 +1152,43 @@ describe('pollDemandAndReserve', () => {
|
|
|
641
1152
|
}
|
|
642
1153
|
}
|
|
643
1154
|
|
|
1155
|
+
async function createIntendedReservation(params: {
|
|
1156
|
+
workspaceId: string;
|
|
1157
|
+
expiresAt: Date;
|
|
1158
|
+
}): Promise<{id: string}> {
|
|
1159
|
+
const [reservation] = await db()
|
|
1160
|
+
.insert(reservations)
|
|
1161
|
+
.values({
|
|
1162
|
+
workspaceId: params.workspaceId,
|
|
1163
|
+
provisionerId,
|
|
1164
|
+
requiredLabels: ['linux'],
|
|
1165
|
+
count: 1,
|
|
1166
|
+
expiresAt: params.expiresAt,
|
|
1167
|
+
})
|
|
1168
|
+
.returning({id: reservations.id});
|
|
1169
|
+
if (!reservation) throw new Error('Expected reservation');
|
|
1170
|
+
return reservation;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
644
1173
|
async function createIdleRunner(params: {
|
|
645
1174
|
labels: string[];
|
|
646
1175
|
createdAt?: Date;
|
|
647
1176
|
controlSessionExpiresAt?: Date;
|
|
1177
|
+
workspaceId?: string | null;
|
|
1178
|
+
reservationId?: string | null;
|
|
1179
|
+
intendedReservationId?: string | null;
|
|
1180
|
+
reservationReleasedAt?: Date | null;
|
|
648
1181
|
}) {
|
|
649
1182
|
const createdAt = params.createdAt ?? new Date();
|
|
650
1183
|
const [runner] = await db()
|
|
651
1184
|
.insert(providerRunners)
|
|
652
1185
|
.values({
|
|
653
1186
|
provisionerId,
|
|
1187
|
+
workspaceId: params.workspaceId,
|
|
1188
|
+
reservationId: params.reservationId,
|
|
654
1189
|
providerRunnerId: crypto.randomUUID(),
|
|
1190
|
+
intendedReservationId: params.intendedReservationId,
|
|
1191
|
+
reservationReleasedAt: params.reservationReleasedAt,
|
|
655
1192
|
labels: params.labels,
|
|
656
1193
|
state: 'running',
|
|
657
1194
|
reportedAt: createdAt,
|