@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.
- package/README.md +87 -18
- package/dist/commonjs/client/index.d.ts +33 -8
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +37 -7
- 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 +82 -0
- package/dist/commonjs/component/complete.js.map +1 -0
- package/dist/commonjs/component/kick.d.ts +3 -3
- package/dist/commonjs/component/kick.d.ts.map +1 -1
- package/dist/commonjs/component/kick.js +17 -12
- package/dist/commonjs/component/kick.js.map +1 -1
- package/dist/commonjs/component/lib.d.ts +6 -6
- package/dist/commonjs/component/lib.d.ts.map +1 -1
- package/dist/commonjs/component/lib.js +53 -24
- package/dist/commonjs/component/lib.js.map +1 -1
- package/dist/commonjs/component/logging.d.ts +3 -2
- package/dist/commonjs/component/logging.d.ts.map +1 -1
- package/dist/commonjs/component/logging.js +34 -16
- 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 +216 -179
- package/dist/commonjs/component/loop.js.map +1 -1
- package/dist/commonjs/component/recovery.d.ts +45 -0
- package/dist/commonjs/component/recovery.d.ts.map +1 -1
- package/dist/commonjs/component/recovery.js +88 -65
- package/dist/commonjs/component/recovery.js.map +1 -1
- package/dist/commonjs/component/schema.d.ts +17 -13
- 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 +24 -15
- package/dist/commonjs/component/shared.d.ts.map +1 -1
- package/dist/commonjs/component/shared.js +20 -7
- package/dist/commonjs/component/shared.js.map +1 -1
- package/dist/commonjs/component/stats.d.ts +36 -29
- package/dist/commonjs/component/stats.d.ts.map +1 -1
- package/dist/commonjs/component/stats.js +110 -52
- package/dist/commonjs/component/stats.js.map +1 -1
- package/dist/commonjs/component/worker.d.ts +4 -14
- 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 +33 -8
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +37 -7
- 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 +82 -0
- package/dist/esm/component/complete.js.map +1 -0
- package/dist/esm/component/kick.d.ts +3 -3
- package/dist/esm/component/kick.d.ts.map +1 -1
- package/dist/esm/component/kick.js +17 -12
- package/dist/esm/component/kick.js.map +1 -1
- package/dist/esm/component/lib.d.ts +6 -6
- package/dist/esm/component/lib.d.ts.map +1 -1
- package/dist/esm/component/lib.js +53 -24
- package/dist/esm/component/lib.js.map +1 -1
- package/dist/esm/component/logging.d.ts +3 -2
- package/dist/esm/component/logging.d.ts.map +1 -1
- package/dist/esm/component/logging.js +34 -16
- 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 +216 -179
- package/dist/esm/component/loop.js.map +1 -1
- package/dist/esm/component/recovery.d.ts +45 -0
- package/dist/esm/component/recovery.d.ts.map +1 -1
- package/dist/esm/component/recovery.js +88 -65
- package/dist/esm/component/recovery.js.map +1 -1
- package/dist/esm/component/schema.d.ts +17 -13
- 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 +24 -15
- package/dist/esm/component/shared.d.ts.map +1 -1
- package/dist/esm/component/shared.js +20 -7
- package/dist/esm/component/shared.js.map +1 -1
- package/dist/esm/component/stats.d.ts +36 -29
- package/dist/esm/component/stats.d.ts.map +1 -1
- package/dist/esm/component/stats.js +110 -52
- package/dist/esm/component/stats.js.map +1 -1
- package/dist/esm/component/worker.d.ts +4 -14
- 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 +12 -12
- package/src/client/index.ts +82 -43
- package/src/component/README.md +15 -15
- package/src/component/_generated/api.d.ts +10 -5
- package/src/component/complete.test.ts +508 -0
- package/src/component/complete.ts +109 -0
- package/src/component/kick.test.ts +29 -19
- package/src/component/kick.ts +25 -17
- package/src/component/lib.test.ts +262 -17
- package/src/component/lib.ts +68 -30
- package/src/component/logging.test.ts +16 -0
- package/src/component/logging.ts +45 -24
- package/src/component/loop.test.ts +1158 -0
- package/src/component/loop.ts +292 -224
- package/src/component/recovery.test.ts +536 -0
- package/src/component/recovery.ts +100 -75
- package/src/component/schema.ts +6 -4
- package/src/component/shared.ts +23 -8
- package/src/component/stats.test.ts +345 -0
- package/src/component/stats.ts +149 -56
- package/src/component/worker.ts +25 -38
package/src/component/stats.ts
CHANGED
|
@@ -1,89 +1,179 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { Doc } from "./_generated/dataModel.js";
|
|
3
|
-
import {
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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(
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
)
|
|
39
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
169
|
+
export const diagnostics = internalQuery({
|
|
82
170
|
args: {},
|
|
83
171
|
returns: v.any(),
|
|
84
172
|
handler: async (ctx) => {
|
|
85
|
-
const
|
|
86
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
pendingCompletion,
|
|
100
|
-
|
|
101
|
-
|
|
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
|
});
|
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";
|