@convex-dev/workpool 0.4.6 → 0.4.7-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/dist/component/_generated/api.d.ts +2 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/complete.d.ts.map +1 -1
- package/dist/component/complete.js +8 -7
- package/dist/component/complete.js.map +1 -1
- package/dist/component/danger.js +7 -7
- package/dist/component/danger.js.map +1 -1
- package/dist/component/future.d.ts +11 -0
- package/dist/component/future.d.ts.map +1 -0
- package/dist/component/future.js +21 -0
- package/dist/component/future.js.map +1 -0
- package/dist/component/kick.d.ts +3 -3
- package/dist/component/kick.d.ts.map +1 -1
- package/dist/component/kick.js +14 -16
- package/dist/component/kick.js.map +1 -1
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +13 -13
- package/dist/component/lib.js.map +1 -1
- package/dist/component/loop.d.ts +44 -1
- package/dist/component/loop.d.ts.map +1 -1
- package/dist/component/loop.js +171 -217
- package/dist/component/loop.js.map +1 -1
- package/dist/component/recovery.d.ts.map +1 -1
- package/dist/component/recovery.js +2 -2
- package/dist/component/recovery.js.map +1 -1
- package/dist/component/schema.d.ts.map +1 -1
- package/dist/component/schema.js +2 -1
- package/dist/component/schema.js.map +1 -1
- package/dist/component/worker.js +1 -1
- package/dist/component/worker.js.map +1 -1
- package/package.json +8 -12
- package/src/component/_generated/api.ts +2 -0
- package/src/component/complete.test.ts +13 -13
- package/src/component/complete.ts +13 -7
- package/src/component/danger.ts +7 -7
- package/src/component/future.ts +38 -0
- package/src/component/kick.test.ts +17 -20
- package/src/component/kick.ts +20 -17
- package/src/component/lib.test.ts +7 -7
- package/src/component/lib.ts +12 -15
- package/src/component/loop.test.ts +695 -1127
- package/src/component/loop.ts +212 -283
- package/src/component/recovery.test.ts +3 -3
- package/src/component/recovery.ts +5 -2
- package/src/component/schema.ts +2 -1
- package/src/component/stateMachine.test.ts +1246 -0
- package/src/component/stats.test.ts +4 -4
- package/src/component/worker.ts +1 -1
package/src/component/kick.ts
CHANGED
|
@@ -8,31 +8,29 @@ import {
|
|
|
8
8
|
type Config,
|
|
9
9
|
fromSegment,
|
|
10
10
|
getCurrentSegment,
|
|
11
|
-
getNextSegment,
|
|
12
11
|
SECOND,
|
|
13
12
|
toSegment,
|
|
14
13
|
} from "./shared.js";
|
|
15
14
|
|
|
16
15
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* Wakes the main loop if it isn't already running. No-ops when a wake-up
|
|
17
|
+
* wouldn't be productive (e.g. enqueue while saturated).
|
|
19
18
|
*/
|
|
20
19
|
export async function kickMainLoop(
|
|
21
20
|
ctx: MutationCtx,
|
|
22
21
|
source: "enqueue" | "cancel" | "complete" | "kick",
|
|
23
22
|
config?: Config,
|
|
24
|
-
): Promise<
|
|
23
|
+
): Promise<void> {
|
|
25
24
|
const globals = config ?? (await getOrUpdateGlobals(ctx, config));
|
|
26
25
|
const console = createLogger(globals.logLevel);
|
|
27
26
|
const runStatus = await getOrCreateRunStatus(ctx);
|
|
28
|
-
const next = getNextSegment();
|
|
29
27
|
|
|
30
28
|
// Only kick to run now if we're scheduled or idle.
|
|
31
29
|
if (runStatus.state.kind === "running") {
|
|
32
30
|
console.debug(
|
|
33
31
|
`[${source}] main is actively running, so we don't need to kick it`,
|
|
34
32
|
);
|
|
35
|
-
return
|
|
33
|
+
return;
|
|
36
34
|
}
|
|
37
35
|
// main is scheduled to run later, so we should cancel it and reschedule.
|
|
38
36
|
if (runStatus.state.kind === "scheduled") {
|
|
@@ -40,24 +38,27 @@ export async function kickMainLoop(
|
|
|
40
38
|
console.debug(
|
|
41
39
|
`[${source}] main is saturated, so we don't need to kick it`,
|
|
42
40
|
);
|
|
43
|
-
return
|
|
41
|
+
return;
|
|
44
42
|
}
|
|
45
43
|
if (source === "complete" && !runStatus.state.saturated) {
|
|
46
44
|
console.debug(
|
|
47
45
|
`[${source}] main is not saturated, so kicking for completion isn't necessary`,
|
|
48
46
|
);
|
|
49
|
-
return
|
|
47
|
+
return;
|
|
50
48
|
}
|
|
51
49
|
if (runStatus.state.segment <= toSegment(Date.now() + SECOND)) {
|
|
52
50
|
console.debug(
|
|
53
51
|
`[${source}] main is scheduled to run soon enough, so we don't need to kick it`,
|
|
54
52
|
);
|
|
55
|
-
return
|
|
53
|
+
return;
|
|
56
54
|
}
|
|
57
55
|
console.debug(
|
|
58
56
|
`[${source}] main is scheduled to run later, so reschedule it to run now`,
|
|
59
57
|
);
|
|
60
|
-
const scheduled = await ctx.db.system.get(
|
|
58
|
+
const scheduled = await ctx.db.system.get(
|
|
59
|
+
"_scheduled_functions",
|
|
60
|
+
runStatus.state.scheduledId,
|
|
61
|
+
);
|
|
61
62
|
if (scheduled && scheduled.state.kind === "pending") {
|
|
62
63
|
await ctx.scheduler.cancel(runStatus.state.scheduledId);
|
|
63
64
|
} else {
|
|
@@ -68,21 +69,23 @@ export async function kickMainLoop(
|
|
|
68
69
|
} else if (runStatus.state.kind === "idle") {
|
|
69
70
|
console.debug(`[${source}] main was idle, so run it now`);
|
|
70
71
|
}
|
|
71
|
-
await ctx.db.patch(runStatus._id, {
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
await ctx.db.patch("runStatus", runStatus._id, {
|
|
73
|
+
state: { kind: "running" },
|
|
74
|
+
});
|
|
75
|
+
const scheduledTime = boundScheduledTime(
|
|
76
|
+
fromSegment(getCurrentSegment()),
|
|
77
|
+
console,
|
|
78
|
+
);
|
|
74
79
|
await ctx.scheduler.runAt(scheduledTime, internal.loop.main, {
|
|
75
80
|
generation: runStatus.state.generation,
|
|
76
|
-
segment: current,
|
|
77
81
|
});
|
|
78
|
-
return current;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
export const forceKick = internalMutation({
|
|
82
85
|
args: {},
|
|
83
86
|
handler: async (ctx) => {
|
|
84
87
|
const runStatus = await getOrCreateRunStatus(ctx);
|
|
85
|
-
await ctx.db.delete(runStatus._id);
|
|
88
|
+
await ctx.db.delete("runStatus", runStatus._id);
|
|
86
89
|
await kickMainLoop(ctx, "kick");
|
|
87
90
|
},
|
|
88
91
|
});
|
|
@@ -97,7 +100,7 @@ async function getOrCreateRunStatus(ctx: MutationCtx) {
|
|
|
97
100
|
generation: state?.generation ?? INITIAL_STATE.generation,
|
|
98
101
|
},
|
|
99
102
|
});
|
|
100
|
-
runStatus = (await ctx.db.get(id))!;
|
|
103
|
+
runStatus = (await ctx.db.get("runStatus", id))!;
|
|
101
104
|
if (!state) {
|
|
102
105
|
await ctx.db.insert("internalState", INITIAL_STATE);
|
|
103
106
|
}
|
|
@@ -165,7 +165,7 @@ describe("lib", () => {
|
|
|
165
165
|
|
|
166
166
|
// Delete the work item
|
|
167
167
|
await t.run(async (ctx) => {
|
|
168
|
-
await ctx.db.delete(id);
|
|
168
|
+
await ctx.db.delete("work", id);
|
|
169
169
|
});
|
|
170
170
|
|
|
171
171
|
// Try to cancel the deleted work
|
|
@@ -281,7 +281,7 @@ describe("lib", () => {
|
|
|
281
281
|
},
|
|
282
282
|
});
|
|
283
283
|
await t.run(async (ctx) => {
|
|
284
|
-
await ctx.db.delete(id);
|
|
284
|
+
await ctx.db.delete("work", id);
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
const status = await t.query(api.lib.status, { id });
|
|
@@ -303,7 +303,7 @@ describe("lib", () => {
|
|
|
303
303
|
|
|
304
304
|
// Verify work item and pending start were created
|
|
305
305
|
await t.run(async (ctx) => {
|
|
306
|
-
const work = await ctx.db.get(id);
|
|
306
|
+
const work = await ctx.db.get("work", id);
|
|
307
307
|
expect(work).toBeDefined();
|
|
308
308
|
const pendingStarts = await ctx.db.query("pendingStart").collect();
|
|
309
309
|
expect(pendingStarts).toHaveLength(1);
|
|
@@ -332,7 +332,7 @@ describe("lib", () => {
|
|
|
332
332
|
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
333
333
|
expect(pendingStart).toBeDefined();
|
|
334
334
|
assert(pendingStart);
|
|
335
|
-
await ctx.db.delete(pendingStart._id);
|
|
335
|
+
await ctx.db.delete("pendingStart", pendingStart._id);
|
|
336
336
|
});
|
|
337
337
|
|
|
338
338
|
const status = await t.query(api.lib.status, { id });
|
|
@@ -362,7 +362,7 @@ describe("lib", () => {
|
|
|
362
362
|
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
363
363
|
expect(pendingStart).toBeDefined();
|
|
364
364
|
assert(pendingStart);
|
|
365
|
-
await ctx.db.delete(pendingStart._id);
|
|
365
|
+
await ctx.db.delete("pendingStart", pendingStart._id);
|
|
366
366
|
|
|
367
367
|
// Create a pendingCompletion with retry=true to simulate a failed job that will be retried
|
|
368
368
|
await ctx.db.insert("pendingCompletion", {
|
|
@@ -395,7 +395,7 @@ describe("lib", () => {
|
|
|
395
395
|
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
396
396
|
expect(pendingStart).toBeDefined();
|
|
397
397
|
assert(pendingStart);
|
|
398
|
-
await ctx.db.delete(pendingStart._id);
|
|
398
|
+
await ctx.db.delete("pendingStart", pendingStart._id);
|
|
399
399
|
|
|
400
400
|
// Create a pendingCancelation
|
|
401
401
|
await ctx.db.insert("pendingCancelation", {
|
|
@@ -428,7 +428,7 @@ describe("lib", () => {
|
|
|
428
428
|
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
429
429
|
expect(pendingStart).toBeDefined();
|
|
430
430
|
assert(pendingStart);
|
|
431
|
-
await ctx.db.delete(pendingStart._id);
|
|
431
|
+
await ctx.db.delete("pendingStart", pendingStart._id);
|
|
432
432
|
|
|
433
433
|
// Create a pendingCompletion with retry=false
|
|
434
434
|
await ctx.db.insert("pendingCompletion", {
|
package/src/component/lib.ts
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
boundScheduledTime,
|
|
20
20
|
vConfig,
|
|
21
21
|
fnType,
|
|
22
|
-
|
|
22
|
+
getCurrentSegment,
|
|
23
23
|
max,
|
|
24
24
|
vOnCompleteFnContext,
|
|
25
25
|
retryBehavior,
|
|
@@ -53,14 +53,13 @@ export const enqueue = mutation({
|
|
|
53
53
|
handler: async (ctx, { config, ...itemArgs }) => {
|
|
54
54
|
const globals = await getOrUpdateGlobals(ctx, config);
|
|
55
55
|
const console = createLogger(globals.logLevel);
|
|
56
|
-
|
|
57
|
-
return await enqueueHandler(ctx, console,
|
|
56
|
+
await kickMainLoop(ctx, "enqueue", globals);
|
|
57
|
+
return await enqueueHandler(ctx, console, itemArgs);
|
|
58
58
|
},
|
|
59
59
|
});
|
|
60
60
|
async function enqueueHandler(
|
|
61
61
|
ctx: MutationCtx,
|
|
62
62
|
console: Logger,
|
|
63
|
-
kickSegment: bigint,
|
|
64
63
|
{ runAt, ...workArgs }: ObjectType<typeof itemArgs>,
|
|
65
64
|
) {
|
|
66
65
|
runAt = boundScheduledTime(runAt, console);
|
|
@@ -115,7 +114,7 @@ async function enqueueHandler(
|
|
|
115
114
|
|
|
116
115
|
await ctx.db.insert("pendingStart", {
|
|
117
116
|
workId,
|
|
118
|
-
segment: max(toSegment(runAt),
|
|
117
|
+
segment: max(toSegment(runAt), getCurrentSegment()),
|
|
119
118
|
});
|
|
120
119
|
recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
|
|
121
120
|
return workId;
|
|
@@ -130,10 +129,8 @@ export const enqueueBatch = mutation({
|
|
|
130
129
|
handler: async (ctx, { config, items }) => {
|
|
131
130
|
const globals = await getOrUpdateGlobals(ctx, config);
|
|
132
131
|
const console = createLogger(globals.logLevel);
|
|
133
|
-
|
|
134
|
-
return Promise.all(
|
|
135
|
-
items.map((item) => enqueueHandler(ctx, console, kickSegment, item)),
|
|
136
|
-
);
|
|
132
|
+
await kickMainLoop(ctx, "enqueue", globals);
|
|
133
|
+
return Promise.all(items.map((item) => enqueueHandler(ctx, console, item)));
|
|
137
134
|
},
|
|
138
135
|
});
|
|
139
136
|
|
|
@@ -146,10 +143,10 @@ export const cancel = mutation({
|
|
|
146
143
|
const globals = await getOrUpdateGlobals(ctx, { logLevel });
|
|
147
144
|
const shouldCancel = await shouldCancelWorkItem(ctx, id, globals.logLevel);
|
|
148
145
|
if (shouldCancel) {
|
|
149
|
-
|
|
146
|
+
await kickMainLoop(ctx, "cancel", globals);
|
|
150
147
|
await ctx.db.insert("pendingCancelation", {
|
|
151
148
|
workId: id,
|
|
152
|
-
segment,
|
|
149
|
+
segment: getCurrentSegment(),
|
|
153
150
|
});
|
|
154
151
|
}
|
|
155
152
|
},
|
|
@@ -176,10 +173,10 @@ export const cancelAll = mutation({
|
|
|
176
173
|
shouldCancelWorkItem(ctx, _id, globals.logLevel),
|
|
177
174
|
),
|
|
178
175
|
);
|
|
179
|
-
let segment = getNextSegment();
|
|
180
176
|
if (shouldCancel.some((c) => c)) {
|
|
181
|
-
|
|
177
|
+
await kickMainLoop(ctx, "cancel", globals);
|
|
182
178
|
}
|
|
179
|
+
const segment = getCurrentSegment();
|
|
183
180
|
await Promise.all(
|
|
184
181
|
pageOfWork.map(({ _id }, index) => {
|
|
185
182
|
if (shouldCancel[index]) {
|
|
@@ -206,7 +203,7 @@ export const status = query({
|
|
|
206
203
|
handler: statusHandler,
|
|
207
204
|
});
|
|
208
205
|
async function statusHandler(ctx: QueryCtx, { id }: { id: Id<"work"> }) {
|
|
209
|
-
const work = await ctx.db.get(id);
|
|
206
|
+
const work = await ctx.db.get("work", id);
|
|
210
207
|
if (!work) {
|
|
211
208
|
return { state: "finished" } as const;
|
|
212
209
|
}
|
|
@@ -245,7 +242,7 @@ async function shouldCancelWorkItem(
|
|
|
245
242
|
) {
|
|
246
243
|
const console = createLogger(logLevel);
|
|
247
244
|
// No-op if the work doesn't exist or has completed.
|
|
248
|
-
const work = await ctx.db.get(workId);
|
|
245
|
+
const work = await ctx.db.get("work", workId);
|
|
249
246
|
if (!work) {
|
|
250
247
|
console.warn(`[cancel] work ${workId} doesn't exist`);
|
|
251
248
|
return false;
|