@convex-dev/workpool 0.2.0-beta.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 (106) hide show
  1. package/README.md +7 -16
  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.d.ts +89 -0
  7. package/dist/commonjs/component/complete.d.ts.map +1 -0
  8. package/dist/commonjs/component/complete.js +80 -0
  9. package/dist/commonjs/component/complete.js.map +1 -0
  10. package/dist/commonjs/component/kick.d.ts +1 -2
  11. package/dist/commonjs/component/kick.d.ts.map +1 -1
  12. package/dist/commonjs/component/kick.js +7 -5
  13. package/dist/commonjs/component/kick.js.map +1 -1
  14. package/dist/commonjs/component/lib.d.ts +3 -3
  15. package/dist/commonjs/component/lib.d.ts.map +1 -1
  16. package/dist/commonjs/component/lib.js +43 -20
  17. package/dist/commonjs/component/lib.js.map +1 -1
  18. package/dist/commonjs/component/logging.d.ts.map +1 -1
  19. package/dist/commonjs/component/logging.js +1 -2
  20. package/dist/commonjs/component/logging.js.map +1 -1
  21. package/dist/commonjs/component/loop.d.ts +1 -14
  22. package/dist/commonjs/component/loop.d.ts.map +1 -1
  23. package/dist/commonjs/component/loop.js +215 -178
  24. package/dist/commonjs/component/loop.js.map +1 -1
  25. package/dist/commonjs/component/recovery.d.ts +16 -0
  26. package/dist/commonjs/component/recovery.d.ts.map +1 -1
  27. package/dist/commonjs/component/recovery.js +64 -44
  28. package/dist/commonjs/component/recovery.js.map +1 -1
  29. package/dist/commonjs/component/schema.d.ts +6 -2
  30. package/dist/commonjs/component/schema.d.ts.map +1 -1
  31. package/dist/commonjs/component/schema.js +5 -3
  32. package/dist/commonjs/component/schema.js.map +1 -1
  33. package/dist/commonjs/component/shared.d.ts +20 -11
  34. package/dist/commonjs/component/shared.d.ts.map +1 -1
  35. package/dist/commonjs/component/shared.js +18 -5
  36. package/dist/commonjs/component/shared.js.map +1 -1
  37. package/dist/commonjs/component/stats.d.ts +21 -13
  38. package/dist/commonjs/component/stats.d.ts.map +1 -1
  39. package/dist/commonjs/component/stats.js +32 -22
  40. package/dist/commonjs/component/stats.js.map +1 -1
  41. package/dist/commonjs/component/worker.d.ts +2 -12
  42. package/dist/commonjs/component/worker.d.ts.map +1 -1
  43. package/dist/commonjs/component/worker.js +23 -36
  44. package/dist/commonjs/component/worker.js.map +1 -1
  45. package/dist/esm/client/index.d.ts +3 -3
  46. package/dist/esm/client/index.d.ts.map +1 -1
  47. package/dist/esm/client/index.js +10 -5
  48. package/dist/esm/client/index.js.map +1 -1
  49. package/dist/esm/component/complete.d.ts +89 -0
  50. package/dist/esm/component/complete.d.ts.map +1 -0
  51. package/dist/esm/component/complete.js +80 -0
  52. package/dist/esm/component/complete.js.map +1 -0
  53. package/dist/esm/component/kick.d.ts +1 -2
  54. package/dist/esm/component/kick.d.ts.map +1 -1
  55. package/dist/esm/component/kick.js +7 -5
  56. package/dist/esm/component/kick.js.map +1 -1
  57. package/dist/esm/component/lib.d.ts +3 -3
  58. package/dist/esm/component/lib.d.ts.map +1 -1
  59. package/dist/esm/component/lib.js +43 -20
  60. package/dist/esm/component/lib.js.map +1 -1
  61. package/dist/esm/component/logging.d.ts.map +1 -1
  62. package/dist/esm/component/logging.js +1 -2
  63. package/dist/esm/component/logging.js.map +1 -1
  64. package/dist/esm/component/loop.d.ts +1 -14
  65. package/dist/esm/component/loop.d.ts.map +1 -1
  66. package/dist/esm/component/loop.js +215 -178
  67. package/dist/esm/component/loop.js.map +1 -1
  68. package/dist/esm/component/recovery.d.ts +16 -0
  69. package/dist/esm/component/recovery.d.ts.map +1 -1
  70. package/dist/esm/component/recovery.js +64 -44
  71. package/dist/esm/component/recovery.js.map +1 -1
  72. package/dist/esm/component/schema.d.ts +6 -2
  73. package/dist/esm/component/schema.d.ts.map +1 -1
  74. package/dist/esm/component/schema.js +5 -3
  75. package/dist/esm/component/schema.js.map +1 -1
  76. package/dist/esm/component/shared.d.ts +20 -11
  77. package/dist/esm/component/shared.d.ts.map +1 -1
  78. package/dist/esm/component/shared.js +18 -5
  79. package/dist/esm/component/shared.js.map +1 -1
  80. package/dist/esm/component/stats.d.ts +21 -13
  81. package/dist/esm/component/stats.d.ts.map +1 -1
  82. package/dist/esm/component/stats.js +32 -22
  83. package/dist/esm/component/stats.js.map +1 -1
  84. package/dist/esm/component/worker.d.ts +2 -12
  85. package/dist/esm/component/worker.d.ts.map +1 -1
  86. package/dist/esm/component/worker.js +23 -36
  87. package/dist/esm/component/worker.js.map +1 -1
  88. package/package.json +7 -6
  89. package/src/client/index.ts +18 -8
  90. package/src/component/README.md +15 -15
  91. package/src/component/_generated/api.d.ts +7 -2
  92. package/src/component/complete.test.ts +508 -0
  93. package/src/component/complete.ts +98 -0
  94. package/src/component/kick.test.ts +13 -13
  95. package/src/component/kick.ts +13 -8
  96. package/src/component/lib.test.ts +262 -17
  97. package/src/component/lib.ts +55 -24
  98. package/src/component/logging.ts +1 -2
  99. package/src/component/loop.test.ts +1158 -0
  100. package/src/component/loop.ts +289 -221
  101. package/src/component/recovery.test.ts +541 -0
  102. package/src/component/recovery.ts +80 -63
  103. package/src/component/schema.ts +6 -4
  104. package/src/component/shared.ts +21 -6
  105. package/src/component/stats.ts +48 -25
  106. package/src/component/worker.ts +25 -38
