@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.
- package/README.md +7 -16
- 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.d.ts +89 -0
- package/dist/commonjs/component/complete.d.ts.map +1 -0
- package/dist/commonjs/component/complete.js +80 -0
- package/dist/commonjs/component/complete.js.map +1 -0
- package/dist/commonjs/component/kick.d.ts +1 -2
- package/dist/commonjs/component/kick.d.ts.map +1 -1
- package/dist/commonjs/component/kick.js +7 -5
- package/dist/commonjs/component/kick.js.map +1 -1
- package/dist/commonjs/component/lib.d.ts +3 -3
- package/dist/commonjs/component/lib.d.ts.map +1 -1
- package/dist/commonjs/component/lib.js +43 -20
- 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 +1 -14
- package/dist/commonjs/component/loop.d.ts.map +1 -1
- package/dist/commonjs/component/loop.js +215 -178
- package/dist/commonjs/component/loop.js.map +1 -1
- package/dist/commonjs/component/recovery.d.ts +16 -0
- package/dist/commonjs/component/recovery.d.ts.map +1 -1
- package/dist/commonjs/component/recovery.js +64 -44
- package/dist/commonjs/component/recovery.js.map +1 -1
- package/dist/commonjs/component/schema.d.ts +6 -2
- package/dist/commonjs/component/schema.d.ts.map +1 -1
- package/dist/commonjs/component/schema.js +5 -3
- package/dist/commonjs/component/schema.js.map +1 -1
- package/dist/commonjs/component/shared.d.ts +20 -11
- package/dist/commonjs/component/shared.d.ts.map +1 -1
- package/dist/commonjs/component/shared.js +18 -5
- package/dist/commonjs/component/shared.js.map +1 -1
- package/dist/commonjs/component/stats.d.ts +21 -13
- package/dist/commonjs/component/stats.d.ts.map +1 -1
- package/dist/commonjs/component/stats.js +32 -22
- package/dist/commonjs/component/stats.js.map +1 -1
- package/dist/commonjs/component/worker.d.ts +2 -12
- package/dist/commonjs/component/worker.d.ts.map +1 -1
- package/dist/commonjs/component/worker.js +23 -36
- package/dist/commonjs/component/worker.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.d.ts +89 -0
- package/dist/esm/component/complete.d.ts.map +1 -0
- package/dist/esm/component/complete.js +80 -0
- package/dist/esm/component/complete.js.map +1 -0
- package/dist/esm/component/kick.d.ts +1 -2
- package/dist/esm/component/kick.d.ts.map +1 -1
- package/dist/esm/component/kick.js +7 -5
- package/dist/esm/component/kick.js.map +1 -1
- package/dist/esm/component/lib.d.ts +3 -3
- package/dist/esm/component/lib.d.ts.map +1 -1
- package/dist/esm/component/lib.js +43 -20
- 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 +1 -14
- package/dist/esm/component/loop.d.ts.map +1 -1
- package/dist/esm/component/loop.js +215 -178
- package/dist/esm/component/loop.js.map +1 -1
- package/dist/esm/component/recovery.d.ts +16 -0
- package/dist/esm/component/recovery.d.ts.map +1 -1
- package/dist/esm/component/recovery.js +64 -44
- package/dist/esm/component/recovery.js.map +1 -1
- package/dist/esm/component/schema.d.ts +6 -2
- package/dist/esm/component/schema.d.ts.map +1 -1
- package/dist/esm/component/schema.js +5 -3
- package/dist/esm/component/schema.js.map +1 -1
- package/dist/esm/component/shared.d.ts +20 -11
- package/dist/esm/component/shared.d.ts.map +1 -1
- package/dist/esm/component/shared.js +18 -5
- package/dist/esm/component/shared.js.map +1 -1
- package/dist/esm/component/stats.d.ts +21 -13
- package/dist/esm/component/stats.d.ts.map +1 -1
- package/dist/esm/component/stats.js +32 -22
- package/dist/esm/component/stats.js.map +1 -1
- package/dist/esm/component/worker.d.ts +2 -12
- package/dist/esm/component/worker.d.ts.map +1 -1
- package/dist/esm/component/worker.js +23 -36
- package/dist/esm/component/worker.js.map +1 -1
- package/package.json +7 -6
- package/src/client/index.ts +18 -8
- package/src/component/README.md +15 -15
- package/src/component/_generated/api.d.ts +7 -2
- package/src/component/complete.test.ts +508 -0
- package/src/component/complete.ts +98 -0
- package/src/component/kick.test.ts +13 -13
- package/src/component/kick.ts +13 -8
- package/src/component/lib.test.ts +262 -17
- package/src/component/lib.ts +55 -24
- package/src/component/logging.ts +1 -2
- package/src/component/loop.test.ts +1158 -0
- package/src/component/loop.ts +289 -221
- package/src/component/recovery.test.ts +541 -0
- package/src/component/recovery.ts +80 -63
- package/src/component/schema.ts +6 -4
- package/src/component/shared.ts +21 -6
- package/src/component/stats.ts +48 -25
- package/src/component/worker.ts +25 -38
package/src/component/schema.ts
CHANGED
|
@@ -51,18 +51,19 @@ export default defineSchema({
|
|
|
51
51
|
),
|
|
52
52
|
}),
|
|
53
53
|
|
|
54
|
-
// Written on enqueue.
|
|
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
|
|
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"]),
|
package/src/component/shared.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
102
|
+
previousAttempts: v.number(),
|
|
102
103
|
}),
|
|
103
104
|
v.object({
|
|
104
105
|
state: v.literal("running"),
|
|
105
|
-
|
|
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.
|
|
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.
|
|
121
|
-
"
|
|
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
|
+
}
|
package/src/component/stats.ts
CHANGED
|
@@ -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
|
|
20
|
+
| extend startLag = parsed_message["startLag"]
|
|
19
21
|
| extend fnName = parsed_message["fnName"]
|
|
20
|
-
| summarize avg(todouble(
|
|
22
|
+
| summarize avg(todouble(startLag)) by bin_auto(_time), tostring(fnName)
|
|
21
23
|
|
|
22
24
|
*/
|
|
23
25
|
|
|
24
|
-
export function
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
39
|
-
|
|
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">)
|
|
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
|
-
|
|
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 =
|
|
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
|
|
99
|
+
export const diagnostic = internalQuery({
|
|
82
100
|
args: {},
|
|
83
101
|
returns: v.any(),
|
|
84
102
|
handler: async (ctx) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
pendingCompletion,
|
|
100
|
-
|
|
101
|
-
|
|
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
|
});
|
package/src/component/worker.ts
CHANGED
|
@@ -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 {
|
|
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,
|
|
22
|
-
const console = createLogger(logLevel);
|
|
23
|
-
const fnHandle =
|
|
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.
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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,
|
|
57
|
-
const console = createLogger(logLevel);
|
|
58
|
-
const fnHandle =
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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";
|