@convex-dev/workpool 0.2.0-alpha.0 → 0.2.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/README.md +1 -1
- package/dist/commonjs/client/index.d.ts +3 -3
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +10 -5
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/commonjs/component/complete.js +1 -1
- package/dist/commonjs/component/complete.js.map +1 -1
- package/dist/commonjs/component/kick.d.ts +0 -1
- package/dist/commonjs/component/kick.d.ts.map +1 -1
- package/dist/commonjs/component/kick.js +6 -4
- package/dist/commonjs/component/kick.js.map +1 -1
- package/dist/commonjs/component/lib.d.ts.map +1 -1
- package/dist/commonjs/component/lib.js +4 -3
- package/dist/commonjs/component/lib.js.map +1 -1
- package/dist/commonjs/component/logging.d.ts.map +1 -1
- package/dist/commonjs/component/logging.js +1 -2
- package/dist/commonjs/component/logging.js.map +1 -1
- package/dist/commonjs/component/loop.d.ts.map +1 -1
- package/dist/commonjs/component/loop.js +46 -38
- package/dist/commonjs/component/loop.js.map +1 -1
- package/dist/commonjs/component/shared.d.ts +1 -0
- package/dist/commonjs/component/shared.d.ts.map +1 -1
- package/dist/commonjs/component/shared.js +1 -0
- package/dist/commonjs/component/shared.js.map +1 -1
- package/dist/commonjs/component/stats.d.ts +19 -13
- package/dist/commonjs/component/stats.d.ts.map +1 -1
- package/dist/commonjs/component/stats.js +26 -21
- package/dist/commonjs/component/stats.js.map +1 -1
- package/dist/esm/client/index.d.ts +3 -3
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +10 -5
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/component/complete.js +1 -1
- package/dist/esm/component/complete.js.map +1 -1
- package/dist/esm/component/kick.d.ts +0 -1
- package/dist/esm/component/kick.d.ts.map +1 -1
- package/dist/esm/component/kick.js +6 -4
- package/dist/esm/component/kick.js.map +1 -1
- package/dist/esm/component/lib.d.ts.map +1 -1
- package/dist/esm/component/lib.js +4 -3
- package/dist/esm/component/lib.js.map +1 -1
- package/dist/esm/component/logging.d.ts.map +1 -1
- package/dist/esm/component/logging.js +1 -2
- package/dist/esm/component/logging.js.map +1 -1
- package/dist/esm/component/loop.d.ts.map +1 -1
- package/dist/esm/component/loop.js +46 -38
- package/dist/esm/component/loop.js.map +1 -1
- package/dist/esm/component/shared.d.ts +1 -0
- package/dist/esm/component/shared.d.ts.map +1 -1
- package/dist/esm/component/shared.js +1 -0
- package/dist/esm/component/shared.js.map +1 -1
- package/dist/esm/component/stats.d.ts +19 -13
- package/dist/esm/component/stats.d.ts.map +1 -1
- package/dist/esm/component/stats.js +26 -21
- package/dist/esm/component/stats.js.map +1 -1
- package/package.json +7 -6
- package/src/client/index.ts +18 -8
- package/src/component/_generated/api.d.ts +3 -0
- package/src/component/complete.test.ts +10 -10
- package/src/component/complete.ts +1 -1
- package/src/component/kick.test.ts +10 -9
- package/src/component/kick.ts +11 -6
- package/src/component/lib.test.ts +20 -20
- package/src/component/lib.ts +4 -2
- package/src/component/logging.ts +1 -2
- package/src/component/loop.test.ts +101 -147
- package/src/component/loop.ts +49 -39
- package/src/component/recovery.test.ts +7 -7
- package/src/component/shared.ts +1 -0
- package/src/component/stats.ts +41 -22
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
import { convexTest } from "convex-test";
|
|
2
|
+
import { WithoutSystemFields } from "convex/server";
|
|
2
3
|
import {
|
|
4
|
+
afterEach,
|
|
5
|
+
assert,
|
|
6
|
+
beforeEach,
|
|
3
7
|
describe,
|
|
4
8
|
expect,
|
|
5
9
|
it,
|
|
6
|
-
beforeEach,
|
|
7
|
-
afterEach,
|
|
8
10
|
vi,
|
|
9
|
-
assert,
|
|
10
11
|
} from "vitest";
|
|
12
|
+
import { api, internal } from "./_generated/api";
|
|
11
13
|
import { Doc, Id } from "./_generated/dataModel";
|
|
12
|
-
import schema from "./schema";
|
|
13
|
-
import { internal } from "./_generated/api";
|
|
14
|
-
import { currentSegment, nextSegment, toSegment } from "./shared";
|
|
15
|
-
import { api } from "./_generated/api";
|
|
16
|
-
import { DEFAULT_MAX_PARALLELISM } from "./kick";
|
|
17
|
-
import { WithoutSystemFields } from "convex/server";
|
|
18
14
|
import { MutationCtx } from "./_generated/server";
|
|
15
|
+
import schema from "./schema";
|
|
16
|
+
import {
|
|
17
|
+
currentSegment,
|
|
18
|
+
DEFAULT_MAX_PARALLELISM,
|
|
19
|
+
nextSegment,
|
|
20
|
+
toSegment,
|
|
21
|
+
} from "./shared";
|
|
19
22
|
|
|
20
23
|
const modules = import.meta.glob("./**/*.ts");
|
|
21
24
|
|
|
@@ -57,17 +60,38 @@ describe("loop", () => {
|
|
|
57
60
|
workId,
|
|
58
61
|
fnHandle: "test_handle",
|
|
59
62
|
fnArgs: {},
|
|
60
|
-
logLevel: "
|
|
63
|
+
logLevel: "WARN",
|
|
61
64
|
attempt: 0,
|
|
62
65
|
});
|
|
63
66
|
}
|
|
64
67
|
|
|
68
|
+
async function insertInternalState(
|
|
69
|
+
ctx: MutationCtx,
|
|
70
|
+
overrides: Partial<WithoutSystemFields<Doc<"internalState">>> = {}
|
|
71
|
+
) {
|
|
72
|
+
await ctx.db.insert("internalState", {
|
|
73
|
+
generation: 1n,
|
|
74
|
+
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
75
|
+
lastRecovery: currentSegment(),
|
|
76
|
+
report: {
|
|
77
|
+
completed: 0,
|
|
78
|
+
succeeded: 0,
|
|
79
|
+
failed: 0,
|
|
80
|
+
retries: 0,
|
|
81
|
+
canceled: 0,
|
|
82
|
+
lastReportTs: Date.now(),
|
|
83
|
+
},
|
|
84
|
+
running: [],
|
|
85
|
+
...overrides,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
65
89
|
beforeEach(async () => {
|
|
66
90
|
vi.useFakeTimers();
|
|
67
91
|
t = await setupTest();
|
|
68
92
|
await t.run(async (ctx) => {
|
|
69
93
|
await ctx.db.insert("globals", {
|
|
70
|
-
logLevel: "
|
|
94
|
+
logLevel: "WARN",
|
|
71
95
|
maxParallelism: DEFAULT_MAX_PARALLELISM,
|
|
72
96
|
});
|
|
73
97
|
});
|
|
@@ -82,20 +106,7 @@ describe("loop", () => {
|
|
|
82
106
|
// Setup initial state
|
|
83
107
|
const workId = await t.run<Id<"work">>(async (ctx) => {
|
|
84
108
|
// Create internal state
|
|
85
|
-
await ctx
|
|
86
|
-
generation: 1n,
|
|
87
|
-
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
88
|
-
lastRecovery: 0n,
|
|
89
|
-
report: {
|
|
90
|
-
completed: 0,
|
|
91
|
-
succeeded: 0,
|
|
92
|
-
failed: 0,
|
|
93
|
-
retries: 0,
|
|
94
|
-
canceled: 0,
|
|
95
|
-
lastReportTs: Date.now(),
|
|
96
|
-
},
|
|
97
|
-
running: [],
|
|
98
|
-
});
|
|
109
|
+
await insertInternalState(ctx);
|
|
99
110
|
|
|
100
111
|
// Create running runStatus
|
|
101
112
|
await ctx.db.insert("runStatus", {
|
|
@@ -158,20 +169,7 @@ describe("loop", () => {
|
|
|
158
169
|
// Setup initial state
|
|
159
170
|
const workId = await t.run<Id<"work">>(async (ctx) => {
|
|
160
171
|
// Create internal state
|
|
161
|
-
await ctx
|
|
162
|
-
generation: 1n,
|
|
163
|
-
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
164
|
-
lastRecovery: 0n,
|
|
165
|
-
report: {
|
|
166
|
-
completed: 0,
|
|
167
|
-
succeeded: 0,
|
|
168
|
-
failed: 0,
|
|
169
|
-
retries: 0,
|
|
170
|
-
canceled: 0,
|
|
171
|
-
lastReportTs: Date.now(),
|
|
172
|
-
},
|
|
173
|
-
running: [],
|
|
174
|
-
});
|
|
172
|
+
await insertInternalState(ctx);
|
|
175
173
|
|
|
176
174
|
// Create running runStatus
|
|
177
175
|
await ctx.db.insert("runStatus", {
|
|
@@ -228,20 +226,7 @@ describe("loop", () => {
|
|
|
228
226
|
// Setup initial state with a running job that will need retry
|
|
229
227
|
const workId = await t.run<Id<"work">>(async (ctx) => {
|
|
230
228
|
// Create internal state
|
|
231
|
-
await ctx
|
|
232
|
-
generation: 1n,
|
|
233
|
-
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
234
|
-
lastRecovery: 0n,
|
|
235
|
-
report: {
|
|
236
|
-
completed: 0,
|
|
237
|
-
succeeded: 0,
|
|
238
|
-
failed: 0,
|
|
239
|
-
retries: 0,
|
|
240
|
-
canceled: 0,
|
|
241
|
-
lastReportTs: Date.now(),
|
|
242
|
-
},
|
|
243
|
-
running: [],
|
|
244
|
-
});
|
|
229
|
+
await insertInternalState(ctx);
|
|
245
230
|
|
|
246
231
|
// Create running runStatus
|
|
247
232
|
await ctx.db.insert("runStatus", {
|
|
@@ -325,20 +310,7 @@ describe("loop", () => {
|
|
|
325
310
|
// Setup initial idle state
|
|
326
311
|
await t.run(async (ctx) => {
|
|
327
312
|
// Create internal state
|
|
328
|
-
await ctx
|
|
329
|
-
generation: 1n,
|
|
330
|
-
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
331
|
-
lastRecovery: 0n,
|
|
332
|
-
report: {
|
|
333
|
-
completed: 0,
|
|
334
|
-
succeeded: 0,
|
|
335
|
-
failed: 0,
|
|
336
|
-
retries: 0,
|
|
337
|
-
canceled: 0,
|
|
338
|
-
lastReportTs: Date.now(),
|
|
339
|
-
},
|
|
340
|
-
running: [],
|
|
341
|
-
});
|
|
313
|
+
await insertInternalState(ctx);
|
|
342
314
|
|
|
343
315
|
// Create idle runStatus
|
|
344
316
|
await ctx.db.insert("runStatus", {
|
|
@@ -372,20 +344,7 @@ describe("loop", () => {
|
|
|
372
344
|
// Setup initial running state with work
|
|
373
345
|
await t.run(async (ctx) => {
|
|
374
346
|
// Create internal state
|
|
375
|
-
await ctx
|
|
376
|
-
generation: 1n,
|
|
377
|
-
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
378
|
-
lastRecovery: 0n,
|
|
379
|
-
report: {
|
|
380
|
-
completed: 0,
|
|
381
|
-
succeeded: 0,
|
|
382
|
-
failed: 0,
|
|
383
|
-
retries: 0,
|
|
384
|
-
canceled: 0,
|
|
385
|
-
lastReportTs: Date.now(),
|
|
386
|
-
},
|
|
387
|
-
running: [],
|
|
388
|
-
});
|
|
347
|
+
await insertInternalState(ctx);
|
|
389
348
|
|
|
390
349
|
// Create running runStatus
|
|
391
350
|
await ctx.db.insert("runStatus", {
|
|
@@ -428,6 +387,7 @@ describe("loop", () => {
|
|
|
428
387
|
it("should transition from running to saturated when maxed out", async () => {
|
|
429
388
|
// Setup initial running state with max capacity
|
|
430
389
|
await setMaxParallelism(1);
|
|
390
|
+
const segment = currentSegment();
|
|
431
391
|
await t.run(async (ctx) => {
|
|
432
392
|
// Create work item
|
|
433
393
|
const workId = await makeDummyWork(ctx);
|
|
@@ -436,18 +396,7 @@ describe("loop", () => {
|
|
|
436
396
|
const scheduledId = await makeDummyScheduledFunction(ctx, workId);
|
|
437
397
|
|
|
438
398
|
// Create internal state with running job
|
|
439
|
-
await ctx
|
|
440
|
-
generation: 1n,
|
|
441
|
-
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
442
|
-
lastRecovery: 0n,
|
|
443
|
-
report: {
|
|
444
|
-
completed: 0,
|
|
445
|
-
succeeded: 0,
|
|
446
|
-
failed: 0,
|
|
447
|
-
retries: 0,
|
|
448
|
-
canceled: 0,
|
|
449
|
-
lastReportTs: Date.now(),
|
|
450
|
-
},
|
|
399
|
+
await insertInternalState(ctx, {
|
|
451
400
|
running: [{ workId, scheduledId, started: Date.now() }],
|
|
452
401
|
});
|
|
453
402
|
|
|
@@ -461,14 +410,14 @@ describe("loop", () => {
|
|
|
461
410
|
|
|
462
411
|
await ctx.db.insert("pendingStart", {
|
|
463
412
|
workId: anotherWorkId,
|
|
464
|
-
segment
|
|
413
|
+
segment,
|
|
465
414
|
});
|
|
466
415
|
});
|
|
467
416
|
|
|
468
417
|
// Run updateRunStatus to transition to scheduled with saturated=true
|
|
469
418
|
await t.mutation(internal.loop.updateRunStatus, {
|
|
470
419
|
generation: 1n,
|
|
471
|
-
segment
|
|
420
|
+
segment,
|
|
472
421
|
});
|
|
473
422
|
|
|
474
423
|
// Verify state transition to scheduled with saturated=true
|
|
@@ -484,45 +433,30 @@ describe("loop", () => {
|
|
|
484
433
|
|
|
485
434
|
it("should transition from scheduled to running when new work is enqueued", async () => {
|
|
486
435
|
// Setup initial scheduled state
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
436
|
+
await t.run<Id<"_scheduled_functions">>(async (ctx) => {
|
|
437
|
+
// Create internal state
|
|
438
|
+
await insertInternalState(ctx);
|
|
439
|
+
|
|
440
|
+
// Schedule main loop
|
|
441
|
+
const scheduledId = await ctx.scheduler.runAfter(
|
|
442
|
+
1000,
|
|
443
|
+
internal.loop.main,
|
|
444
|
+
{ generation: 1n, segment: nextSegment() + 10n }
|
|
445
|
+
);
|
|
446
|
+
|
|
447
|
+
// Create scheduled runStatus
|
|
448
|
+
await ctx.db.insert("runStatus", {
|
|
449
|
+
state: {
|
|
450
|
+
kind: "scheduled",
|
|
451
|
+
segment: nextSegment() + 10n,
|
|
452
|
+
scheduledId,
|
|
453
|
+
saturated: false,
|
|
491
454
|
generation: 1n,
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
report: {
|
|
495
|
-
completed: 0,
|
|
496
|
-
succeeded: 0,
|
|
497
|
-
failed: 0,
|
|
498
|
-
retries: 0,
|
|
499
|
-
canceled: 0,
|
|
500
|
-
lastReportTs: Date.now(),
|
|
501
|
-
},
|
|
502
|
-
running: [],
|
|
503
|
-
});
|
|
504
|
-
|
|
505
|
-
// Schedule main loop
|
|
506
|
-
const scheduledId = await ctx.scheduler.runAfter(
|
|
507
|
-
1000,
|
|
508
|
-
internal.loop.main,
|
|
509
|
-
{ generation: 1n, segment: nextSegment() + 10n }
|
|
510
|
-
);
|
|
511
|
-
|
|
512
|
-
// Create scheduled runStatus
|
|
513
|
-
await ctx.db.insert("runStatus", {
|
|
514
|
-
state: {
|
|
515
|
-
kind: "scheduled",
|
|
516
|
-
segment: nextSegment() + 10n,
|
|
517
|
-
scheduledId,
|
|
518
|
-
saturated: false,
|
|
519
|
-
generation: 1n,
|
|
520
|
-
},
|
|
521
|
-
});
|
|
455
|
+
},
|
|
456
|
+
});
|
|
522
457
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
);
|
|
458
|
+
return scheduledId;
|
|
459
|
+
});
|
|
526
460
|
|
|
527
461
|
// Enqueue work to trigger transition to running
|
|
528
462
|
await t.mutation(api.lib.enqueue, {
|
|
@@ -551,20 +485,7 @@ describe("loop", () => {
|
|
|
551
485
|
// Setup initial running state with work
|
|
552
486
|
const workId = await t.run<Id<"work">>(async (ctx) => {
|
|
553
487
|
// Create internal state
|
|
554
|
-
await ctx
|
|
555
|
-
generation: 1n,
|
|
556
|
-
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
557
|
-
lastRecovery: 0n,
|
|
558
|
-
report: {
|
|
559
|
-
completed: 0,
|
|
560
|
-
succeeded: 0,
|
|
561
|
-
failed: 0,
|
|
562
|
-
retries: 0,
|
|
563
|
-
canceled: 0,
|
|
564
|
-
lastReportTs: Date.now(),
|
|
565
|
-
},
|
|
566
|
-
running: [],
|
|
567
|
-
});
|
|
488
|
+
await insertInternalState(ctx);
|
|
568
489
|
|
|
569
490
|
// Create running runStatus
|
|
570
491
|
await ctx.db.insert("runStatus", {
|
|
@@ -615,6 +536,38 @@ describe("loop", () => {
|
|
|
615
536
|
assert(runStatus.state.kind === "idle");
|
|
616
537
|
});
|
|
617
538
|
});
|
|
539
|
+
it("should transition from scheduled to running when main loop runs", async () => {
|
|
540
|
+
const segment = nextSegment();
|
|
541
|
+
await t.run(async (ctx) => {
|
|
542
|
+
await insertInternalState(ctx);
|
|
543
|
+
|
|
544
|
+
const scheduledId = await ctx.scheduler.runAfter(
|
|
545
|
+
1000,
|
|
546
|
+
internal.loop.main,
|
|
547
|
+
{ generation: 1n, segment }
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
await ctx.db.insert("runStatus", {
|
|
551
|
+
state: {
|
|
552
|
+
kind: "scheduled",
|
|
553
|
+
scheduledId,
|
|
554
|
+
generation: 1n,
|
|
555
|
+
segment,
|
|
556
|
+
saturated: false,
|
|
557
|
+
},
|
|
558
|
+
});
|
|
559
|
+
});
|
|
560
|
+
// Run main loop
|
|
561
|
+
await t.mutation(internal.loop.main, { generation: 1n, segment });
|
|
562
|
+
|
|
563
|
+
// Verify state transition to running
|
|
564
|
+
await t.run(async (ctx) => {
|
|
565
|
+
const runStatus = await ctx.db.query("runStatus").unique();
|
|
566
|
+
expect(runStatus).toBeDefined();
|
|
567
|
+
assert(runStatus);
|
|
568
|
+
expect(runStatus.state.kind).toBe("running");
|
|
569
|
+
});
|
|
570
|
+
});
|
|
618
571
|
});
|
|
619
572
|
|
|
620
573
|
describe("main function", () => {
|
|
@@ -1106,7 +1059,7 @@ describe("loop", () => {
|
|
|
1106
1059
|
await ctx.db.insert("internalState", {
|
|
1107
1060
|
generation: 1n,
|
|
1108
1061
|
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
1109
|
-
lastRecovery:
|
|
1062
|
+
lastRecovery: now,
|
|
1110
1063
|
report: {
|
|
1111
1064
|
completed: 0,
|
|
1112
1065
|
succeeded: 0,
|
|
@@ -1156,6 +1109,7 @@ describe("loop", () => {
|
|
|
1156
1109
|
const workId = await makeDummyWork(ctx, {
|
|
1157
1110
|
attempts: 0,
|
|
1158
1111
|
onComplete: {
|
|
1112
|
+
// TODO: make this a real handle
|
|
1159
1113
|
fnHandle: "onComplete_handle",
|
|
1160
1114
|
context: { data: "test" },
|
|
1161
1115
|
},
|
package/src/component/loop.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { v } from "convex/values";
|
|
|
3
3
|
import { internal } from "./_generated/api.js";
|
|
4
4
|
import { Doc, Id } from "./_generated/dataModel.js";
|
|
5
5
|
import { internalMutation, MutationCtx } from "./_generated/server.js";
|
|
6
|
-
import {
|
|
6
|
+
import type { CompleteJob } from "./complete.js";
|
|
7
7
|
import {
|
|
8
8
|
createLogger,
|
|
9
9
|
DEFAULT_LOG_LEVEL,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
boundScheduledTime,
|
|
15
15
|
Config,
|
|
16
16
|
currentSegment,
|
|
17
|
+
DEFAULT_MAX_PARALLELISM,
|
|
17
18
|
fromSegment,
|
|
18
19
|
max,
|
|
19
20
|
nextSegment,
|
|
@@ -21,7 +22,6 @@ import {
|
|
|
21
22
|
toSegment,
|
|
22
23
|
} from "./shared.js";
|
|
23
24
|
import { recordCompleted, recordReport, recordStarted } from "./stats.js";
|
|
24
|
-
import type { CompleteJob } from "./complete.js";
|
|
25
25
|
|
|
26
26
|
const CANCELLATION_BATCH_SIZE = 64; // the only queue that can get unbounded.
|
|
27
27
|
const SECOND = 1000;
|
|
@@ -47,41 +47,49 @@ export const INITIAL_STATE: WithoutSystemFields<Doc<"internalState">> = {
|
|
|
47
47
|
// There should only ever be at most one of these scheduled or running.
|
|
48
48
|
export const main = internalMutation({
|
|
49
49
|
args: { generation: v.int64(), segment: v.int64() },
|
|
50
|
-
handler: async (ctx,
|
|
50
|
+
handler: async (ctx, { generation, segment }) => {
|
|
51
51
|
// State will be modified and patched at the end of the function.
|
|
52
52
|
const state = await getOrCreateState(ctx);
|
|
53
|
-
if (
|
|
53
|
+
if (generation !== state.generation) {
|
|
54
54
|
throw new Error(
|
|
55
|
-
`generation mismatch: ${
|
|
55
|
+
`generation mismatch: ${generation} !== ${state.generation}`
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
state.generation++;
|
|
59
|
+
const runStatus = await getOrCreateRunningStatus(ctx);
|
|
60
|
+
if (runStatus.state.kind !== "running") {
|
|
61
|
+
await ctx.db.patch(runStatus._id, {
|
|
62
|
+
state: { kind: "running" },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
59
65
|
|
|
60
66
|
const globals = await getGlobals(ctx);
|
|
61
67
|
const console = createLogger(globals.logLevel);
|
|
68
|
+
const delayMs = Date.now() - fromSegment(segment);
|
|
69
|
+
console.debug(`[main] generation ${generation} behind: ${delayMs}ms`);
|
|
62
70
|
|
|
63
71
|
// Read pendingCompletions, including retry handling.
|
|
64
72
|
console.time("[main] pendingCompletion");
|
|
65
|
-
const toCancel = await handleCompletions(ctx, state,
|
|
73
|
+
const toCancel = await handleCompletions(ctx, state, segment, console);
|
|
66
74
|
console.timeEnd("[main] pendingCompletion");
|
|
67
75
|
|
|
68
76
|
// Read pendingCancelation, deleting from pendingStart. If it's still running, queue to cancel.
|
|
69
77
|
console.time("[main] pendingCancelation");
|
|
70
|
-
await handleCancelation(ctx, state,
|
|
78
|
+
await handleCancelation(ctx, state, segment, console, toCancel);
|
|
71
79
|
console.timeEnd("[main] pendingCancelation");
|
|
72
80
|
|
|
73
81
|
if (state.running.length === 0) {
|
|
74
82
|
// If there's nothing active, reset lastRecovery.
|
|
75
|
-
state.lastRecovery =
|
|
76
|
-
} else if (
|
|
83
|
+
state.lastRecovery = segment;
|
|
84
|
+
} else if (segment - state.lastRecovery >= RECOVERY_PERIOD_SEGMENTS) {
|
|
77
85
|
// Otherwise schedule recovery for any old jobs.
|
|
78
86
|
await handleRecovery(ctx, state, console);
|
|
79
|
-
state.lastRecovery =
|
|
87
|
+
state.lastRecovery = segment;
|
|
80
88
|
}
|
|
81
89
|
|
|
82
90
|
// Read pendingStart up to max capacity. Update the config, and incomingSegmentCursor.
|
|
83
91
|
console.time("[main] pendingStart");
|
|
84
|
-
await handleStart(ctx, state,
|
|
92
|
+
await handleStart(ctx, state, segment, console, globals);
|
|
85
93
|
console.timeEnd("[main] pendingStart");
|
|
86
94
|
|
|
87
95
|
if (Date.now() - state.report.lastReportTs >= MINUTE) {
|
|
@@ -92,7 +100,7 @@ export const main = internalMutation({
|
|
|
92
100
|
// It's been a while, let's start fresh.
|
|
93
101
|
lastReportTs = Date.now();
|
|
94
102
|
}
|
|
95
|
-
|
|
103
|
+
recordReport(console, state);
|
|
96
104
|
state.report = {
|
|
97
105
|
completed: 0,
|
|
98
106
|
succeeded: 0,
|
|
@@ -106,41 +114,42 @@ export const main = internalMutation({
|
|
|
106
114
|
await ctx.db.replace(state._id, state);
|
|
107
115
|
await ctx.scheduler.runAfter(0, internal.loop.updateRunStatus, {
|
|
108
116
|
generation: state.generation,
|
|
109
|
-
segment
|
|
117
|
+
segment,
|
|
110
118
|
});
|
|
119
|
+
// TODO: if there were more cancellations, schedule main directly.
|
|
111
120
|
},
|
|
112
121
|
});
|
|
113
122
|
|
|
114
123
|
export const updateRunStatus = internalMutation({
|
|
115
124
|
args: { generation: v.int64(), segment: v.int64() },
|
|
116
|
-
handler: async (ctx,
|
|
125
|
+
handler: async (ctx, { generation, segment }) => {
|
|
117
126
|
const globals = await getGlobals(ctx);
|
|
118
127
|
const console = createLogger(globals.logLevel);
|
|
119
128
|
const maxParallelism = globals.maxParallelism;
|
|
120
129
|
const state = await getOrCreateState(ctx);
|
|
121
|
-
if (
|
|
130
|
+
if (generation !== state.generation) {
|
|
122
131
|
throw new Error(
|
|
123
|
-
`generation mismatch: ${
|
|
132
|
+
`generation mismatch: ${generation} !== ${state.generation}`
|
|
124
133
|
);
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
console.time("[updateRunStatus] outstandingCancelations");
|
|
128
137
|
const outstandingCancelations = await getNextUp(ctx, "pendingCancelation", {
|
|
129
138
|
start: state.segmentCursors.cancelation,
|
|
130
|
-
end:
|
|
139
|
+
end: segment,
|
|
131
140
|
});
|
|
132
141
|
console.timeEnd("[updateRunStatus] outstandingCancelations");
|
|
133
142
|
if (outstandingCancelations) {
|
|
134
143
|
await ctx.scheduler.runAfter(0, internal.loop.main, {
|
|
135
|
-
generation
|
|
136
|
-
segment
|
|
144
|
+
generation,
|
|
145
|
+
segment,
|
|
137
146
|
});
|
|
138
147
|
return;
|
|
139
148
|
}
|
|
140
149
|
|
|
141
150
|
// TODO: check for current segment (or from args) first, to avoid OCCs.
|
|
142
151
|
console.time("[updateRunStatus] nextSegmentIsActionable");
|
|
143
|
-
const next = max(
|
|
152
|
+
const next = max(segment + 1n, currentSegment());
|
|
144
153
|
const nextIsActionable = await nextSegmentIsActionable(
|
|
145
154
|
ctx,
|
|
146
155
|
state,
|
|
@@ -154,7 +163,7 @@ export const updateRunStatus = internalMutation({
|
|
|
154
163
|
boundScheduledTime(fromSegment(next), console),
|
|
155
164
|
internal.loop.main,
|
|
156
165
|
{
|
|
157
|
-
generation
|
|
166
|
+
generation,
|
|
158
167
|
segment: next,
|
|
159
168
|
}
|
|
160
169
|
);
|
|
@@ -177,7 +186,7 @@ export const updateRunStatus = internalMutation({
|
|
|
177
186
|
},
|
|
178
187
|
});
|
|
179
188
|
await ctx.scheduler.runAfter(0, internal.loop.main, {
|
|
180
|
-
generation
|
|
189
|
+
generation,
|
|
181
190
|
segment: currentSegment(),
|
|
182
191
|
});
|
|
183
192
|
return;
|
|
@@ -199,29 +208,29 @@ export const updateRunStatus = internalMutation({
|
|
|
199
208
|
)
|
|
200
209
|
);
|
|
201
210
|
console.timeEnd("[updateRunStatus] findNextSegment");
|
|
202
|
-
let
|
|
211
|
+
let targetSegment = docs.map((d) => d?.segment).sort()[0];
|
|
203
212
|
const runStatus = await getOrCreateRunningStatus(ctx);
|
|
204
213
|
const saturated = state.running.length >= maxParallelism;
|
|
205
|
-
if (
|
|
214
|
+
if (targetSegment !== undefined || state.running.length > 0) {
|
|
206
215
|
// If there's something to do, schedule for next actionable segment.
|
|
207
216
|
// Or the next recovery, whichever comes first.
|
|
208
217
|
const nextRecoverySegment = state.lastRecovery + RECOVERY_PERIOD_SEGMENTS;
|
|
209
|
-
if (!
|
|
210
|
-
|
|
218
|
+
if (!targetSegment || targetSegment > nextRecoverySegment) {
|
|
219
|
+
targetSegment = nextRecoverySegment;
|
|
211
220
|
}
|
|
212
221
|
const scheduledId = await ctx.scheduler.runAt(
|
|
213
|
-
boundScheduledTime(fromSegment(
|
|
222
|
+
boundScheduledTime(fromSegment(targetSegment), console),
|
|
214
223
|
internal.loop.main,
|
|
215
|
-
{ generation
|
|
224
|
+
{ generation, segment: targetSegment }
|
|
216
225
|
);
|
|
217
|
-
if (
|
|
226
|
+
if (targetSegment > nextSegment()) {
|
|
218
227
|
await ctx.db.patch(runStatus._id, {
|
|
219
228
|
state: {
|
|
220
229
|
kind: "scheduled",
|
|
221
230
|
scheduledId,
|
|
222
231
|
saturated,
|
|
223
|
-
generation
|
|
224
|
-
segment,
|
|
232
|
+
generation,
|
|
233
|
+
segment: targetSegment,
|
|
225
234
|
},
|
|
226
235
|
});
|
|
227
236
|
} else {
|
|
@@ -233,7 +242,7 @@ export const updateRunStatus = internalMutation({
|
|
|
233
242
|
}
|
|
234
243
|
// There seems to be nothing in the future to do, so go idle.
|
|
235
244
|
await ctx.db.patch(runStatus._id, {
|
|
236
|
-
state: { kind: "idle", generation
|
|
245
|
+
state: { kind: "idle", generation },
|
|
237
246
|
});
|
|
238
247
|
},
|
|
239
248
|
});
|
|
@@ -370,7 +379,7 @@ async function handleCompletions(
|
|
|
370
379
|
const retried = await rescheduleJob(ctx, work, console);
|
|
371
380
|
if (retried) {
|
|
372
381
|
state.report.retries++;
|
|
373
|
-
|
|
382
|
+
recordCompleted(console, work, "retrying");
|
|
374
383
|
} else {
|
|
375
384
|
// We don't retry if it's been canceled in the mean time.
|
|
376
385
|
state.report.canceled++;
|
|
@@ -492,9 +501,8 @@ async function handleStart(
|
|
|
492
501
|
state: Doc<"internalState">,
|
|
493
502
|
segment: bigint,
|
|
494
503
|
console: Logger,
|
|
495
|
-
|
|
504
|
+
{ maxParallelism, logLevel }: Config
|
|
496
505
|
) {
|
|
497
|
-
const maxParallelism = globals.maxParallelism;
|
|
498
506
|
// Schedule as many as needed to reach maxParallelism.
|
|
499
507
|
const toSchedule = maxParallelism - state.running.length;
|
|
500
508
|
|
|
@@ -513,12 +521,13 @@ async function handleStart(
|
|
|
513
521
|
state.running.push(
|
|
514
522
|
...(
|
|
515
523
|
await Promise.all(
|
|
516
|
-
pending.map(async ({ _id, workId }) => {
|
|
524
|
+
pending.map(async ({ _id, workId, segment }) => {
|
|
517
525
|
if (state.running.some((r) => r.workId === workId)) {
|
|
518
526
|
console.error(`[main] ${workId} already running (skipping start)`);
|
|
519
527
|
return null;
|
|
520
528
|
}
|
|
521
|
-
const
|
|
529
|
+
const lagMs = Date.now() - fromSegment(segment);
|
|
530
|
+
const scheduledId = await beginWork(ctx, workId, logLevel, lagMs);
|
|
522
531
|
await ctx.db.delete(_id);
|
|
523
532
|
return { scheduledId, workId, started: Date.now() };
|
|
524
533
|
})
|
|
@@ -530,14 +539,15 @@ async function handleStart(
|
|
|
530
539
|
async function beginWork(
|
|
531
540
|
ctx: MutationCtx,
|
|
532
541
|
workId: Id<"work">,
|
|
533
|
-
logLevel: LogLevel
|
|
542
|
+
logLevel: LogLevel,
|
|
543
|
+
lagMs: number
|
|
534
544
|
): Promise<Id<"_scheduled_functions">> {
|
|
535
545
|
const console = createLogger(logLevel);
|
|
536
546
|
const work = await ctx.db.get(workId);
|
|
537
547
|
if (!work) {
|
|
538
548
|
throw new Error("work not found");
|
|
539
549
|
}
|
|
540
|
-
|
|
550
|
+
recordStarted(console, work, lagMs);
|
|
541
551
|
const { attempts: attempt, fnHandle, fnArgs } = work;
|
|
542
552
|
const args = { workId, fnHandle, fnArgs, logLevel, attempt };
|
|
543
553
|
if (work.fnType === "action") {
|
|
@@ -54,7 +54,7 @@ describe("recovery", () => {
|
|
|
54
54
|
workId,
|
|
55
55
|
fnHandle: "test_handle",
|
|
56
56
|
fnArgs: {},
|
|
57
|
-
logLevel: "
|
|
57
|
+
logLevel: "WARN",
|
|
58
58
|
attempt: 0,
|
|
59
59
|
});
|
|
60
60
|
}
|
|
@@ -67,7 +67,7 @@ describe("recovery", () => {
|
|
|
67
67
|
await t.run(async (ctx) => {
|
|
68
68
|
await ctx.db.insert("globals", {
|
|
69
69
|
maxParallelism: 10,
|
|
70
|
-
logLevel: "
|
|
70
|
+
logLevel: "WARN",
|
|
71
71
|
});
|
|
72
72
|
});
|
|
73
73
|
});
|
|
@@ -261,7 +261,7 @@ describe("recovery", () => {
|
|
|
261
261
|
workId,
|
|
262
262
|
fnHandle: "test_handle",
|
|
263
263
|
fnArgs: {},
|
|
264
|
-
logLevel: "
|
|
264
|
+
logLevel: "WARN",
|
|
265
265
|
attempt: 0,
|
|
266
266
|
},
|
|
267
267
|
],
|
|
@@ -327,7 +327,7 @@ describe("recovery", () => {
|
|
|
327
327
|
workId,
|
|
328
328
|
fnHandle: "test_handle",
|
|
329
329
|
fnArgs: {},
|
|
330
|
-
logLevel: "
|
|
330
|
+
logLevel: "WARN",
|
|
331
331
|
attempt: 0,
|
|
332
332
|
},
|
|
333
333
|
],
|
|
@@ -396,7 +396,7 @@ describe("recovery", () => {
|
|
|
396
396
|
workId: workId1,
|
|
397
397
|
fnHandle: "test_handle",
|
|
398
398
|
fnArgs: { test: 1 },
|
|
399
|
-
logLevel: "
|
|
399
|
+
logLevel: "WARN",
|
|
400
400
|
attempt: 0,
|
|
401
401
|
},
|
|
402
402
|
],
|
|
@@ -416,7 +416,7 @@ describe("recovery", () => {
|
|
|
416
416
|
workId: workId2,
|
|
417
417
|
fnHandle: "test_handle",
|
|
418
418
|
fnArgs: { test: 2 },
|
|
419
|
-
logLevel: "
|
|
419
|
+
logLevel: "WARN",
|
|
420
420
|
attempt: 0,
|
|
421
421
|
},
|
|
422
422
|
],
|
|
@@ -503,7 +503,7 @@ describe("recovery", () => {
|
|
|
503
503
|
workId,
|
|
504
504
|
fnHandle: "test_handle",
|
|
505
505
|
fnArgs: {},
|
|
506
|
-
logLevel: "
|
|
506
|
+
logLevel: "WARN",
|
|
507
507
|
attempt: 0,
|
|
508
508
|
},
|
|
509
509
|
],
|