@convex-dev/workpool 0.2.0-beta.0 → 0.2.1

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 (110) hide show
  1. package/README.md +87 -18
  2. package/dist/commonjs/client/index.d.ts +33 -8
  3. package/dist/commonjs/client/index.d.ts.map +1 -1
  4. package/dist/commonjs/client/index.js +37 -7
  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 +82 -0
  9. package/dist/commonjs/component/complete.js.map +1 -0
  10. package/dist/commonjs/component/kick.d.ts +3 -3
  11. package/dist/commonjs/component/kick.d.ts.map +1 -1
  12. package/dist/commonjs/component/kick.js +17 -12
  13. package/dist/commonjs/component/kick.js.map +1 -1
  14. package/dist/commonjs/component/lib.d.ts +6 -6
  15. package/dist/commonjs/component/lib.d.ts.map +1 -1
  16. package/dist/commonjs/component/lib.js +53 -24
  17. package/dist/commonjs/component/lib.js.map +1 -1
  18. package/dist/commonjs/component/logging.d.ts +3 -2
  19. package/dist/commonjs/component/logging.d.ts.map +1 -1
  20. package/dist/commonjs/component/logging.js +34 -16
  21. package/dist/commonjs/component/logging.js.map +1 -1
  22. package/dist/commonjs/component/loop.d.ts +1 -14
  23. package/dist/commonjs/component/loop.d.ts.map +1 -1
  24. package/dist/commonjs/component/loop.js +216 -179
  25. package/dist/commonjs/component/loop.js.map +1 -1
  26. package/dist/commonjs/component/recovery.d.ts +45 -0
  27. package/dist/commonjs/component/recovery.d.ts.map +1 -1
  28. package/dist/commonjs/component/recovery.js +88 -65
  29. package/dist/commonjs/component/recovery.js.map +1 -1
  30. package/dist/commonjs/component/schema.d.ts +17 -13
  31. package/dist/commonjs/component/schema.d.ts.map +1 -1
  32. package/dist/commonjs/component/schema.js +5 -3
  33. package/dist/commonjs/component/schema.js.map +1 -1
  34. package/dist/commonjs/component/shared.d.ts +24 -15
  35. package/dist/commonjs/component/shared.d.ts.map +1 -1
  36. package/dist/commonjs/component/shared.js +20 -7
  37. package/dist/commonjs/component/shared.js.map +1 -1
  38. package/dist/commonjs/component/stats.d.ts +36 -29
  39. package/dist/commonjs/component/stats.d.ts.map +1 -1
  40. package/dist/commonjs/component/stats.js +110 -52
  41. package/dist/commonjs/component/stats.js.map +1 -1
  42. package/dist/commonjs/component/worker.d.ts +4 -14
  43. package/dist/commonjs/component/worker.d.ts.map +1 -1
  44. package/dist/commonjs/component/worker.js +23 -36
  45. package/dist/commonjs/component/worker.js.map +1 -1
  46. package/dist/esm/client/index.d.ts +33 -8
  47. package/dist/esm/client/index.d.ts.map +1 -1
  48. package/dist/esm/client/index.js +37 -7
  49. package/dist/esm/client/index.js.map +1 -1
  50. package/dist/esm/component/complete.d.ts +89 -0
  51. package/dist/esm/component/complete.d.ts.map +1 -0
  52. package/dist/esm/component/complete.js +82 -0
  53. package/dist/esm/component/complete.js.map +1 -0
  54. package/dist/esm/component/kick.d.ts +3 -3
  55. package/dist/esm/component/kick.d.ts.map +1 -1
  56. package/dist/esm/component/kick.js +17 -12
  57. package/dist/esm/component/kick.js.map +1 -1
  58. package/dist/esm/component/lib.d.ts +6 -6
  59. package/dist/esm/component/lib.d.ts.map +1 -1
  60. package/dist/esm/component/lib.js +53 -24
  61. package/dist/esm/component/lib.js.map +1 -1
  62. package/dist/esm/component/logging.d.ts +3 -2
  63. package/dist/esm/component/logging.d.ts.map +1 -1
  64. package/dist/esm/component/logging.js +34 -16
  65. package/dist/esm/component/logging.js.map +1 -1
  66. package/dist/esm/component/loop.d.ts +1 -14
  67. package/dist/esm/component/loop.d.ts.map +1 -1
  68. package/dist/esm/component/loop.js +216 -179
  69. package/dist/esm/component/loop.js.map +1 -1
  70. package/dist/esm/component/recovery.d.ts +45 -0
  71. package/dist/esm/component/recovery.d.ts.map +1 -1
  72. package/dist/esm/component/recovery.js +88 -65
  73. package/dist/esm/component/recovery.js.map +1 -1
  74. package/dist/esm/component/schema.d.ts +17 -13
  75. package/dist/esm/component/schema.d.ts.map +1 -1
  76. package/dist/esm/component/schema.js +5 -3
  77. package/dist/esm/component/schema.js.map +1 -1
  78. package/dist/esm/component/shared.d.ts +24 -15
  79. package/dist/esm/component/shared.d.ts.map +1 -1
  80. package/dist/esm/component/shared.js +20 -7
  81. package/dist/esm/component/shared.js.map +1 -1
  82. package/dist/esm/component/stats.d.ts +36 -29
  83. package/dist/esm/component/stats.d.ts.map +1 -1
  84. package/dist/esm/component/stats.js +110 -52
  85. package/dist/esm/component/stats.js.map +1 -1
  86. package/dist/esm/component/worker.d.ts +4 -14
  87. package/dist/esm/component/worker.d.ts.map +1 -1
  88. package/dist/esm/component/worker.js +23 -36
  89. package/dist/esm/component/worker.js.map +1 -1
  90. package/package.json +12 -12
  91. package/src/client/index.ts +82 -43
  92. package/src/component/README.md +15 -15
  93. package/src/component/_generated/api.d.ts +10 -5
  94. package/src/component/complete.test.ts +508 -0
  95. package/src/component/complete.ts +109 -0
  96. package/src/component/kick.test.ts +29 -19
  97. package/src/component/kick.ts +25 -17
  98. package/src/component/lib.test.ts +262 -17
  99. package/src/component/lib.ts +68 -30
  100. package/src/component/logging.test.ts +16 -0
  101. package/src/component/logging.ts +45 -24
  102. package/src/component/loop.test.ts +1158 -0
  103. package/src/component/loop.ts +292 -224
  104. package/src/component/recovery.test.ts +536 -0
  105. package/src/component/recovery.ts +100 -75
  106. package/src/component/schema.ts +6 -4
  107. package/src/component/shared.ts +23 -8
  108. package/src/component/stats.test.ts +345 -0
  109. package/src/component/stats.ts +149 -56
  110. package/src/component/worker.ts +25 -38
