@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.
Files changed (70) hide show
  1. package/README.md +1 -1
  2. package/dist/commonjs/client/index.d.ts +3 -3
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +10 -5
  5. package/dist/commonjs/client/index.js.map +1 -1
  6. package/dist/commonjs/component/complete.js +1 -1
  7. package/dist/commonjs/component/complete.js.map +1 -1
  8. package/dist/commonjs/component/kick.d.ts +0 -1
  9. package/dist/commonjs/component/kick.d.ts.map +1 -1
  10. package/dist/commonjs/component/kick.js +6 -4
  11. package/dist/commonjs/component/kick.js.map +1 -1
  12. package/dist/commonjs/component/lib.d.ts.map +1 -1
  13. package/dist/commonjs/component/lib.js +4 -3
  14. package/dist/commonjs/component/lib.js.map +1 -1
  15. package/dist/commonjs/component/logging.d.ts.map +1 -1
  16. package/dist/commonjs/component/logging.js +1 -2
  17. package/dist/commonjs/component/logging.js.map +1 -1
  18. package/dist/commonjs/component/loop.d.ts.map +1 -1
  19. package/dist/commonjs/component/loop.js +46 -38
  20. package/dist/commonjs/component/loop.js.map +1 -1
  21. package/dist/commonjs/component/shared.d.ts +1 -0
  22. package/dist/commonjs/component/shared.d.ts.map +1 -1
  23. package/dist/commonjs/component/shared.js +1 -0
  24. package/dist/commonjs/component/shared.js.map +1 -1
  25. package/dist/commonjs/component/stats.d.ts +19 -13
  26. package/dist/commonjs/component/stats.d.ts.map +1 -1
  27. package/dist/commonjs/component/stats.js +26 -21
  28. package/dist/commonjs/component/stats.js.map +1 -1
  29. package/dist/esm/client/index.d.ts +3 -3
  30. package/dist/esm/client/index.d.ts.map +1 -1
  31. package/dist/esm/client/index.js +10 -5
  32. package/dist/esm/client/index.js.map +1 -1
  33. package/dist/esm/component/complete.js +1 -1
  34. package/dist/esm/component/complete.js.map +1 -1
  35. package/dist/esm/component/kick.d.ts +0 -1
  36. package/dist/esm/component/kick.d.ts.map +1 -1
  37. package/dist/esm/component/kick.js +6 -4
  38. package/dist/esm/component/kick.js.map +1 -1
  39. package/dist/esm/component/lib.d.ts.map +1 -1
  40. package/dist/esm/component/lib.js +4 -3
  41. package/dist/esm/component/lib.js.map +1 -1
  42. package/dist/esm/component/logging.d.ts.map +1 -1
  43. package/dist/esm/component/logging.js +1 -2
  44. package/dist/esm/component/logging.js.map +1 -1
  45. package/dist/esm/component/loop.d.ts.map +1 -1
  46. package/dist/esm/component/loop.js +46 -38
  47. package/dist/esm/component/loop.js.map +1 -1
  48. package/dist/esm/component/shared.d.ts +1 -0
  49. package/dist/esm/component/shared.d.ts.map +1 -1
  50. package/dist/esm/component/shared.js +1 -0
  51. package/dist/esm/component/shared.js.map +1 -1
  52. package/dist/esm/component/stats.d.ts +19 -13
  53. package/dist/esm/component/stats.d.ts.map +1 -1
  54. package/dist/esm/component/stats.js +26 -21
  55. package/dist/esm/component/stats.js.map +1 -1
  56. package/package.json +7 -6
  57. package/src/client/index.ts +18 -8
  58. package/src/component/_generated/api.d.ts +3 -0
  59. package/src/component/complete.test.ts +10 -10
  60. package/src/component/complete.ts +1 -1
  61. package/src/component/kick.test.ts +10 -9
  62. package/src/component/kick.ts +11 -6
  63. package/src/component/lib.test.ts +20 -20
  64. package/src/component/lib.ts +4 -2
  65. package/src/component/logging.ts +1 -2
  66. package/src/component/loop.test.ts +101 -147
  67. package/src/component/loop.ts +49 -39
  68. package/src/component/recovery.test.ts +7 -7
  69. package/src/component/shared.ts +1 -0
  70. 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: "DEBUG",
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: "DEBUG",
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.db.insert("internalState", {
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.db.insert("internalState", {
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.db.insert("internalState", {
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.db.insert("internalState", {
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.db.insert("internalState", {
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.db.insert("internalState", {
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: 1n,
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: 1n,
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
- const scheduledId = await t.run<Id<"_scheduled_functions">>(
488
- async (ctx) => {
489
- // Create internal state
490
- await ctx.db.insert("internalState", {
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
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
493
- lastRecovery: 0n,
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
- return scheduledId;
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.db.insert("internalState", {
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: 0n,
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
  },
@@ -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 { DEFAULT_MAX_PARALLELISM } from "./kick.js";
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, args) => {
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 (args.generation !== state.generation) {
53
+ if (generation !== state.generation) {
54
54
  throw new Error(
55
- `generation mismatch: ${args.generation} !== ${state.generation}`
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, args.segment, console);
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, args.segment, console, toCancel);
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 = args.segment;
76
- } else if (args.segment - state.lastRecovery >= RECOVERY_PERIOD_SEGMENTS) {
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 = args.segment;
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, args.segment, console, globals);
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
- console.info(recordReport(state));
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: args.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, args) => {
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 (args.generation !== state.generation) {
130
+ if (generation !== state.generation) {
122
131
  throw new Error(
123
- `generation mismatch: ${args.generation} !== ${state.generation}`
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: args.segment,
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: args.generation,
136
- segment: args.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(args.segment + 1n, currentSegment());
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: args.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: args.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 segment = docs.map((d) => d?.segment).sort()[0];
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 (segment !== undefined || state.running.length > 0) {
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 (!segment || segment > nextRecoverySegment) {
210
- segment = nextRecoverySegment;
218
+ if (!targetSegment || targetSegment > nextRecoverySegment) {
219
+ targetSegment = nextRecoverySegment;
211
220
  }
212
221
  const scheduledId = await ctx.scheduler.runAt(
213
- boundScheduledTime(fromSegment(segment), console),
222
+ boundScheduledTime(fromSegment(targetSegment), console),
214
223
  internal.loop.main,
215
- { generation: args.generation, segment }
224
+ { generation, segment: targetSegment }
216
225
  );
217
- if (segment > nextSegment()) {
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: args.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: args.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
- console.info(recordCompleted(work, "retrying"));
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
- globals: Config
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 scheduledId = await beginWork(ctx, workId, globals.logLevel);
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
- console.info(recordStarted(work));
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: "DEBUG",
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: "INFO",
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: "DEBUG",
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: "DEBUG",
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: "DEBUG",
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: "DEBUG",
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: "DEBUG",
506
+ logLevel: "WARN",
507
507
  attempt: 0,
508
508
  },
509
509
  ],