@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/client/index.ts
CHANGED
|
@@ -8,19 +8,22 @@ import {
|
|
|
8
8
|
import { v, VString } from "convex/values";
|
|
9
9
|
import { Mounts } from "../component/_generated/api.js";
|
|
10
10
|
import {
|
|
11
|
+
DEFAULT_LOG_LEVEL,
|
|
12
|
+
type LogLevel,
|
|
13
|
+
logLevel,
|
|
14
|
+
} from "../component/logging.js";
|
|
15
|
+
import {
|
|
16
|
+
Config,
|
|
17
|
+
DEFAULT_MAX_PARALLELISM,
|
|
11
18
|
OnComplete,
|
|
12
|
-
runResult as
|
|
13
|
-
RunResult,
|
|
19
|
+
runResult as resultValidator,
|
|
14
20
|
type RetryBehavior,
|
|
21
|
+
RunResult,
|
|
15
22
|
OnCompleteArgs as SharedOnCompleteArgs,
|
|
16
23
|
Status,
|
|
17
|
-
Config,
|
|
18
24
|
} from "../component/shared.js";
|
|
19
|
-
import { type LogLevel, logLevel } from "../component/logging.js";
|
|
20
25
|
import { RunMutationCtx, RunQueryCtx, UseApi } from "./utils.js";
|
|
21
|
-
|
|
22
|
-
import { DEFAULT_MAX_PARALLELISM } from "../component/kick.js";
|
|
23
|
-
export { runResultValidator, type RunResult };
|
|
26
|
+
export { resultValidator, type RunResult };
|
|
24
27
|
|
|
25
28
|
// Attempts will run with delay [0, 250, 500, 1000, 2000] (ms)
|
|
26
29
|
export const DEFAULT_RETRY_BEHAVIOR: RetryBehavior = {
|
|
@@ -52,17 +55,20 @@ export class Workpool {
|
|
|
52
55
|
maxParallelism?: number;
|
|
53
56
|
/** How much to log. This is updated on each call to `enqueue*`,
|
|
54
57
|
* `status`, or `cancel*`.
|
|
55
|
-
* Default is
|
|
56
|
-
* With INFO, you can see events for started and completed work
|
|
57
|
-
* be parsed by tools like
|
|
58
|
+
* Default is REPORT, which logs warnings, errors, and a periodic report.
|
|
59
|
+
* With INFO, you can also see events for started and completed work.
|
|
60
|
+
* Stats generated can be parsed by tools like
|
|
61
|
+
* [Axiom](https://axiom.co) for monitoring.
|
|
58
62
|
* With DEBUG, you can see timers and internal events for work being
|
|
59
63
|
* scheduled.
|
|
60
64
|
*/
|
|
61
65
|
logLevel?: LogLevel;
|
|
62
|
-
/** Default retry behavior for enqueued actions.
|
|
66
|
+
/** Default retry behavior for enqueued actions.
|
|
67
|
+
* See {@link RetryBehavior}.
|
|
68
|
+
*/
|
|
63
69
|
defaultRetryBehavior?: RetryBehavior;
|
|
64
70
|
/** Whether to retry actions that fail by default. Default: false.
|
|
65
|
-
* NOTE: Only
|
|
71
|
+
* NOTE: Only enable this if your actions are idempotent.
|
|
66
72
|
* See the docs (README.md) for more details.
|
|
67
73
|
*/
|
|
68
74
|
retryActionsByDefault?: boolean;
|
|
@@ -133,60 +139,59 @@ export class Workpool {
|
|
|
133
139
|
fnArgs: Args,
|
|
134
140
|
options?: CallbackOptions & SchedulerOptions
|
|
135
141
|
): Promise<WorkId> {
|
|
142
|
+
const onComplete: OnComplete | undefined = options?.onComplete
|
|
143
|
+
? {
|
|
144
|
+
fnHandle: await createFunctionHandle(options.onComplete),
|
|
145
|
+
context: options.context,
|
|
146
|
+
}
|
|
147
|
+
: undefined;
|
|
136
148
|
const id = await ctx.runMutation(this.component.lib.enqueue, {
|
|
137
149
|
...(await defaultEnqueueArgs(fn, this.options)),
|
|
138
150
|
fnArgs,
|
|
139
151
|
fnType: "mutation",
|
|
140
152
|
runAt: getRunAt(options),
|
|
153
|
+
onComplete,
|
|
141
154
|
});
|
|
142
155
|
return id as WorkId;
|
|
143
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Cancels a work item. If it's already started, it will be allowed to finish
|
|
159
|
+
* but will not be retried.
|
|
160
|
+
*
|
|
161
|
+
* @param ctx - The mutation or action context that can call ctx.runMutation.
|
|
162
|
+
* @param id - The ID of the work to cancel.
|
|
163
|
+
*/
|
|
144
164
|
async cancel(ctx: RunMutationCtx, id: WorkId): Promise<void> {
|
|
145
165
|
await ctx.runMutation(this.component.lib.cancel, {
|
|
146
166
|
id,
|
|
147
167
|
logLevel: this.options.logLevel ?? getDefaultLogLevel(),
|
|
148
168
|
});
|
|
149
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Cancels all pending work items. See {@link cancel}.
|
|
172
|
+
*
|
|
173
|
+
* @param ctx - The mutation or action context that can call ctx.runMutation.
|
|
174
|
+
*/
|
|
150
175
|
async cancelAll(ctx: RunMutationCtx): Promise<void> {
|
|
151
176
|
await ctx.runMutation(this.component.lib.cancelAll, {
|
|
152
177
|
logLevel: this.options.logLevel ?? getDefaultLogLevel(),
|
|
153
178
|
});
|
|
154
179
|
}
|
|
180
|
+
/**
|
|
181
|
+
* Gets the status of a work item.
|
|
182
|
+
*
|
|
183
|
+
* @param ctx - The query context that can call ctx.runQuery.
|
|
184
|
+
* @param id - The ID of the work to get the status of.
|
|
185
|
+
* @returns The status of the work item. One of:
|
|
186
|
+
* - `{ state: "pending", previousAttempts: number }`
|
|
187
|
+
* - `{ state: "running", previousAttempts: number }`
|
|
188
|
+
* - `{ state: "finished" }`
|
|
189
|
+
*/
|
|
155
190
|
async status(ctx: RunQueryCtx, id: WorkId): Promise<Status> {
|
|
156
191
|
return ctx.runQuery(this.component.lib.status, { id });
|
|
157
192
|
}
|
|
158
193
|
}
|
|
159
194
|
|
|
160
|
-
function getRetryBehavior(
|
|
161
|
-
defaultRetryBehavior: RetryBehavior | undefined,
|
|
162
|
-
retryActionsByDefault: boolean | undefined,
|
|
163
|
-
retryOverride: boolean | RetryBehavior | undefined
|
|
164
|
-
): RetryBehavior | undefined {
|
|
165
|
-
const defaultRetry = defaultRetryBehavior ?? DEFAULT_RETRY_BEHAVIOR;
|
|
166
|
-
const retryByDefault = retryActionsByDefault ?? false;
|
|
167
|
-
if (retryOverride === true) {
|
|
168
|
-
return defaultRetry;
|
|
169
|
-
}
|
|
170
|
-
if (retryOverride === false) {
|
|
171
|
-
return undefined;
|
|
172
|
-
}
|
|
173
|
-
return retryOverride ?? (retryByDefault ? defaultRetry : undefined);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
async function defaultEnqueueArgs(
|
|
177
|
-
fn: FunctionReference<"action" | "mutation", FunctionVisibility>,
|
|
178
|
-
{ logLevel, maxParallelism }: Partial<Config>
|
|
179
|
-
) {
|
|
180
|
-
return {
|
|
181
|
-
fnHandle: await createFunctionHandle(fn),
|
|
182
|
-
fnName: getFunctionName(fn),
|
|
183
|
-
config: {
|
|
184
|
-
logLevel: logLevel ?? getDefaultLogLevel(),
|
|
185
|
-
maxParallelism: maxParallelism ?? DEFAULT_MAX_PARALLELISM,
|
|
186
|
-
},
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
|
|
190
195
|
export type SchedulerOptions =
|
|
191
196
|
| {
|
|
192
197
|
/**
|
|
@@ -215,7 +220,7 @@ export type CallbackOptions = {
|
|
|
215
220
|
* args: {
|
|
216
221
|
* workId: workIdValidator,
|
|
217
222
|
* context: v.any(),
|
|
218
|
-
* result:
|
|
223
|
+
* result: resultValidator,
|
|
219
224
|
* },
|
|
220
225
|
* handler: async (ctx, args) => {
|
|
221
226
|
* console.log(args.result, "Got Context back -> ", args.context, Date.now() - args.context);
|
|
@@ -254,6 +259,40 @@ export type OnCompleteArgs = {
|
|
|
254
259
|
// ensure OnCompleteArgs satisfies SharedOnCompleteArgs
|
|
255
260
|
const _ = {} as OnCompleteArgs satisfies SharedOnCompleteArgs;
|
|
256
261
|
|
|
262
|
+
//
|
|
263
|
+
// Helper functions
|
|
264
|
+
//
|
|
265
|
+
|
|
266
|
+
function getRetryBehavior(
|
|
267
|
+
defaultRetryBehavior: RetryBehavior | undefined,
|
|
268
|
+
retryActionsByDefault: boolean | undefined,
|
|
269
|
+
retryOverride: boolean | RetryBehavior | undefined
|
|
270
|
+
): RetryBehavior | undefined {
|
|
271
|
+
const defaultRetry = defaultRetryBehavior ?? DEFAULT_RETRY_BEHAVIOR;
|
|
272
|
+
const retryByDefault = retryActionsByDefault ?? false;
|
|
273
|
+
if (retryOverride === true) {
|
|
274
|
+
return defaultRetry;
|
|
275
|
+
}
|
|
276
|
+
if (retryOverride === false) {
|
|
277
|
+
return undefined;
|
|
278
|
+
}
|
|
279
|
+
return retryOverride ?? (retryByDefault ? defaultRetry : undefined);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
async function defaultEnqueueArgs(
|
|
283
|
+
fn: FunctionReference<"action" | "mutation", FunctionVisibility>,
|
|
284
|
+
{ logLevel, maxParallelism }: Partial<Config>
|
|
285
|
+
) {
|
|
286
|
+
return {
|
|
287
|
+
fnHandle: await createFunctionHandle(fn),
|
|
288
|
+
fnName: getFunctionName(fn),
|
|
289
|
+
config: {
|
|
290
|
+
logLevel: logLevel ?? getDefaultLogLevel(),
|
|
291
|
+
maxParallelism: maxParallelism ?? DEFAULT_MAX_PARALLELISM,
|
|
292
|
+
},
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
257
296
|
function getRunAt(options?: SchedulerOptions): number {
|
|
258
297
|
if (!options) {
|
|
259
298
|
return Date.now();
|
package/src/component/README.md
CHANGED
|
@@ -25,19 +25,19 @@ Concepts:
|
|
|
25
25
|
flowchart LR
|
|
26
26
|
Client -->|enqueue| pendingStart
|
|
27
27
|
Client -->|cancel| pendingCancelation
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
workerRunning
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
28
|
+
complete --> |success or failure| pendingCompletion
|
|
29
|
+
pendingCompletion -->|retry| pendingStart
|
|
30
|
+
pendingStart --> workerRunning["worker running"]
|
|
31
|
+
workerRunning -->|worker finished| complete
|
|
32
|
+
workerRunning --> |recovery| complete
|
|
33
|
+
successfulCancel["AND"]@{shape: delay} --> |canceled| complete
|
|
34
|
+
pendingStart --> successfulCancel
|
|
35
|
+
pendingCancelation --> successfulCancel
|
|
36
36
|
```
|
|
37
37
|
|
|
38
38
|
Notably:
|
|
39
39
|
|
|
40
|
-
- The pending\* states are
|
|
40
|
+
- The pending\* states are written by outside sources.
|
|
41
41
|
- The main loop federates changes to/from "running"
|
|
42
42
|
- Canceling only impacts pending and retrying jobs.
|
|
43
43
|
|
|
@@ -53,12 +53,12 @@ flowchart TD
|
|
|
53
53
|
running-->|"all done"| idle
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
- While the loop is running,
|
|
57
|
-
|
|
58
|
-
- The "saturated" state is concretely "running" or "scheduled"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
terminating.
|
|
56
|
+
- While the loop is running, the runStatus doesn't change, making it safer to
|
|
57
|
+
read from clients without database conflicts.
|
|
58
|
+
- The "saturated" state is concretely "running" or "scheduled" at max
|
|
59
|
+
parallelism. There is a boolean set on "scheduled" to avoid clients from
|
|
60
|
+
kicking the main loop on enqueueing, which is unlikely to be productive, since
|
|
61
|
+
the next action needs to be something terminating.
|
|
62
62
|
|
|
63
63
|
## Retention optimization strategy
|
|
64
64
|
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type * as complete from "../complete.js";
|
|
11
12
|
import type * as kick from "../kick.js";
|
|
12
13
|
import type * as lib from "../lib.js";
|
|
13
14
|
import type * as logging from "../logging.js";
|
|
@@ -31,6 +32,7 @@ import type {
|
|
|
31
32
|
* ```
|
|
32
33
|
*/
|
|
33
34
|
declare const fullApi: ApiFromModules<{
|
|
35
|
+
complete: typeof complete;
|
|
34
36
|
kick: typeof kick;
|
|
35
37
|
lib: typeof lib;
|
|
36
38
|
logging: typeof logging;
|
|
@@ -45,13 +47,16 @@ export type Mounts = {
|
|
|
45
47
|
cancel: FunctionReference<
|
|
46
48
|
"mutation",
|
|
47
49
|
"public",
|
|
48
|
-
{ id: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
|
|
50
|
+
{ id: string; logLevel: "DEBUG" | "INFO" | "REPORT" | "WARN" | "ERROR" },
|
|
49
51
|
any
|
|
50
52
|
>;
|
|
51
53
|
cancelAll: FunctionReference<
|
|
52
54
|
"mutation",
|
|
53
55
|
"public",
|
|
54
|
-
{
|
|
56
|
+
{
|
|
57
|
+
before?: number;
|
|
58
|
+
logLevel: "DEBUG" | "INFO" | "REPORT" | "WARN" | "ERROR";
|
|
59
|
+
},
|
|
55
60
|
any
|
|
56
61
|
>;
|
|
57
62
|
enqueue: FunctionReference<
|
|
@@ -59,7 +64,7 @@ export type Mounts = {
|
|
|
59
64
|
"public",
|
|
60
65
|
{
|
|
61
66
|
config: {
|
|
62
|
-
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
|
|
67
|
+
logLevel: "DEBUG" | "INFO" | "REPORT" | "WARN" | "ERROR";
|
|
63
68
|
maxParallelism: number;
|
|
64
69
|
};
|
|
65
70
|
fnArgs: any;
|
|
@@ -80,8 +85,8 @@ export type Mounts = {
|
|
|
80
85
|
"query",
|
|
81
86
|
"public",
|
|
82
87
|
{ id: string },
|
|
83
|
-
| {
|
|
84
|
-
| {
|
|
88
|
+
| { previousAttempts: number; state: "pending" }
|
|
89
|
+
| { previousAttempts: number; state: "running" }
|
|
85
90
|
| { state: "finished" }
|
|
86
91
|
>;
|
|
87
92
|
};
|