@@ -1,89 +1,179 @@
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 {
4
+ internalMutation,
5
+ internalQuery,
6
+ MutationCtx,
7
+ } from "./_generated/server.js";
8
+ import {
9
+ Config,
10
+ DEFAULT_MAX_PARALLELISM,
11
+ getCurrentSegment,
12
+ } from "./shared.js";
13
+ import { createLogger, Logger, logLevel, shouldLog } from "./logging.js";
14
+ import { internal } from "./_generated/api.js";
15
+ import schema from "./schema.js";
16
+ import { paginator } from "convex-helpers/server/pagination";
17
+
18
+ const BACKLOG_BATCH_SIZE = 100;
4
19
 
5
20
  /**
6
21
  * Record stats about work execution. Intended to be queried by Axiom or Datadog.
22
+ * See the [README](https://github.com/get-convex/workpool) for example queries.
7
23
  */
8
24
 
9
- /**
10
- * Sample axiom dashboard query:
11
-
12
- workpool
13
- | extend parsed_message = iff(
14
- isnotnull(parse_json(trim("'", tostring(["data.message"])))),
15
- parse_json(trim("'", tostring(["data.message"]))),
16
- parse_json('{}')
17
- )
18
- | extend lagSinceEnqueued = parsed_message["lagSinceEnqueued"]
19
- | extend fnName = parsed_message["fnName"]
20
- | summarize avg(todouble(lagSinceEnqueued)) by bin_auto(_time), tostring(fnName)
21
-
22
- */
25
+ export function recordEnqueued(
26
+ console: Logger,
27
+ data: {
28
+ workId: Id<"work">;
29
+ fnName: string;
30
+ runAt: number;
31
+ }
32
+ ) {
33
+ console.event("enqueued", {
34
+ ...data,
35
+ enqueuedAt: Date.now(),
36
+ });
37
+ }
23
38
 
