@convex-dev/workpool 0.2.11 → 0.2.13

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 (44) hide show
  1. package/dist/commonjs/client/index.d.ts +1 -1
  2. package/dist/commonjs/client/index.d.ts.map +1 -1
  3. package/dist/commonjs/client/index.js.map +1 -1
  4. package/dist/commonjs/component/_generated/api.d.ts +1 -3
  5. package/dist/commonjs/component/_generated/api.d.ts.map +1 -1
  6. package/dist/commonjs/component/complete.js.map +1 -1
  7. package/dist/commonjs/component/crons.js.map +1 -1
  8. package/dist/commonjs/component/kick.js.map +1 -1
  9. package/dist/commonjs/component/lib.js.map +1 -1
  10. package/dist/commonjs/component/logging.js.map +1 -1
  11. package/dist/commonjs/component/loop.d.ts.map +1 -1
  12. package/dist/commonjs/component/loop.js +10 -2
  13. package/dist/commonjs/component/loop.js.map +1 -1
  14. package/dist/commonjs/component/recovery.js.map +1 -1
  15. package/dist/commonjs/component/schema.js +6 -6
  16. package/dist/commonjs/component/schema.js.map +1 -1
  17. package/dist/commonjs/component/shared.js +1 -1
  18. package/dist/commonjs/component/shared.js.map +1 -1
  19. package/dist/commonjs/component/stats.js.map +1 -1
  20. package/dist/commonjs/component/worker.js.map +1 -1
  21. package/dist/esm/client/index.d.ts +1 -1
  22. package/dist/esm/client/index.d.ts.map +1 -1
  23. package/dist/esm/client/index.js.map +1 -1
  24. package/dist/esm/component/_generated/api.d.ts +1 -3
  25. package/dist/esm/component/_generated/api.d.ts.map +1 -1
  26. package/dist/esm/component/complete.js.map +1 -1
  27. package/dist/esm/component/crons.js.map +1 -1
  28. package/dist/esm/component/kick.js.map +1 -1
  29. package/dist/esm/component/lib.js.map +1 -1
  30. package/dist/esm/component/logging.js.map +1 -1
  31. package/dist/esm/component/loop.d.ts.map +1 -1
  32. package/dist/esm/component/loop.js +10 -2
  33. package/dist/esm/component/loop.js.map +1 -1
  34. package/dist/esm/component/recovery.js.map +1 -1
  35. package/dist/esm/component/schema.js +6 -6
  36. package/dist/esm/component/schema.js.map +1 -1
  37. package/dist/esm/component/shared.js +1 -1
  38. package/dist/esm/component/shared.js.map +1 -1
  39. package/dist/esm/component/stats.js.map +1 -1
  40. package/dist/esm/component/worker.js.map +1 -1
  41. package/package.json +3 -3
  42. package/src/component/_generated/api.d.ts +1 -0
  43. package/src/component/loop.test.ts +79 -153
  44. package/src/component/loop.ts +9 -2
@@ -17,8 +17,10 @@ import {
17
17
  DEFAULT_MAX_PARALLELISM,
18
18
  getCurrentSegment,
19
19
  getNextSegment,
20
+ HOUR,
20
21
  toSegment,
21
22
  } from "./shared";
23
+ import { DEFAULT_LOG_LEVEL } from "./logging";
22
24
 
23
25
  const modules = import.meta.glob("./**/*.ts");
24
26
 