@@ -51,18 +51,19 @@ export default defineSchema({
51
51
  ),
52
52
  }),
53
53
 
54
- // Written on enqueue. Safe to read. Deleted by `complete`.
54
+ // Written on enqueue. Deleted by `complete` for success, failure, canceled.
55
55
  work: defineTable({
56
56
  fnType: v.union(v.literal("action"), v.literal("mutation")),
57
57
  fnHandle: v.string(),
58
58
  fnName: v.string(),
59
59
  fnArgs: v.any(),
60
- attempts: v.number(),
60
+ attempts: v.number(), // number of completed attempts
61
61
  onComplete: v.optional(onComplete),
62
62
  retryBehavior: v.optional(retryBehavior),
63
+ canceled: v.optional(v.boolean()),
63
64
  }),
64
65
 
65
- // Written on enqueue, read & deleted by `main`.
66
+ // Written on enqueue & rescheduled for retry, read & deleted by `main`.
66
67
  pendingStart: defineTable({
67
68
  workId: v.id("work"),
68
69
  segment,
@@ -70,11 +71,12 @@ export default defineSchema({
70
71
  .index("workId", ["workId"])
71
72
  .index("segment", ["segment"]),
72
73
 
73
- // Written by job, read & deleted by `main`.
74
+ // Written by complete, read & deleted by `main`.
74
75
  pendingCompletion: defineTable({
75
76
  segment,
76
77
  runResult,
77
78
  workId: v.id("work"),
79
+ retry: v.boolean(),
78
80
  })
79
81
  .index("workId", ["workId"])
80
82
  .index("segment", ["segment"]),
@@ -3,7 +3,8 @@ import { Infer } from "convex/values";
3
3
  import { v } from "convex/values";
4
4
  import { Logger, logLevel } from "./logging.js";
5
5
 
6
- const SEGMENT_MS = 250;
6
+ export const DEFAULT_MAX_PARALLELISM = 10;
7
+ const SEGMENT_MS = 100;
7
8
  export const SECOND = 1000;
8
9
  export const MINUTE = 60 * SECOND;
9
10
  export const HOUR = 60 * MINUTE;
@@ -98,11 +99,11 @@ export const status = v.union(
98
99
  v.union(
99
100
  v.object({
100
101
  state: v.literal("pending"),
101
- attempt: v.number(),
102
+ previousAttempts: v.number(),
102
103
  }),
103
104
  v.object({
104
105
  state: v.literal("running"),
105
- attempt: v.number(),
106
+ previousAttempts: v.number(),
106
107
  }),
107
108
  v.object({
108
109
  state: v.literal("finished"),
@@ -113,15 +114,29 @@ export type Status = Infer<typeof status>;
113
114
 
114
115
  export function boundScheduledTime(ms: number, console: Logger): number {
115
116
  if (ms < Date.now() - YEAR) {
116
- console.warn("runAt is too far in the past, defaulting to now", ms);
117
+ console.error("scheduled time is too old, defaulting to now", ms);
117
118
  return Date.now();
118
119
  }
119
120
  if (ms > Date.now() + 4 * YEAR) {
120
- console.warn(
121
- "runAt is too far in the future, defaulting to 1 year from now",
121
+ console.error(
122
+ "scheduled time is too far in the future, defaulting to 1 year from now",
122
123
  ms
123
124
  );
124
125
  return Date.now() + YEAR;
125
126
  }
126
127
  return ms;
127
128
  }
129
+
130
+ /**
131
+ * Returns the smaller of two bigint values.
132
+ */
133
+ export function min<T extends bigint>(a: T, b: T): T {
134
+ return a > b ? b : a;
135
+ }
136
+
137
+ /**
138
+ * Returns the larger of two bigint values.
139
+ */
140
+ export function max<T extends bigint>(a: T, b: T): T {
141
+ return a < b ? b : a;
142
+ }
@@ -1,6 +1,8 @@
1
1
  import { v } from "convex/values";
2
- import { Doc } from "./_generated/dataModel.js";
3
- import { internalQuery } from "./_generated/server.js";
2
+ import { Doc, Id } from "./_generated/dataModel.js";
3
+ import { internalQuery, query } from "./_generated/server.js";
4
+ import { DEFAULT_MAX_PARALLELISM } from "./shared.js";
5
+ import { Logger } from "./logging.js";
4
6
 
5
7
  /**
6
8
  * Record stats about work execution. Intended to be queried by Axiom or Datadog.
@@ -15,42 +17,58 @@ workpool
15
17
  parse_json(trim("'", tostring(["data.message"]))),
16
18
  parse_json('{}')
17
19
  )
18
- | extend lagSinceEnqueued = parsed_message["lagSinceEnqueued"]
20
+ | extend startLag = parsed_message["startLag"]
19
21
  | extend fnName = parsed_message["fnName"]
20
- | summarize avg(todouble(lagSinceEnqueued)) by bin_auto(_time), tostring(fnName)
22
+ | summarize avg(todouble(startLag)) by bin_auto(_time), tostring(fnName)
21
23
 
22
24
  */
23
25
 
24
- export function recordStarted(work: Doc<"work">): string {
25
- return JSON.stringify({
26
+ export function recordEnqueued(
27
+ console: Logger,
28
+ data: {
29
+ workId: Id<"work">;
30
+ fnName: string;
31
+ runAt: number;
32
+ }
33
+ ) {
34
+ console.event("enqueued", {
35
+ ...data,
36
+ enqueuedAt: Date.now(),
37
+ });
38
+ }
39
+
40
+ export function recordStarted(
41
+ console: Logger,
42
+ work: Doc<"work">,
43
+ lagMs: number
44
+ ) {
45
+ console.event("started", {
26
46
  workId: work._id,
27
- event: "started",
28
47
  fnName: work.fnName,
29
48
  enqueuedAt: work._creationTime,
30
49
  startedAt: Date.now(),
31
- lagSinceEnqueued: Date.now() - work._creationTime,
50
+ startLag: lagMs,
32
51
  });
33
52
  }
34
53
 
35
54
  export function recordCompleted(
55
+ console: Logger,
36
56
  work: Doc<"work">,
37
- status: "success" | "failed" | "canceled"
38
- ): string {
39
- return JSON.stringify({
57
+ status: "success" | "failed" | "canceled" | "retrying"
58
+ ) {
59
+ console.event("completed", {
40
60
  workId: work._id,
41
- event: "completed",
42
61
  fnName: work.fnName,
43
62
  completedAt: Date.now(),
63
+ attempts: work.attempts,
44
64
  status,
45
- lagSinceEnqueued: Date.now() - work._creationTime,
46
65
  });
47
66
  }
48
67
 
49
- export function recordReport(state: Doc<"internalState">): string {
68
+ export function recordReport(console: Logger, state: Doc<"internalState">) {
50
69
  const { completed, succeeded, failed, retries, canceled } = state.report;
51
70
  const withoutRetries = completed - retries;
52
- return JSON.stringify({
53
- event: "report",
71
+ console.event("report", {
54
72
  completed,
55
73
  succeeded,
56
74
  failed,
@@ -65,7 +83,7 @@ export function recordReport(state: Doc<"internalState">): string {
65
83
  * Warning: this should not be used from a mutation, as it will cause conflicts.
66
84
  * Use this to debug or diagnose your queue length when it's backed up.
67
85
  */
68
- export const queueLength = internalQuery({
86
+ export const queueLength = query({
69
87
  args: {},
70
88
  returns: v.number(),
71
89
  handler: async (ctx) => {
@@ -78,12 +96,14 @@ export const queueLength = internalQuery({
78
96
  * Warning: this should not be used from a mutation, as it will cause conflicts.
79
97
  * Use this while developing to see the state of the queue.
80
98
  */
81
- export const debugCounts = internalQuery({
99
+ export const diagnostic = internalQuery({
82
100
  args: {},
83
101
  returns: v.any(),
84
102
  handler: async (ctx) => {
85
- const inProgressWork =
86
- (await ctx.db.query("internalState").unique())?.running.length ?? 0;
103
+ const global = await ctx.db.query("globals").unique();
104
+ const internalState = await ctx.db.query("internalState").unique();
105
+ const inProgressWork = internalState?.running.length ?? 0;
106
+ const maxParallelism = global?.maxParallelism ?? DEFAULT_MAX_PARALLELISM;
87
107
  /* eslint-disable @typescript-eslint/no-explicit-any */
88
108
  const pendingStart = await (ctx.db.query("pendingStart") as any).count();
89
109
  const pendingCompletion = await (
@@ -92,13 +112,16 @@ export const debugCounts = internalQuery({
92
112
  const pendingCancelation = await (
93
113
  ctx.db.query("pendingCancelation") as any
94
114
  ).count();
115
+ const runStatus = await ctx.db.query("runStatus").unique();
95
116
  /* eslint-enable @typescript-eslint/no-explicit-any */
96
117
  return {
97
- pendingStart,
98
- inProgressWork,
99
- pendingCompletion,
100
- pendingCancelation,
101
- active: inProgressWork - pendingCompletion,
118
+ canceling: pendingCancelation,
119
+ waiting: pendingStart,
120
+ running: inProgressWork - pendingCompletion,
121
+ completing: pendingCompletion,
122
+ spareCapacity: maxParallelism - inProgressWork,
123
+ runStatus: runStatus?.state.kind,
124
+ generation: internalState?.generation,
102
125
  };
103
126
  },
104
127
  });
@@ -3,13 +3,12 @@
3
3
  * Should not touch any of loop's tables other than writing to `pendingCompletion`.
4
4
  * It is not responsible for handling retries.
5
5
  */
6
- import { FunctionHandle } from "convex/server";
6
+ import type { FunctionHandle } from "convex/server";
7
7
  import { v } from "convex/values";
8
8
  import { internal } from "./_generated/api.js";
9
9
  import { internalAction, internalMutation } from "./_generated/server.js";
10
- import { kickMainLoop } from "./kick.js";
11
10
  import { createLogger, logLevel } from "./logging.js";
12
- import { nextSegment, runResult } from "./shared.js";
11
+ import type { RunResult } from "./shared.js";
13
12
 
14
13
  export const runMutationWrapper = internalMutation({
15
14
  args: {
@@ -17,23 +16,25 @@ export const runMutationWrapper = internalMutation({
17
16
  fnHandle: v.string(),
18
17
  fnArgs: v.any(),
19
18
  logLevel,
19
+ attempt: v.number(),
20
20
  },
21
- handler: async (ctx, { workId, fnHandle: handleStr, fnArgs, logLevel }) => {
22
- const console = createLogger(logLevel);
23
- const fnHandle = handleStr as FunctionHandle<"mutation">;
21
+ handler: async (ctx, { workId, attempt, ...args }) => {
22
+ const console = createLogger(args.logLevel);
23
+ const fnHandle = args.fnHandle as FunctionHandle<"mutation">;
24
24
  try {
25
- const returnValue = await ctx.runMutation(fnHandle, fnArgs);
25
+ const returnValue = await ctx.runMutation(fnHandle, args.fnArgs);
26
26
  // NOTE: we could run the `saveResult` handler here, or call `ctx.runMutation`,
27
27
  // but we want the mutation to be a separate transaction to reduce the window for OCCs.
28
- await ctx.scheduler.runAfter(0, internal.worker.saveResult, {
29
- workId,
30
- runResult: { kind: "success", returnValue },
28
+ await ctx.scheduler.runAfter(0, internal.complete.complete, {
29
+ jobs: [
30
+ { workId, runResult: { kind: "success", returnValue }, attempt },
31
+ ],
31
32
  });
32
33
  } catch (e: unknown) {
33
34
  console.error(e);
34
- await ctx.scheduler.runAfter(0, internal.worker.saveResult, {
35
- workId,
36
- runResult: { kind: "failed", error: formatError(e) },
35
+ const runResult = { kind: "failed" as const, error: formatError(e) };
36
+ await ctx.scheduler.runAfter(0, internal.complete.complete, {
37
+ jobs: [{ workId, runResult, attempt }],
37
38
  });
38
39
  }
39
40
  },
@@ -52,43 +53,29 @@ export const runActionWrapper = internalAction({
52
53
  fnHandle: v.string(),
53
54
  fnArgs: v.any(),
54
55
  logLevel,
56
+ attempt: v.number(),
55
57
  },
56
- handler: async (ctx, { workId, fnHandle: handleStr, fnArgs, logLevel }) => {
57
- const console = createLogger(logLevel);
58
- const fnHandle = handleStr as FunctionHandle<"action">;
58
+ handler: async (ctx, { workId, attempt, ...args }) => {
59
+ const console = createLogger(args.logLevel);
60
+ const fnHandle = args.fnHandle as FunctionHandle<"action">;
59
61
  try {
60
- const returnValue = await ctx.runAction(fnHandle, fnArgs);
62
+ const returnValue = await ctx.runAction(fnHandle, args.fnArgs);
61
63
  // NOTE: we could run `ctx.runMutation`, but we want to guarantee execution,
62
64
  // and `ctx.scheduler.runAfter` won't OCC.
63
- await ctx.scheduler.runAfter(0, internal.worker.saveResult, {
64
- workId,
65
- runResult: { kind: "success", returnValue },
65
+ const runResult: RunResult = { kind: "success", returnValue };
66
+ await ctx.scheduler.runAfter(0, internal.complete.complete, {
67
+ jobs: [{ workId, runResult, attempt }],
66
68
  });
67
69
  } catch (e: unknown) {
68
70
  console.error(e);
69
71
  // We let the main loop handle the retries.
70
- await ctx.scheduler.runAfter(0, internal.worker.saveResult, {
71
- workId,
72
- runResult: { kind: "failed", error: formatError(e) },
72
+ const runResult: RunResult = { kind: "failed", error: formatError(e) };
73
+ await ctx.scheduler.runAfter(0, internal.complete.complete, {
74
+ jobs: [{ workId, runResult, attempt }],
73
75
  });
74
76
  }
75
77
  },
76
78
  });
77
79
 
78
- export const saveResult = internalMutation({
79
- args: {
80
- workId: v.id("work"),
81
- runResult,
82
- },
83
- handler: async (ctx, { workId, runResult }) => {
84
- await ctx.db.insert("pendingCompletion", {
85
- runResult,
86
- workId,
87
- segment: nextSegment(),
88
- });
89
- await kickMainLoop(ctx, "saveResult");
90
- },
91
- });
92
-
93
80
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
94
81
  const console = "THIS IS A REMINDER TO USE createLogger";