24
- export function recordStarted(work: Doc<"work">): string {
25
- return JSON.stringify({
39
+ export function recordStarted(
40
+ console: Logger,
41
+ work: Doc<"work">,
42
+ lagMs: number
43
+ ) {
44
+ console.event("started", {
26
45
  workId: work._id,
27
- event: "started",
28
46
  fnName: work.fnName,
29
47
  enqueuedAt: work._creationTime,
30
48
  startedAt: Date.now(),
31
- lagSinceEnqueued: Date.now() - work._creationTime,
49
+ startLag: lagMs,
32
50
  });
33
51
  }
34
52
 
35
53
  export function recordCompleted(
54
+ console: Logger,
36
55
  work: Doc<"work">,
37
- status: "success" | "failed" | "canceled"
38
- ): string {
39
- return JSON.stringify({
56
+ status: "success" | "failed" | "canceled" | "retrying"
57
+ ) {
58
+ console.event("completed", {
40
59
  workId: work._id,
41
- event: "completed",
42
60
  fnName: work.fnName,
43
61
  completedAt: Date.now(),
62
+ attempts: work.attempts,
44
63
  status,
45
- lagSinceEnqueued: Date.now() - work._creationTime,
46
64
  });
47
65
  }
48
66
 
49
- export function recordReport(state: Doc<"internalState">): string {
50
- const { completed, succeeded, failed, retries, canceled } = state.report;
51
- const withoutRetries = completed - retries;
52
- return JSON.stringify({
53
- event: "report",
54
- completed,
55
- succeeded,
56
- failed,
57
- retries,
58
- canceled,
59
- failureRate: completed ? (failed + retries) / completed : 0,
60
- permanentFailureRate: withoutRetries ? failed / withoutRetries : 0,
61
- });
67
+ export async function generateReport(
68
+ ctx: MutationCtx,
69
+ console: Logger,
70
+ state: Doc<"internalState">,
71
+ { maxParallelism, logLevel }: Config
72
+ ) {
73
+ if (!shouldLog(logLevel, "REPORT")) {
74
+ // Don't waste time if we're not going to log.
75
+ return;
76
+ }
77
+ const currentSegment = getCurrentSegment();
78
+ const pendingStart = await paginator(ctx.db, schema)
79
+ .query("pendingStart")
80
+ .withIndex("segment", (q) =>
81
+ q
82
+ .gte("segment", state.segmentCursors.incoming)
83
+ .lt("segment", currentSegment)
84
+ )
85
+ .paginate({
86
+ numItems: maxParallelism,
87
+ cursor: null,
88
+ });
89
+ if (pendingStart.isDone) {
90
+ recordReport(console, {
91
+ ...state.report,
92
+ running: state.running.length,
93
+ backlog: pendingStart.page.length,
94
+ });
95
+ } else {
96
+ await ctx.scheduler.runAfter(0, internal.stats.calculateBacklogAndReport, {
97
+ startSegment: state.segmentCursors.incoming,
98
+ endSegment: currentSegment,
99
+ cursor: pendingStart.continueCursor,
100
+ report: state.report,
101
+ running: state.running.length,
102
+ logLevel,
103
+ });
104
+ }
62
105
  }
63
106
 
64
- /**
65
- * Warning: this should not be used from a mutation, as it will cause conflicts.
66
- * Use this to debug or diagnose your queue length when it's backed up.
67
- */
68
- export const queueLength = internalQuery({
69
- args: {},
70
- returns: v.number(),
71
- handler: async (ctx) => {
72
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
73
- return (ctx.db.query("pendingStart") as any).count();
107
+ export const calculateBacklogAndReport = internalMutation({
108
+ args: {
109
+ startSegment: v.int64(),
110
+ endSegment: v.int64(),
111
+ cursor: v.string(),
112
+ report: schema.tables.internalState.validator.fields.report,
113
+ running: v.number(),
114
+ logLevel,
115
+ },
116
+ handler: async (ctx, args) => {
117
+ const pendingStart = await paginator(ctx.db, schema)
118
+ .query("pendingStart")
119
+ .withIndex("segment", (q) =>
120
+ q.gte("segment", args.startSegment).lt("segment", args.endSegment)
121
+ )
122
+ .paginate({
123
+ numItems: BACKLOG_BATCH_SIZE,
124
+ cursor: args.cursor,
125
+ });
126
+ const console = createLogger(args.logLevel);
127
+ if (pendingStart.isDone) {
128
+ recordReport(console, {
129
+ ...args.report,
130
+ running: args.running,
131
+ backlog: pendingStart.page.length,
132
+ });
133
+ } else {
134
+ await ctx.scheduler.runAfter(
135
+ 0,
136
+ internal.stats.calculateBacklogAndReport,
137
+ {
138
+ startSegment: args.startSegment,
139
+ endSegment: args.endSegment,
140
+ cursor: pendingStart.continueCursor,
141
+ report: args.report,
142
+ running: args.running,
143
+ logLevel: args.logLevel,
144
+ }
145
+ );
146
+ }
74
147
  },
75
148
  });
76
149
 
150
+ function recordReport(
151
+ console: Logger,
152
+ report: Doc<"internalState">["report"] & { running: number; backlog: number }
153
+ ) {
154
+ const { completed, failed, retries } = report;
155
+ const withoutRetries = completed - retries;
156
+ const failureRate = completed ? (failed + retries) / completed : 0;
157
+ const permanentFailureRate = withoutRetries ? failed / withoutRetries : 0;
158
+ console.event("report", {
159
+ ...report,
160
+ failureRate: Number(failureRate.toFixed(4)),
161
+ permanentFailureRate: Number(permanentFailureRate.toFixed(4)),
162
+ });
163
+ }
164
+
77
165
  /**
78
166
  * Warning: this should not be used from a mutation, as it will cause conflicts.
79
167
  * Use this while developing to see the state of the queue.
80
168
  */
81
- export const debugCounts = internalQuery({
169
+ export const diagnostics = internalQuery({
82
170
  args: {},
83
171
  returns: v.any(),
84
172
  handler: async (ctx) => {
85
- const inProgressWork =
86
- (await ctx.db.query("internalState").unique())?.running.length ?? 0;
173
+ const global = await ctx.db.query("globals").unique();
174
+ const internalState = await ctx.db.query("internalState").unique();
175
+ const inProgressWork = internalState?.running.length ?? 0;
176
+ const maxParallelism = global?.maxParallelism ?? DEFAULT_MAX_PARALLELISM;
87
177
  /* eslint-disable @typescript-eslint/no-explicit-any */
88
178
  const pendingStart = await (ctx.db.query("pendingStart") as any).count();
89
179
  const pendingCompletion = await (
@@ -92,13 +182,16 @@ export const debugCounts = internalQuery({
92
182
  const pendingCancelation = await (
93
183
  ctx.db.query("pendingCancelation") as any
94
184
  ).count();
185
+ const runStatus = await ctx.db.query("runStatus").unique();
95
186
  /* eslint-enable @typescript-eslint/no-explicit-any */
96
187
  return {
97
- pendingStart,
98
- inProgressWork,
99
- pendingCompletion,
100
- pendingCancelation,
101
- active: inProgressWork - pendingCompletion,
188
+ canceling: pendingCancelation,
189
+ waiting: pendingStart,
190
+ running: inProgressWork - pendingCompletion,
191
+ completing: pendingCompletion,
192
+ spareCapacity: maxParallelism - inProgressWork,
193
+ runStatus: runStatus?.state.kind,
194
+ generation: internalState?.generation,
102
195
  };
103
196
  },
104
197
  });
@@ -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";