@@ -32,9 +34,17 @@ describe("loop", () => {
32
34
 
33
35
  async function setMaxParallelism(maxParallelism: number) {
34
36
  await t.run(async (ctx) => {
35
- await ctx.db.patch((await ctx.db.query("globals").unique())!._id, {
36
- maxParallelism,
37
- });
37
+ const globals = await ctx.db.query("globals").unique();
38
+ if (!globals) {
39
+ await ctx.db.insert("globals", {
40
+ logLevel: DEFAULT_LOG_LEVEL,
41
+ maxParallelism,
42
+ });
43
+ } else {
44
+ await ctx.db.patch(globals._id, {
45
+ maxParallelism,
46
+ });
47
+ }
38
48
  });
39
49
  }
40
50
 
@@ -574,20 +584,7 @@ describe("loop", () => {
574
584
  it("should handle generation mismatch", async () => {
575
585
  // Setup state with different generation
576
586
  await t.run(async (ctx) => {
577
- await ctx.db.insert("internalState", {
578
- generation: 2n,
579
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
580
- lastRecovery: 0n,
581
- report: {
582
- completed: 0,
583
- succeeded: 0,
584
- failed: 0,
585
- retries: 0,
586
- canceled: 0,
587
- lastReportTs: Date.now(),
588
- },
589
- running: [],
590
- });
587
+ await insertInternalState(ctx, { generation: 2n });
591
588
  });
592
589
 
593
590
  // Call main with mismatched generation
@@ -606,25 +603,8 @@ describe("loop", () => {
606
603
  const scheduledId = await makeDummyScheduledFunction(ctx, workId);
607
604
 
608
605
  // Create internal state
609
- await ctx.db.insert("internalState", {
610
- generation: 1n,
611
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
612
- lastRecovery: 0n,
613
- report: {
614
- completed: 0,
615
- succeeded: 0,
616
- failed: 0,
617
- retries: 0,
618
- canceled: 0,
619
- lastReportTs: Date.now(),
620
- },
621
- running: [
622
- {
623
- workId,
624
- scheduledId,
625
- started: 900000,
626
- },
627
- ],
606
+ await insertInternalState(ctx, {
607
+ running: [{ workId, scheduledId, started: 900000 }],
628
608
  });
629
609
 
630
610
  // Create pending completion
@@ -672,18 +652,7 @@ describe("loop", () => {
672
652
  const scheduledId = await makeDummyScheduledFunction(ctx, workId);
673
653
 
674
654
  // Create internal state
675
- await ctx.db.insert("internalState", {
676
- generation: 1n,
677
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
678
- lastRecovery: 0n,
679
- report: {
680
- completed: 0,
681
- succeeded: 0,
682
- failed: 0,
683
- retries: 0,
684
- canceled: 0,
685
- lastReportTs: Date.now(),
686
- },
655
+ await insertInternalState(ctx, {
687
656
  running: [
688
657
  {
689
658
  workId,
@@ -743,25 +712,8 @@ describe("loop", () => {
743
712
  );
744
713
 
745
714
  // Create internal state
746
- await ctx.db.insert("internalState", {
747
- generation: 1n,
748
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
749
- lastRecovery: 0n,
750
- report: {
751
- completed: 0,
752
- succeeded: 0,
753
- failed: 0,
754
- retries: 0,
755
- canceled: 0,
756
- lastReportTs: Date.now(),
757
- },
758
- running: [
759
- {
760
- workId: runningWorkId,
761
- scheduledId,
762
- started: 900000,
763
- },
764
- ],
715
+ await insertInternalState(ctx, {
716
+ running: [{ workId: runningWorkId, scheduledId, started: 900000 }],
765
717
  });
766
718
 
767
719
  // Create work
@@ -816,20 +768,7 @@ describe("loop", () => {
816
768
  // Setup state with pending start items
817
769
  const workId = await t.run<Id<"work">>(async (ctx) => {
818
770
  // Create internal state
819
- await ctx.db.insert("internalState", {
820
- generation: 1n,
821
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
822
- lastRecovery: 0n,
823
- report: {
824
- completed: 0,
825
- succeeded: 0,
826
- failed: 0,
827
- retries: 0,
828
- canceled: 0,
829
- lastReportTs: Date.now(),
830
- },
831
- running: [],
832
- });
771
+ await insertInternalState(ctx);
833
772
 
834
773
  // Create work
835
774
  const workId = await makeDummyWork(ctx);
@@ -872,25 +811,9 @@ describe("loop", () => {
872
811
  const scheduledId = await makeDummyScheduledFunction(ctx, workId);
873
812
 
874
813
  // Create internal state with old job
875
- await ctx.db.insert("internalState", {
876
- generation: 1n,
877
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
814
+ await insertInternalState(ctx, {
878
815
  lastRecovery: 0n,
879
- report: {
880
- completed: 0,
881
- succeeded: 0,
882
- failed: 0,
883
- retries: 0,
884
- canceled: 0,
885
- lastReportTs: Date.now(),
886
- },
887
- running: [
888
- {
889
- workId,
890
- scheduledId,
891
- started: oldTime,
892
- },
893
- ],
816
+ running: [{ workId, scheduledId, started: oldTime }],
894
817
  });
895
818
  });
896
819
 
@@ -918,20 +841,7 @@ describe("loop", () => {
918
841
  it("should handle generation mismatch", async () => {
919
842
  // Setup state with different generation
920
843
  await t.run(async (ctx) => {
921
- await ctx.db.insert("internalState", {
922
- generation: 2n,
923
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
924
- lastRecovery: 0n,
925
- report: {
926
- completed: 0,
927
- succeeded: 0,
928
- failed: 0,
929
- retries: 0,
930
- canceled: 0,
931
- lastReportTs: Date.now(),
932
- },
933
- running: [],
934
- });
844
+ await insertInternalState(ctx, { generation: 2n });
935
845
  });
936
846
 
937
847
  // Call updateRunStatus with mismatched generation
@@ -950,20 +860,7 @@ describe("loop", () => {
950
860
  const workId = await makeDummyWork(ctx);
951
861
 
952
862
  // Create internal state
953
- await ctx.db.insert("internalState", {
954
- generation: 1n,
955
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
956
- lastRecovery: 0n,
957
- report: {
958
- completed: 0,
959
- succeeded: 0,
960
- failed: 0,
961
- retries: 0,
962
- canceled: 0,
963
- lastReportTs: Date.now(),
964
- },
965
- running: [],
966
- });
863
+ await insertInternalState(ctx, {});
967
864
 
968
865
  // Create run status
969
866
  await ctx.db.insert("runStatus", {
@@ -998,20 +895,7 @@ describe("loop", () => {
998
895
  // Setup state with no work
999
896
  await t.run(async (ctx) => {
1000
897
  // Create internal state with no running jobs
1001
- await ctx.db.insert("internalState", {
1002
- generation: 1n,
1003
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
1004
- lastRecovery: 0n,
1005
- report: {
1006
- completed: 0,
1007
- succeeded: 0,
1008
- failed: 0,
1009
- retries: 0,
1010
- canceled: 0,
1011
- lastReportTs: Date.now(),
1012
- },
1013
- running: [],
1014
- });
898
+ await insertInternalState(ctx, {});
1015
899
 
1016
900
  // Create run status in running state
1017
901
  await ctx.db.insert("runStatus", {
@@ -1056,18 +940,7 @@ describe("loop", () => {
1056
940
  );
1057
941
 
1058
942
  // Create internal state with max running jobs
1059
- await ctx.db.insert("internalState", {
1060
- generation: 1n,
1061
- segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
1062
- lastRecovery: now,
1063
- report: {
1064
- completed: 0,
1065
- succeeded: 0,
1066
- failed: 0,
1067
- retries: 0,
1068
- canceled: 0,
1069
- lastReportTs: Date.now(),
1070
- },
943
+ await insertInternalState(ctx, {
1071
944
  running: runningJobs,
1072
945
  });
1073
946
 
@@ -1100,6 +973,59 @@ describe("loop", () => {
1100
973
  expect(runStatus!.state.saturated).toBe(true);
1101
974
  });
1102
975
  });
976
+
977
+ it("should reset cursors correctly when there's old work detected", async () => {
978
+ // Setup state with old work
979
+ const now = getCurrentSegment();
980
+ await t.run(async (ctx) => {
981
+ // Create internal state with old work
982
+ await insertInternalState(ctx, {
983
+ segmentCursors: {
984
+ incoming: now - 1n,
985
+ completion: now - 1n,
986
+ cancelation: now - 1n,
987
+ },
988
+ });
989
+ });
990
+
991
+ // Insert very old work
992
+ await t.run(async (ctx) => {
993
+ const workId = await makeDummyWork(ctx);
994
+ await ctx.db.insert("pendingStart", {
995
+ workId,
996
+ segment: 0n,
997
+ });
998
+ });
999
+
1000
+ // Call updateRunStatus
1001
+ await t.mutation(internal.loop.updateRunStatus, {
1002
+ generation: 1n,
1003
+ segment: now,
1004
+ });
1005
+
1006
+ // Verify cursors were reset
1007
+ await t.run(async (ctx) => {
1008
+ const state = await ctx.db.query("internalState").unique();
1009
+ expect(state).toBeDefined();
1010
+ expect(state!.segmentCursors.incoming).toBe(0n);
1011
+ });
1012
+
1013
+ // Set maxParallelism to 0 so it doesn't schedule anything / make progress
1014
+ await setMaxParallelism(0);
1015
+
1016
+ // Run main
1017
+ await t.mutation(internal.loop.main, {
1018
+ generation: 1n,
1019
+ segment: now,
1020
+ });
1021
+
1022
+ // Verify start cursor weren't updated
1023
+ await t.run(async (ctx) => {
1024
+ const state = await ctx.db.query("internalState").unique();
1025
+ expect(state).toBeDefined();
1026
+ expect(state!.segmentCursors.incoming).toBe(0n);
1027
+ });
1028
+ });
1103
1029
  });
1104
1030
 
1105
1031
  describe("complete function", () => {
@@ -28,7 +28,7 @@ const SECOND = 1000;
28
28
  const MINUTE = 60 * SECOND;
29
29
  const RECOVERY_THRESHOLD_MS = 5 * MINUTE; // attempt to recover jobs this old.
30
30
  export const RECOVERY_PERIOD_SEGMENTS = toSegment(1 * MINUTE); // how often to check.
31
- const CURSOR_BUFFER_SEGMENTS = toSegment(2 * SECOND); // buffer for cursor updates.
31
+ const CURSOR_BUFFER_SEGMENTS = toSegment(30 * SECOND); // buffer for cursor updates.
32
32
  export const INITIAL_STATE: WithoutSystemFields<Doc<"internalState">> = {
33
33
  generation: 0n,
34
34
  segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
@@ -521,7 +521,14 @@ async function handleStart(
521
521
  .take(toSchedule)
522
522
  : [];
523
523
 
524
- state.segmentCursors.incoming = pending.at(-1)?.segment ?? segment;
524
+ if (pending) {
525
+ if (pending.length > 0) {
526
+ state.segmentCursors.incoming = pending.at(-1)!.segment;
527
+ } else if (toSchedule > 0) {
528
+ // We have no more pending work, update to now
529
+ state.segmentCursors.incoming = segment;
530
+ }
531
+ }
525
532
  console.debug(`[main] scheduling ${pending.length} pending work`);
526
533
  // Start new work.
527
534
  state.running.push(