@convex-dev/workpool 0.1.2 → 0.2.0-alpha.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 +155 -17
- package/dist/commonjs/client/index.d.ts +123 -35
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +122 -15
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/commonjs/client/utils.d.ts +16 -0
- package/dist/commonjs/client/utils.d.ts.map +1 -0
- package/dist/commonjs/client/utils.js +2 -0
- package/dist/commonjs/client/utils.js.map +1 -0
- 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/convex.config.d.ts.map +1 -1
- package/dist/commonjs/component/convex.config.js +0 -2
- package/dist/commonjs/component/convex.config.js.map +1 -1
- package/dist/commonjs/component/kick.d.ts +9 -0
- package/dist/commonjs/component/kick.d.ts.map +1 -0
- package/dist/commonjs/component/kick.js +97 -0
- package/dist/commonjs/component/kick.js.map +1 -0
- package/dist/commonjs/component/lib.d.ts +23 -32
- package/dist/commonjs/component/lib.d.ts.map +1 -1
- package/dist/commonjs/component/lib.js +91 -563
- package/dist/commonjs/component/lib.js.map +1 -1
- package/dist/commonjs/component/logging.d.ts +5 -3
- package/dist/commonjs/component/logging.d.ts.map +1 -1
- package/dist/commonjs/component/logging.js +13 -2
- package/dist/commonjs/component/logging.js.map +1 -1
- package/dist/commonjs/component/loop.d.ts +13 -0
- package/dist/commonjs/component/loop.d.ts.map +1 -0
- package/dist/commonjs/component/loop.js +482 -0
- package/dist/commonjs/component/loop.js.map +1 -0
- package/dist/commonjs/component/recovery.d.ts +24 -0
- package/dist/commonjs/component/recovery.d.ts.map +1 -0
- package/dist/commonjs/component/recovery.js +94 -0
- package/dist/commonjs/component/recovery.js.map +1 -0
- package/dist/commonjs/component/schema.d.ts +167 -93
- package/dist/commonjs/component/schema.d.ts.map +1 -1
- package/dist/commonjs/component/schema.js +56 -65
- package/dist/commonjs/component/schema.js.map +1 -1
- package/dist/commonjs/component/shared.d.ts +138 -0
- package/dist/commonjs/component/shared.d.ts.map +1 -0
- package/dist/commonjs/component/shared.js +77 -0
- package/dist/commonjs/component/shared.js.map +1 -0
- package/dist/commonjs/component/stats.d.ts +6 -3
- package/dist/commonjs/component/stats.d.ts.map +1 -1
- package/dist/commonjs/component/stats.js +23 -4
- package/dist/commonjs/component/stats.js.map +1 -1
- package/dist/commonjs/component/worker.d.ts +15 -0
- package/dist/commonjs/component/worker.d.ts.map +1 -0
- package/dist/commonjs/component/worker.js +73 -0
- package/dist/commonjs/component/worker.js.map +1 -0
- package/dist/esm/client/index.d.ts +123 -35
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +122 -15
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/utils.d.ts +16 -0
- package/dist/esm/client/utils.d.ts.map +1 -0
- package/dist/esm/client/utils.js +2 -0
- package/dist/esm/client/utils.js.map +1 -0
- 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/convex.config.d.ts.map +1 -1
- package/dist/esm/component/convex.config.js +0 -2
- package/dist/esm/component/convex.config.js.map +1 -1
- package/dist/esm/component/kick.d.ts +9 -0
- package/dist/esm/component/kick.d.ts.map +1 -0
- package/dist/esm/component/kick.js +97 -0
- package/dist/esm/component/kick.js.map +1 -0
- package/dist/esm/component/lib.d.ts +23 -32
- package/dist/esm/component/lib.d.ts.map +1 -1
- package/dist/esm/component/lib.js +91 -563
- package/dist/esm/component/lib.js.map +1 -1
- package/dist/esm/component/logging.d.ts +5 -3
- package/dist/esm/component/logging.d.ts.map +1 -1
- package/dist/esm/component/logging.js +13 -2
- package/dist/esm/component/logging.js.map +1 -1
- package/dist/esm/component/loop.d.ts +13 -0
- package/dist/esm/component/loop.d.ts.map +1 -0
- package/dist/esm/component/loop.js +482 -0
- package/dist/esm/component/loop.js.map +1 -0
- package/dist/esm/component/recovery.d.ts +24 -0
- package/dist/esm/component/recovery.d.ts.map +1 -0
- package/dist/esm/component/recovery.js +94 -0
- package/dist/esm/component/recovery.js.map +1 -0
- package/dist/esm/component/schema.d.ts +167 -93
- package/dist/esm/component/schema.d.ts.map +1 -1
- package/dist/esm/component/schema.js +56 -65
- package/dist/esm/component/schema.js.map +1 -1
- package/dist/esm/component/shared.d.ts +138 -0
- package/dist/esm/component/shared.d.ts.map +1 -0
- package/dist/esm/component/shared.js +77 -0
- package/dist/esm/component/shared.js.map +1 -0
- package/dist/esm/component/stats.d.ts +6 -3
- package/dist/esm/component/stats.d.ts.map +1 -1
- package/dist/esm/component/stats.js +23 -4
- package/dist/esm/component/stats.js.map +1 -1
- package/dist/esm/component/worker.d.ts +15 -0
- package/dist/esm/component/worker.d.ts.map +1 -0
- package/dist/esm/component/worker.js +73 -0
- package/dist/esm/component/worker.js.map +1 -0
- package/package.json +6 -5
- package/src/client/index.ts +232 -68
- package/src/client/utils.ts +45 -0
- package/src/component/README.md +73 -0
- package/src/component/_generated/api.d.ts +38 -66
- package/src/component/complete.test.ts +508 -0
- package/src/component/complete.ts +98 -0
- package/src/component/convex.config.ts +0 -3
- package/src/component/kick.test.ts +285 -0
- package/src/component/kick.ts +118 -0
- package/src/component/lib.test.ts +448 -0
- package/src/component/lib.ts +105 -667
- package/src/component/logging.ts +24 -12
- package/src/component/loop.test.ts +1204 -0
- package/src/component/loop.ts +637 -0
- package/src/component/recovery.test.ts +541 -0
- package/src/component/recovery.ts +96 -0
- package/src/component/schema.ts +61 -77
- package/src/component/setup.test.ts +5 -0
- package/src/component/shared.ts +141 -0
- package/src/component/stats.ts +26 -8
- package/src/component/worker.ts +81 -0
package/src/client/index.ts
CHANGED
|
@@ -1,121 +1,285 @@
|
|
|
1
1
|
import {
|
|
2
2
|
createFunctionHandle,
|
|
3
3
|
DefaultFunctionArgs,
|
|
4
|
-
Expand,
|
|
5
4
|
FunctionReference,
|
|
6
5
|
FunctionVisibility,
|
|
7
|
-
GenericDataModel,
|
|
8
|
-
GenericMutationCtx,
|
|
9
|
-
GenericQueryCtx,
|
|
10
6
|
getFunctionName,
|
|
11
7
|
} from "convex/server";
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
import { v, VString } from "convex/values";
|
|
9
|
+
import { Mounts } from "../component/_generated/api.js";
|
|
10
|
+
import {
|
|
11
|
+
OnComplete,
|
|
12
|
+
runResult as runResultValidator,
|
|
13
|
+
RunResult,
|
|
14
|
+
type RetryBehavior,
|
|
15
|
+
OnCompleteArgs as SharedOnCompleteArgs,
|
|
16
|
+
Status,
|
|
17
|
+
Config,
|
|
18
|
+
} from "../component/shared.js";
|
|
19
|
+
import { type LogLevel, logLevel } from "../component/logging.js";
|
|
20
|
+
import { RunMutationCtx, RunQueryCtx, UseApi } from "./utils.js";
|
|
21
|
+
import { DEFAULT_LOG_LEVEL } from "../component/logging.js";
|
|
22
|
+
import { DEFAULT_MAX_PARALLELISM } from "../component/kick.js";
|
|
23
|
+
export { runResultValidator, type RunResult };
|
|
17
24
|
|
|
18
|
-
|
|
25
|
+
// Attempts will run with delay [0, 250, 500, 1000, 2000] (ms)
|
|
26
|
+
export const DEFAULT_RETRY_BEHAVIOR: RetryBehavior = {
|
|
27
|
+
maxAttempts: 5,
|
|
28
|
+
initialBackoffMs: 250,
|
|
29
|
+
base: 2,
|
|
30
|
+
};
|
|
31
|
+
export type WorkId = string & { __isWorkId: true };
|
|
32
|
+
export const workIdValidator = v.string() as VString<WorkId>;
|
|
19
33
|
|
|
20
34
|
export class Workpool {
|
|
35
|
+
/**
|
|
36
|
+
* Initializes a Workpool.
|
|
37
|
+
*
|
|
38
|
+
* Note: if you want different pools, you need to *create different instances*
|
|
39
|
+
* of Workpool in convex.config.ts. It isn't sufficient to have different
|
|
40
|
+
* instances of this class.
|
|
41
|
+
*
|
|
42
|
+
* @param component - The component to use, like `components.workpool` from
|
|
43
|
+
* `./_generated/api.ts`.
|
|
44
|
+
* @param options - The options for the Workpool.
|
|
45
|
+
*/
|
|
21
46
|
constructor(
|
|
22
|
-
private component: UseApi<
|
|
47
|
+
private component: UseApi<Mounts>, // UseApi<api> for jump to definition
|
|
23
48
|
private options: {
|
|
24
49
|
/** How many actions/mutations can be running at once within this pool.
|
|
25
50
|
* Min 1, Max 300.
|
|
26
51
|
*/
|
|
27
|
-
maxParallelism
|
|
28
|
-
/** How much to log.
|
|
29
|
-
*
|
|
52
|
+
maxParallelism?: number;
|
|
53
|
+
/** How much to log. This is updated on each call to `enqueue*`,
|
|
54
|
+
* `status`, or `cancel*`.
|
|
55
|
+
* Default is WARN.
|
|
30
56
|
* With INFO, you can see events for started and completed work, which can
|
|
31
|
-
* be parsed.
|
|
57
|
+
* be parsed by tools like [Axiom](https://axiom.co) for monitoring.
|
|
32
58
|
* With DEBUG, you can see timers and internal events for work being
|
|
33
59
|
* scheduled.
|
|
34
60
|
*/
|
|
35
61
|
logLevel?: LogLevel;
|
|
36
|
-
/**
|
|
37
|
-
|
|
62
|
+
/** Default retry behavior for enqueued actions. */
|
|
63
|
+
defaultRetryBehavior?: RetryBehavior;
|
|
64
|
+
/** Whether to retry actions that fail by default. Default: false.
|
|
65
|
+
* NOTE: Only do this if your actions are idempotent.
|
|
66
|
+
* See the docs (README.md) for more details.
|
|
38
67
|
*/
|
|
39
|
-
|
|
68
|
+
retryActionsByDefault?: boolean;
|
|
40
69
|
}
|
|
41
70
|
) {}
|
|
71
|
+
/**
|
|
72
|
+
* Enqueues an action to be run.
|
|
73
|
+
*
|
|
74
|
+
* @param ctx - The mutation or action context that can call ctx.runMutation.
|
|
75
|
+
* @param fn - The action to run, like `internal.example.myAction`.
|
|
76
|
+
* @param fnArgs - The arguments to pass to the action.
|
|
77
|
+
* @param options - The options for the action to specify retry behavior,
|
|
78
|
+
* onComplete handling, and scheduling via `runAt` or `runAfter`.
|
|
79
|
+
* @returns The ID of the work that was enqueued.
|
|
80
|
+
*/
|
|
42
81
|
async enqueueAction<Args extends DefaultFunctionArgs, ReturnType>(
|
|
43
82
|
ctx: RunMutationCtx,
|
|
44
83
|
fn: FunctionReference<"action", FunctionVisibility, Args, ReturnType>,
|
|
45
|
-
fnArgs: Args
|
|
84
|
+
fnArgs: Args,
|
|
85
|
+
options?: {
|
|
86
|
+
/** Whether to retry the action if it fails.
|
|
87
|
+
* If true, it will use the default retry behavior.
|
|
88
|
+
* If custom behavior is provided, it will retry using that behavior.
|
|
89
|
+
* If unset, it will use the Workpool's configured default.
|
|
90
|
+
*/
|
|
91
|
+
retry?: boolean | RetryBehavior;
|
|
92
|
+
} & CallbackOptions &
|
|
93
|
+
SchedulerOptions
|
|
46
94
|
): Promise<WorkId> {
|
|
47
|
-
const
|
|
95
|
+
const retryBehavior = getRetryBehavior(
|
|
96
|
+
this.options.defaultRetryBehavior,
|
|
97
|
+
this.options.retryActionsByDefault,
|
|
98
|
+
options?.retry
|
|
99
|
+
);
|
|
100
|
+
const onComplete: OnComplete | undefined = options?.onComplete
|
|
101
|
+
? {
|
|
102
|
+
fnHandle: await createFunctionHandle(options.onComplete),
|
|
103
|
+
context: options.context,
|
|
104
|
+
}
|
|
105
|
+
: undefined;
|
|
48
106
|
const id = await ctx.runMutation(this.component.lib.enqueue, {
|
|
49
|
-
|
|
50
|
-
fnName: getFunctionName(fn),
|
|
107
|
+
...(await defaultEnqueueArgs(fn, this.options)),
|
|
51
108
|
fnArgs,
|
|
52
109
|
fnType: "action",
|
|
53
|
-
|
|
110
|
+
runAt: getRunAt(options),
|
|
111
|
+
onComplete,
|
|
112
|
+
retryBehavior,
|
|
54
113
|
});
|
|
55
114
|
return id as WorkId;
|
|
56
115
|
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Enqueues a mutation to be run.
|
|
119
|
+
*
|
|
120
|
+
* Note: mutations are not retried by the workpool. Convex automatically
|
|
121
|
+
* retries them on database conflicts and transient failures.
|
|
122
|
+
* Because they're deterministic, external retries don't provide any benefit.
|
|
123
|
+
*
|
|
124
|
+
* @param ctx - The mutation or action context that can call ctx.runMutation.
|
|
125
|
+
* @param fn - The mutation to run, like `internal.example.myMutation`.
|
|
126
|
+
* @param fnArgs - The arguments to pass to the mutation.
|
|
127
|
+
* @param options - The options for the mutation to specify onComplete handling
|
|
128
|
+
* and scheduling via `runAt` or `runAfter`.
|
|
129
|
+
*/
|
|
57
130
|
async enqueueMutation<Args extends DefaultFunctionArgs, ReturnType>(
|
|
58
131
|
ctx: RunMutationCtx,
|
|
59
132
|
fn: FunctionReference<"mutation", FunctionVisibility, Args, ReturnType>,
|
|
60
|
-
fnArgs: Args
|
|
133
|
+
fnArgs: Args,
|
|
134
|
+
options?: CallbackOptions & SchedulerOptions
|
|
61
135
|
): Promise<WorkId> {
|
|
62
|
-
const fnHandle = await createFunctionHandle(fn);
|
|
63
136
|
const id = await ctx.runMutation(this.component.lib.enqueue, {
|
|
64
|
-
|
|
65
|
-
fnName: getFunctionName(fn),
|
|
137
|
+
...(await defaultEnqueueArgs(fn, this.options)),
|
|
66
138
|
fnArgs,
|
|
67
139
|
fnType: "mutation",
|
|
68
|
-
|
|
140
|
+
runAt: getRunAt(options),
|
|
69
141
|
});
|
|
70
142
|
return id as WorkId;
|
|
71
143
|
}
|
|
72
144
|
async cancel(ctx: RunMutationCtx, id: WorkId): Promise<void> {
|
|
73
|
-
await ctx.runMutation(this.component.lib.cancel, {
|
|
145
|
+
await ctx.runMutation(this.component.lib.cancel, {
|
|
146
|
+
id,
|
|
147
|
+
logLevel: this.options.logLevel ?? getDefaultLogLevel(),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
async cancelAll(ctx: RunMutationCtx): Promise<void> {
|
|
151
|
+
await ctx.runMutation(this.component.lib.cancelAll, {
|
|
152
|
+
logLevel: this.options.logLevel ?? getDefaultLogLevel(),
|
|
153
|
+
});
|
|
74
154
|
}
|
|
75
|
-
async status(
|
|
76
|
-
ctx
|
|
77
|
-
id: WorkId
|
|
78
|
-
): Promise<
|
|
79
|
-
| { kind: "pending" }
|
|
80
|
-
| { kind: "inProgress" }
|
|
81
|
-
| { kind: "completed"; completionStatus: CompletionStatus }
|
|
82
|
-
> {
|
|
83
|
-
return await ctx.runQuery(this.component.lib.status, { id });
|
|
155
|
+
async status(ctx: RunQueryCtx, id: WorkId): Promise<Status> {
|
|
156
|
+
return ctx.runQuery(this.component.lib.status, { id });
|
|
84
157
|
}
|
|
85
158
|
}
|
|
86
159
|
|
|
87
|
-
|
|
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
|
+
}
|
|
88
175
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
+
export type SchedulerOptions =
|
|
191
|
+
| {
|
|
192
|
+
/**
|
|
193
|
+
* The time (ms since epoch) to run the action at.
|
|
194
|
+
* If not provided, the action will be run as soon as possible.
|
|
195
|
+
* Note: this is advisory only. It may run later.
|
|
196
|
+
*/
|
|
197
|
+
runAt?: number;
|
|
198
|
+
}
|
|
199
|
+
| {
|
|
200
|
+
/**
|
|
201
|
+
* The number of milliseconds to run the action after.
|
|
202
|
+
* If not provided, the action will be run as soon as possible.
|
|
203
|
+
* Note: this is advisory only. It may run later.
|
|
204
|
+
*/
|
|
205
|
+
runAfter?: number;
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
export type CallbackOptions = {
|
|
209
|
+
/**
|
|
210
|
+
* A mutation to run after the function succeeds, fails, or is canceled.
|
|
211
|
+
* The context type is for your use, feel free to provide a validator for it.
|
|
212
|
+
* e.g.
|
|
213
|
+
* ```ts
|
|
214
|
+
* export const completion = internalMutation({
|
|
215
|
+
* args: {
|
|
216
|
+
* workId: workIdValidator,
|
|
217
|
+
* context: v.any(),
|
|
218
|
+
* result: runResult,
|
|
219
|
+
* },
|
|
220
|
+
* handler: async (ctx, args) => {
|
|
221
|
+
* console.log(args.result, "Got Context back -> ", args.context, Date.now() - args.context);
|
|
222
|
+
* },
|
|
223
|
+
* });
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
onComplete?: FunctionReference<
|
|
227
|
+
"mutation",
|
|
228
|
+
FunctionVisibility,
|
|
229
|
+
OnCompleteArgs
|
|
230
|
+
> | null;
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* A context object to pass to the `onComplete` mutation.
|
|
234
|
+
* Useful for passing data from the enqueue site to the onComplete site.
|
|
235
|
+
*/
|
|
236
|
+
context?: unknown;
|
|
91
237
|
};
|
|
92
|
-
|
|
93
|
-
|
|
238
|
+
|
|
239
|
+
export type OnCompleteArgs = {
|
|
240
|
+
/**
|
|
241
|
+
* The ID of the work that completed.
|
|
242
|
+
*/
|
|
243
|
+
workId: WorkId;
|
|
244
|
+
/**
|
|
245
|
+
* The context object passed when enqueuing the work.
|
|
246
|
+
* Useful for passing data from the enqueue site to the onComplete site.
|
|
247
|
+
*/
|
|
248
|
+
context: unknown;
|
|
249
|
+
/**
|
|
250
|
+
* The result of the run that completed.
|
|
251
|
+
*/
|
|
252
|
+
result: RunResult;
|
|
94
253
|
};
|
|
254
|
+
// ensure OnCompleteArgs satisfies SharedOnCompleteArgs
|
|
255
|
+
const _ = {} as OnCompleteArgs satisfies SharedOnCompleteArgs;
|
|
95
256
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
257
|
+
function getRunAt(options?: SchedulerOptions): number {
|
|
258
|
+
if (!options) {
|
|
259
|
+
return Date.now();
|
|
260
|
+
}
|
|
261
|
+
if ("runAt" in options && options.runAt !== undefined) {
|
|
262
|
+
return options.runAt;
|
|
263
|
+
}
|
|
264
|
+
if ("runAfter" in options && options.runAfter !== undefined) {
|
|
265
|
+
return Date.now() + options.runAfter;
|
|
266
|
+
}
|
|
267
|
+
return Date.now();
|
|
268
|
+
}
|
|
104
269
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}>;
|
|
270
|
+
function getDefaultLogLevel(): LogLevel {
|
|
271
|
+
if (process.env.WORKPOOL_LOG_LEVEL) {
|
|
272
|
+
if (
|
|
273
|
+
!logLevel.members
|
|
274
|
+
.map((m) => m.value as string)
|
|
275
|
+
.includes(process.env.WORKPOOL_LOG_LEVEL)
|
|
276
|
+
) {
|
|
277
|
+
console.warn(
|
|
278
|
+
`Invalid log level (${process.env.WORKPOOL_LOG_LEVEL}), defaulting to "INFO"`
|
|
279
|
+
);
|
|
280
|
+
} else {
|
|
281
|
+
return process.env.WORKPOOL_LOG_LEVEL as LogLevel;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return DEFAULT_LOG_LEVEL;
|
|
285
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Expand,
|
|
3
|
+
FunctionReference,
|
|
4
|
+
GenericMutationCtx,
|
|
5
|
+
GenericQueryCtx,
|
|
6
|
+
} from "convex/server";
|
|
7
|
+
import { GenericId } from "convex/values";
|
|
8
|
+
|
|
9
|
+
import { GenericDataModel } from "convex/server";
|
|
10
|
+
|
|
11
|
+
/* Type utils follow */
|
|
12
|
+
|
|
13
|
+
export type RunQueryCtx = {
|
|
14
|
+
runQuery: GenericQueryCtx<GenericDataModel>["runQuery"];
|
|
15
|
+
};
|
|
16
|
+
export type RunMutationCtx = {
|
|
17
|
+
runMutation: GenericMutationCtx<GenericDataModel>["runMutation"];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type OpaqueIds<T> =
|
|
21
|
+
T extends GenericId<infer _T>
|
|
22
|
+
? string
|
|
23
|
+
: T extends (infer U)[]
|
|
24
|
+
? OpaqueIds<U>[]
|
|
25
|
+
: T extends object
|
|
26
|
+
? { [K in keyof T]: OpaqueIds<T[K]> }
|
|
27
|
+
: T;
|
|
28
|
+
|
|
29
|
+
export type UseApi<API> = Expand<{
|
|
30
|
+
[mod in keyof API]: API[mod] extends FunctionReference<
|
|
31
|
+
infer FType,
|
|
32
|
+
"public",
|
|
33
|
+
infer FArgs,
|
|
34
|
+
infer FReturnType,
|
|
35
|
+
infer FComponentPath
|
|
36
|
+
>
|
|
37
|
+
? FunctionReference<
|
|
38
|
+
FType,
|
|
39
|
+
"internal",
|
|
40
|
+
OpaqueIds<FArgs>,
|
|
41
|
+
OpaqueIds<FReturnType>,
|
|
42
|
+
FComponentPath
|
|
43
|
+
>
|
|
44
|
+
: UseApi<API[mod]>;
|
|
45
|
+
}>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Workpool: implementation notes and high-level architecture
|
|
2
|
+
|
|
3
|
+
Concepts:
|
|
4
|
+
|
|
5
|
+
- `segment`: A slice of time to process work. All work is bucketed into one.
|
|
6
|
+
This enables us to batch work and avoid database conflicts.
|
|
7
|
+
- `generation`: A monotonically increasing counter to ensure the loop is
|
|
8
|
+
only running one instance. If two loops start with the same generation,
|
|
9
|
+
one will successfully increase it, the other will retry and find that the
|
|
10
|
+
generation has changed and fail out.
|
|
11
|
+
- "Retention" is used to refer to situations where a query might have to read
|
|
12
|
+
over a lot of "tombstones" - deleted data that hasn't been vacuumed from the
|
|
13
|
+
underlying database yet. If there are frequent deletions, scanning across them
|
|
14
|
+
can delay a query. Because of our delete-heavy queuing strategy, we have to be
|
|
15
|
+
careful. Strategies are below.
|
|
16
|
+
- Cursors: A pointer to the last processed place in a table. In our case, they
|
|
17
|
+
might allow data to be written before them if out-of-order writes happen, so
|
|
18
|
+
we need to account for finding those "missed" writes on some granularity. We
|
|
19
|
+
choose to wait until there isn't any immediate work to do before those scans.
|
|
20
|
+
They help avoid retention issues.
|
|
21
|
+
|
|
22
|
+
## Data state machine
|
|
23
|
+
|
|
24
|
+
```mermaid
|
|
25
|
+
flowchart LR
|
|
26
|
+
Client -->|enqueue| pendingStart
|
|
27
|
+
Client -->|cancel| pendingCancelation
|
|
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
|
+
```
|
|
37
|
+
|
|
38
|
+
Notably:
|
|
39
|
+
|
|
40
|
+
- The pending\* states are written by outside sources.
|
|
41
|
+
- The main loop federates changes to/from "running"
|
|
42
|
+
- Canceling only impacts pending and retrying jobs.
|
|
43
|
+
|
|
44
|
+
## Loop state machine
|
|
45
|
+
|
|
46
|
+
```mermaid
|
|
47
|
+
flowchart TD
|
|
48
|
+
idle -->|enqueue| running
|
|
49
|
+
running-->|"all started, leftover capacity"| scheduled
|
|
50
|
+
scheduled -->|"enqueue, cancel, saveResult, recovery"| running
|
|
51
|
+
running -->|"maxed out"| saturated
|
|
52
|
+
saturated -->|"cancel, saveResult, recovery"| running
|
|
53
|
+
running-->|"all done"| idle
|
|
54
|
+
```
|
|
55
|
+
|
|
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
|
+
|
|
63
|
+
## Retention optimization strategy
|
|
64
|
+
|
|
65
|
+
- Producers (Client, Worker, Recovery) write to a future "segment".
|
|
66
|
+
- Consumers (main) read the current segment.
|
|
67
|
+
- On conflicts, producers will write to progressively higher segments, while
|
|
68
|
+
the main loop will continue to read the segment originally called with.
|
|
69
|
+
This means conflicts are less likely on each retry.
|
|
70
|
+
- Patch singletons to avoid tombstones.
|
|
71
|
+
- Use segements & cursors to bound reads to latest data.
|
|
72
|
+
- Do scans outside of the critical path (during load).
|
|
73
|
+
- Do point reads otherwise.
|
|
@@ -8,9 +8,15 @@
|
|
|
8
8
|
* @module
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import type * as complete from "../complete.js";
|
|
12
|
+
import type * as kick from "../kick.js";
|
|
11
13
|
import type * as lib from "../lib.js";
|
|
12
14
|
import type * as logging from "../logging.js";
|
|
15
|
+
import type * as loop from "../loop.js";
|
|
16
|
+
import type * as recovery from "../recovery.js";
|
|
17
|
+
import type * as shared from "../shared.js";
|
|
13
18
|
import type * as stats from "../stats.js";
|
|
19
|
+
import type * as worker from "../worker.js";
|
|
14
20
|
|
|
15
21
|
import type {
|
|
16
22
|
ApiFromModules,
|
|
@@ -26,27 +32,49 @@ import type {
|
|
|
26
32
|
* ```
|
|
27
33
|
*/
|
|
28
34
|
declare const fullApi: ApiFromModules<{
|
|
35
|
+
complete: typeof complete;
|
|
36
|
+
kick: typeof kick;
|
|
29
37
|
lib: typeof lib;
|
|
30
38
|
logging: typeof logging;
|
|
39
|
+
loop: typeof loop;
|
|
40
|
+
recovery: typeof recovery;
|
|
41
|
+
shared: typeof shared;
|
|
31
42
|
stats: typeof stats;
|
|
43
|
+
worker: typeof worker;
|
|
32
44
|
}>;
|
|
33
45
|
export type Mounts = {
|
|
34
46
|
lib: {
|
|
35
|
-
cancel: FunctionReference<
|
|
36
|
-
|
|
47
|
+
cancel: FunctionReference<
|
|
48
|
+
"mutation",
|
|
49
|
+
"public",
|
|
50
|
+
{ id: string; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
|
|
51
|
+
any
|
|
52
|
+
>;
|
|
53
|
+
cancelAll: FunctionReference<
|
|
54
|
+
"mutation",
|
|
55
|
+
"public",
|
|
56
|
+
{ before?: number; logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR" },
|
|
57
|
+
any
|
|
58
|
+
>;
|
|
37
59
|
enqueue: FunctionReference<
|
|
38
60
|
"mutation",
|
|
39
61
|
"public",
|
|
40
62
|
{
|
|
63
|
+
config: {
|
|
64
|
+
logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR";
|
|
65
|
+
maxParallelism: number;
|
|
66
|
+
};
|
|
41
67
|
fnArgs: any;
|
|
42
68
|
fnHandle: string;
|
|
43
69
|
fnName: string;
|
|
44
70
|
fnType: "action" | "mutation";
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
onComplete?: { context?: any; fnHandle: string };
|
|
72
|
+
retryBehavior?: {
|
|
73
|
+
base: number;
|
|
74
|
+
initialBackoffMs: number;
|
|
75
|
+
maxAttempts: number;
|
|
49
76
|
};
|
|
77
|
+
runAt: number;
|
|
50
78
|
},
|
|
51
79
|
string
|
|
52
80
|
>;
|
|
@@ -54,14 +82,10 @@ export type Mounts = {
|
|
|
54
82
|
"query",
|
|
55
83
|
"public",
|
|
56
84
|
{ id: string },
|
|
57
|
-
| {
|
|
58
|
-
| {
|
|
59
|
-
| {
|
|
60
|
-
completionStatus: "success" | "error" | "canceled" | "timeout";
|
|
61
|
-
kind: "completed";
|
|
62
|
-
}
|
|
85
|
+
| { previousAttempts: number; state: "pending" }
|
|
86
|
+
| { previousAttempts: number; state: "running" }
|
|
87
|
+
| { state: "finished" }
|
|
63
88
|
>;
|
|
64
|
-
stopCleanup: FunctionReference<"mutation", "public", {}, any>;
|
|
65
89
|
};
|
|
66
90
|
};
|
|
67
91
|
// For now fullApiWithMounts is only fullApi which provides
|
|
@@ -78,56 +102,4 @@ export declare const internal: FilterApi<
|
|
|
78
102
|
FunctionReference<any, "internal">
|
|
79
103
|
>;
|
|
80
104
|
|
|
81
|
-
export declare const components: {
|
|
82
|
-
crons: {
|
|
83
|
-
public: {
|
|
84
|
-
del: FunctionReference<
|
|
85
|
-
"mutation",
|
|
86
|
-
"internal",
|
|
87
|
-
{ identifier: { id: string } | { name: string } },
|
|
88
|
-
null
|
|
89
|
-
>;
|
|
90
|
-
get: FunctionReference<
|
|
91
|
-
"query",
|
|
92
|
-
"internal",
|
|
93
|
-
{ identifier: { id: string } | { name: string } },
|
|
94
|
-
{
|
|
95
|
-
args: Record<string, any>;
|
|
96
|
-
functionHandle: string;
|
|
97
|
-
id: string;
|
|
98
|
-
name?: string;
|
|
99
|
-
schedule:
|
|
100
|
-
| { kind: "interval"; ms: number }
|
|
101
|
-
| { cronspec: string; kind: "cron" };
|
|
102
|
-
} | null
|
|
103
|
-
>;
|
|
104
|
-
list: FunctionReference<
|
|
105
|
-
"query",
|
|
106
|
-
"internal",
|
|
107
|
-
{},
|
|
108
|
-
Array<{
|
|
109
|
-
args: Record<string, any>;
|
|
110
|
-
functionHandle: string;
|
|
111
|
-
id: string;
|
|
112
|
-
name?: string;
|
|
113
|
-
schedule:
|
|
114
|
-
| { kind: "interval"; ms: number }
|
|
115
|
-
| { cronspec: string; kind: "cron" };
|
|
116
|
-
}>
|
|
117
|
-
>;
|
|
118
|
-
register: FunctionReference<
|
|
119
|
-
"mutation",
|
|
120
|
-
"internal",
|
|
121
|
-
{
|
|
122
|
-
args: Record<string, any>;
|
|
123
|
-
functionHandle: string;
|
|
124
|
-
name?: string;
|
|
125
|
-
schedule:
|
|
126
|
-
| { kind: "interval"; ms: number }
|
|
127
|
-
| { cronspec: string; kind: "cron" };
|
|
128
|
-
},
|
|
129
|
-
string
|
|
130
|
-
>;
|
|
131
|
-
};
|
|
132
|
-
};
|
|
133
|
-
};
|
|
105
|
+
export declare const components: {};
|