@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
|
@@ -8,13 +8,19 @@ import {
|
|
|
8
8
|
test,
|
|
9
9
|
vi,
|
|
10
10
|
} from "vitest";
|
|
11
|
-
import schema from "./schema.js";
|
|
12
|
-
import { modules } from "./setup.test.js";
|
|
13
|
-
import { DEFAULT_MAX_PARALLELISM, kickMainLoop } from "./kick.js";
|
|
14
|
-
import { DEFAULT_LOG_LEVEL } from "./logging.js";
|
|
15
11
|
import { internal } from "./_generated/api";
|
|
16
|
-
import { toSegment, fromSegment, nextSegment } from "./shared";
|
|
17
12
|
import { Id } from "./_generated/dataModel.js";
|
|
13
|
+
import { kickMainLoop } from "./kick.js";
|
|
14
|
+
import { DEFAULT_LOG_LEVEL } from "./logging.js";
|
|
15
|
+
import schema from "./schema.js";
|
|
16
|
+
import { modules } from "./setup.test.js";
|
|
17
|
+
import {
|
|
18
|
+
fromSegment,
|
|
19
|
+
getCurrentSegment,
|
|
20
|
+
getNextSegment,
|
|
21
|
+
toSegment,
|
|
22
|
+
} from "./shared";
|
|
23
|
+
import { DEFAULT_MAX_PARALLELISM } from "./shared.js";
|
|
18
24
|
|
|
19
25
|
describe("kickMainLoop", () => {
|
|
20
26
|
beforeEach(() => {
|
|
@@ -53,13 +59,13 @@ describe("kickMainLoop", () => {
|
|
|
53
59
|
expect(globals.logLevel).toBe(DEFAULT_LOG_LEVEL);
|
|
54
60
|
await kickMainLoop(ctx, "enqueue", {
|
|
55
61
|
maxParallelism: DEFAULT_MAX_PARALLELISM + 1,
|
|
56
|
-
logLevel: "
|
|
62
|
+
logLevel: "ERROR",
|
|
57
63
|
});
|
|
58
64
|
const after = await ctx.db.query("globals").unique();
|
|
59
65
|
expect(after).not.toBeNull();
|
|
60
66
|
assert(after);
|
|
61
67
|
expect(after.maxParallelism).toBe(DEFAULT_MAX_PARALLELISM + 1);
|
|
62
|
-
expect(after.logLevel).toBe("
|
|
68
|
+
expect(after.logLevel).toBe("ERROR");
|
|
63
69
|
});
|
|
64
70
|
});
|
|
65
71
|
|
|
@@ -73,11 +79,12 @@ describe("kickMainLoop", () => {
|
|
|
73
79
|
expect(runStatus.state.kind).toBe("running");
|
|
74
80
|
|
|
75
81
|
// Second kick should not change state
|
|
76
|
-
await kickMainLoop(ctx, "enqueue");
|
|
82
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
77
83
|
const afterStatus = await ctx.db.query("runStatus").unique();
|
|
78
84
|
assert(afterStatus);
|
|
79
85
|
expect(afterStatus.state.kind).toBe("running");
|
|
80
86
|
expect(afterStatus._id).toBe(runStatus._id);
|
|
87
|
+
expect(segment).toBe(getNextSegment());
|
|
81
88
|
});
|
|
82
89
|
});
|
|
83
90
|
|
|
@@ -114,7 +121,8 @@ describe("kickMainLoop", () => {
|
|
|
114
121
|
});
|
|
115
122
|
|
|
116
123
|
// Kick should reschedule to run sooner
|
|
117
|
-
await kickMainLoop(ctx, "enqueue");
|
|
124
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
125
|
+
expect(segment).toBe(getCurrentSegment());
|
|
118
126
|
|
|
119
127
|
const afterStatus = await ctx.db.query("runStatus").unique();
|
|
120
128
|
assert(afterStatus);
|
|
@@ -155,7 +163,8 @@ describe("kickMainLoop", () => {
|
|
|
155
163
|
});
|
|
156
164
|
|
|
157
165
|
// Kick should not change state when saturated
|
|
158
|
-
await kickMainLoop(ctx, "enqueue");
|
|
166
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
167
|
+
expect(segment).toBe(getNextSegment());
|
|
159
168
|
const afterStatus = await ctx.db.query("runStatus").unique();
|
|
160
169
|
assert(afterStatus);
|
|
161
170
|
expect(afterStatus.state.kind).toBe("scheduled");
|
|
@@ -176,7 +185,7 @@ describe("kickMainLoop", () => {
|
|
|
176
185
|
await ctx.db.delete(runStatus._id);
|
|
177
186
|
|
|
178
187
|
// Kick should recreate runStatus
|
|
179
|
-
await kickMainLoop(ctx, "
|
|
188
|
+
await kickMainLoop(ctx, "complete");
|
|
180
189
|
const newRunStatus = await ctx.db.query("runStatus").unique();
|
|
181
190
|
expect(newRunStatus).not.toBeNull();
|
|
182
191
|
assert(newRunStatus);
|
|
@@ -196,7 +205,7 @@ describe("kickMainLoop", () => {
|
|
|
196
205
|
await ctx.db.delete(globals._id);
|
|
197
206
|
|
|
198
207
|
// Kick should recreate globals
|
|
199
|
-
await kickMainLoop(ctx, "
|
|
208
|
+
await kickMainLoop(ctx, "complete");
|
|
200
209
|
const newGlobals = await ctx.db.query("globals").unique();
|
|
201
210
|
expect(newGlobals).not.toBeNull();
|
|
202
211
|
assert(newGlobals);
|
|
@@ -208,13 +217,15 @@ describe("kickMainLoop", () => {
|
|
|
208
217
|
test("handles race conditions between multiple kicks", async () => {
|
|
209
218
|
const t = convexTest(schema, modules);
|
|
210
219
|
// Run kicks in separate transactions to simulate concurrent access
|
|
211
|
-
await Promise.all(
|
|
220
|
+
const segments = await Promise.all(
|
|
212
221
|
Array.from({ length: 10 }, () =>
|
|
213
222
|
t.run(async (ctx) => {
|
|
214
|
-
await kickMainLoop(ctx, "enqueue");
|
|
223
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
224
|
+
return segment;
|
|
215
225
|
})
|
|
216
226
|
)
|
|
217
227
|
);
|
|
228
|
+
expect(segments.filter((s) => s === getCurrentSegment())).toHaveLength(1);
|
|
218
229
|
|
|
219
230
|
// Check final state in a new transaction
|
|
220
231
|
await t.run(async (ctx) => {
|
|
@@ -242,20 +253,19 @@ describe("kickMainLoop", () => {
|
|
|
242
253
|
// Initial kick with custom config
|
|
243
254
|
await kickMainLoop(ctx, "enqueue", {
|
|
244
255
|
maxParallelism: 5,
|
|
245
|
-
logLevel: "
|
|
256
|
+
logLevel: "ERROR",
|
|
246
257
|
});
|
|
247
258
|
|
|
248
259
|
// Kick from different sources
|
|
249
260
|
await kickMainLoop(ctx, "cancel");
|
|
250
|
-
await kickMainLoop(ctx, "
|
|
251
|
-
await kickMainLoop(ctx, "recovery");
|
|
261
|
+
await kickMainLoop(ctx, "complete");
|
|
252
262
|
|
|
253
263
|
// Config should be preserved
|
|
254
264
|
const globals = await ctx.db.query("globals").unique();
|
|
255
265
|
expect(globals).not.toBeNull();
|
|
256
266
|
assert(globals);
|
|
257
267
|
expect(globals.maxParallelism).toBe(5);
|
|
258
|
-
expect(globals.logLevel).toBe("
|
|
268
|
+
expect(globals.logLevel).toBe("ERROR");
|
|
259
269
|
});
|
|
260
270
|
});
|
|
261
271
|
|
|
@@ -265,7 +275,7 @@ describe("kickMainLoop", () => {
|
|
|
265
275
|
await kickMainLoop(ctx, "enqueue");
|
|
266
276
|
const runStatus = await ctx.db.query("runStatus").unique();
|
|
267
277
|
assert(runStatus);
|
|
268
|
-
const segment =
|
|
278
|
+
const segment = getNextSegment() + 10n;
|
|
269
279
|
await ctx.db.patch(runStatus._id, {
|
|
270
280
|
state: {
|
|
271
281
|
generation: 0n,
|
package/src/component/kick.ts
CHANGED
|
@@ -2,43 +2,49 @@ import { internal } from "./_generated/api.js";
|
|
|
2
2
|
import { internalMutation, MutationCtx } from "./_generated/server.js";
|
|
3
3
|
import { createLogger, DEFAULT_LOG_LEVEL } from "./logging.js";
|
|
4
4
|
import { INITIAL_STATE } from "./loop.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
boundScheduledTime,
|
|
7
|
+
Config,
|
|
8
|
+
DEFAULT_MAX_PARALLELISM,
|
|
9
|
+
fromSegment,
|
|
10
|
+
getCurrentSegment,
|
|
11
|
+
getNextSegment,
|
|
12
|
+
} from "./shared.js";
|
|
6
13
|
|
|
7
|
-
export const DEFAULT_MAX_PARALLELISM = 10;
|
|
8
14
|
/**
|
|
9
|
-
* Called from outside the loop
|
|
15
|
+
* Called from outside the loop.
|
|
16
|
+
* Returns the soonest segment to enqueue work for the main loop.
|
|
10
17
|
*/
|
|
11
|
-
|
|
12
18
|
export async function kickMainLoop(
|
|
13
19
|
ctx: MutationCtx,
|
|
14
|
-
source: "enqueue" | "cancel" | "
|
|
20
|
+
source: "enqueue" | "cancel" | "complete",
|
|
15
21
|
config?: Partial<Config>
|
|
16
|
-
): Promise<
|
|
22
|
+
): Promise<bigint> {
|
|
17
23
|
const globals = await getOrUpdateGlobals(ctx, config);
|
|
18
24
|
const console = createLogger(globals.logLevel);
|
|
19
25
|
const runStatus = await getOrCreateRunStatus(ctx);
|
|
26
|
+
const next = getNextSegment();
|
|
20
27
|
|
|
21
28
|
// Only kick to run now if we're scheduled or idle.
|
|
22
29
|
if (runStatus.state.kind === "running") {
|
|
23
30
|
console.debug(
|
|
24
31
|
`[${source}] main is actively running, so we don't need to kick it`
|
|
25
32
|
);
|
|
26
|
-
return;
|
|
33
|
+
return next;
|
|
27
34
|
}
|
|
28
|
-
const segment = nextSegment();
|
|
29
35
|
// main is scheduled to run later, so we should cancel it and reschedule.
|
|
30
36
|
if (runStatus.state.kind === "scheduled") {
|
|
31
37
|
if (source === "enqueue" && runStatus.state.saturated) {
|
|
32
38
|
console.debug(
|
|
33
39
|
`[${source}] main is saturated, so we don't need to kick it`
|
|
34
40
|
);
|
|
35
|
-
return;
|
|
41
|
+
return next;
|
|
36
42
|
}
|
|
37
|
-
if (runStatus.state.segment <=
|
|
43
|
+
if (runStatus.state.segment <= next) {
|
|
38
44
|
console.debug(
|
|
39
45
|
`[${source}] main is scheduled to run soon enough, so we don't need to kick it`
|
|
40
46
|
);
|
|
41
|
-
return;
|
|
47
|
+
return next;
|
|
42
48
|
}
|
|
43
49
|
console.debug(
|
|
44
50
|
`[${source}] main is scheduled to run later, so reschedule it to run now`
|
|
@@ -51,15 +57,17 @@ export async function kickMainLoop(
|
|
|
51
57
|
`[${source}] main is marked as scheduled, but it's status is ${scheduled?.state.kind}`
|
|
52
58
|
);
|
|
53
59
|
}
|
|
60
|
+
} else if (runStatus.state.kind === "idle") {
|
|
61
|
+
console.debug(`[${source}] main was idle, so run it now`);
|
|
54
62
|
}
|
|
55
|
-
console.debug(
|
|
56
|
-
`[${source}] main was scheduled later, so reschedule it to run now`
|
|
57
|
-
);
|
|
58
63
|
await ctx.db.patch(runStatus._id, { state: { kind: "running" } });
|
|
59
|
-
|
|
64
|
+
const current = getCurrentSegment();
|
|
65
|
+
const scheduledTime = boundScheduledTime(fromSegment(current), console);
|
|
66
|
+
await ctx.scheduler.runAt(scheduledTime, internal.loop.main, {
|
|
60
67
|
generation: runStatus.state.generation,
|
|
61
|
-
segment,
|
|
68
|
+
segment: current,
|
|
62
69
|
});
|
|
70
|
+
return current;
|
|
63
71
|
}
|
|
64
72
|
|
|
65
73
|
export const forceKick = internalMutation({
|
|
@@ -67,7 +75,7 @@ export const forceKick = internalMutation({
|
|
|
67
75
|
handler: async (ctx) => {
|
|
68
76
|
const runStatus = await getOrCreateRunStatus(ctx);
|
|
69
77
|
await ctx.db.delete(runStatus._id);
|
|
70
|
-
await kickMainLoop(ctx, "
|
|
78
|
+
await kickMainLoop(ctx, "complete");
|
|
71
79
|
},
|
|
72
80
|
});
|
|
73
81
|
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { convexTest } from "convex-test";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
describe,
|
|
4
|
+
expect,
|
|
5
|
+
it,
|
|
6
|
+
beforeEach,
|
|
7
|
+
afterEach,
|
|
8
|
+
vi,
|
|
9
|
+
assert,
|
|
10
|
+
} from "vitest";
|
|
3
11
|
import { Id } from "./_generated/dataModel";
|
|
4
12
|
import schema from "./schema";
|
|
5
13
|
import { api } from "./_generated/api";
|
|
@@ -36,13 +44,13 @@ describe("lib", () => {
|
|
|
36
44
|
runAt: Date.now(),
|
|
37
45
|
config: {
|
|
38
46
|
maxParallelism: 10,
|
|
39
|
-
logLevel: "
|
|
47
|
+
logLevel: "WARN",
|
|
40
48
|
},
|
|
41
49
|
});
|
|
42
50
|
|
|
43
51
|
expect(id).toBeDefined();
|
|
44
52
|
const status = await t.query(api.lib.status, { id });
|
|
45
|
-
expect(status).toEqual({ state: "pending",
|
|
53
|
+
expect(status).toEqual({ state: "pending", previousAttempts: 0 });
|
|
46
54
|
});
|
|
47
55
|
|
|
48
56
|
it("should throw error if maxParallelism is too high", async () => {
|
|
@@ -55,11 +63,27 @@ describe("lib", () => {
|
|
|
55
63
|
runAt: Date.now(),
|
|
56
64
|
config: {
|
|
57
65
|
maxParallelism: 101, // More than MAX_POSSIBLE_PARALLELISM
|
|
58
|
-
logLevel: "
|
|
66
|
+
logLevel: "WARN",
|
|
59
67
|
},
|
|
60
68
|
})
|
|
61
69
|
).rejects.toThrow("maxParallelism must be <= 100");
|
|
62
70
|
});
|
|
71
|
+
|
|
72
|
+
it("should throw error if maxParallelism is too low", async () => {
|
|
73
|
+
await expect(
|
|
74
|
+
t.mutation(api.lib.enqueue, {
|
|
75
|
+
fnHandle: "testHandle",
|
|
76
|
+
fnName: "testFunction",
|
|
77
|
+
fnArgs: { test: true },
|
|
78
|
+
fnType: "mutation",
|
|
79
|
+
runAt: Date.now(),
|
|
80
|
+
config: {
|
|
81
|
+
maxParallelism: 0, // Less than minimum
|
|
82
|
+
logLevel: "WARN",
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
).rejects.toThrow("maxParallelism must be >= 1");
|
|
86
|
+
});
|
|
63
87
|
});
|
|
64
88
|
|
|
65
89
|
describe("cancel", () => {
|
|
@@ -72,13 +96,13 @@ describe("lib", () => {
|
|
|
72
96
|
runAt: Date.now(),
|
|
73
97
|
config: {
|
|
74
98
|
maxParallelism: 10,
|
|
75
|
-
logLevel: "
|
|
99
|
+
logLevel: "WARN",
|
|
76
100
|
},
|
|
77
101
|
});
|
|
78
102
|
|
|
79
103
|
await t.mutation(api.lib.cancel, {
|
|
80
104
|
id,
|
|
81
|
-
logLevel: "
|
|
105
|
+
logLevel: "WARN",
|
|
82
106
|
});
|
|
83
107
|
|
|
84
108
|
// Verify a pending cancelation was created
|
|
@@ -90,6 +114,74 @@ describe("lib", () => {
|
|
|
90
114
|
expect(pendingCancelations[0].workId).toBe(id);
|
|
91
115
|
});
|
|
92
116
|
});
|
|
117
|
+
|
|
118
|
+
it("should not create duplicate cancelation requests", async () => {
|
|
119
|
+
const id = await t.mutation(api.lib.enqueue, {
|
|
120
|
+
fnHandle: "testHandle",
|
|
121
|
+
fnName: "testFunction",
|
|
122
|
+
fnArgs: { test: true },
|
|
123
|
+
fnType: "mutation",
|
|
124
|
+
runAt: Date.now(),
|
|
125
|
+
config: {
|
|
126
|
+
maxParallelism: 10,
|
|
127
|
+
logLevel: "WARN",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
// Cancel the first time
|
|
132
|
+
await t.mutation(api.lib.cancel, {
|
|
133
|
+
id,
|
|
134
|
+
logLevel: "WARN",
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Cancel the second time
|
|
138
|
+
await t.mutation(api.lib.cancel, {
|
|
139
|
+
id,
|
|
140
|
+
logLevel: "WARN",
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Verify only one pending cancelation was created
|
|
144
|
+
await t.run(async (ctx) => {
|
|
145
|
+
const pendingCancelations = await ctx.db
|
|
146
|
+
.query("pendingCancelation")
|
|
147
|
+
.collect();
|
|
148
|
+
expect(pendingCancelations).toHaveLength(1);
|
|
149
|
+
expect(pendingCancelations[0].workId).toBe(id);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("should not create cancelation for non-existent work", async () => {
|
|
154
|
+
const id = await t.mutation(api.lib.enqueue, {
|
|
155
|
+
fnHandle: "testHandle",
|
|
156
|
+
fnName: "testFunction",
|
|
157
|
+
fnArgs: { test: true },
|
|
158
|
+
fnType: "mutation",
|
|
159
|
+
runAt: Date.now(),
|
|
160
|
+
config: {
|
|
161
|
+
maxParallelism: 10,
|
|
162
|
+
logLevel: "WARN",
|
|
163
|
+
},
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Delete the work item
|
|
167
|
+
await t.run(async (ctx) => {
|
|
168
|
+
await ctx.db.delete(id);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// Try to cancel the deleted work
|
|
172
|
+
await t.mutation(api.lib.cancel, {
|
|
173
|
+
id,
|
|
174
|
+
logLevel: "WARN",
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// Verify no pending cancelation was created
|
|
178
|
+
await t.run(async (ctx) => {
|
|
179
|
+
const pendingCancelations = await ctx.db
|
|
180
|
+
.query("pendingCancelation")
|
|
181
|
+
.collect();
|
|
182
|
+
expect(pendingCancelations).toHaveLength(0);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
93
185
|
});
|
|
94
186
|
|
|
95
187
|
describe("cancelAll", () => {
|
|
@@ -101,17 +193,17 @@ describe("lib", () => {
|
|
|
101
193
|
fnName: "testFunction",
|
|
102
194
|
fnArgs: { test: i },
|
|
103
195
|
fnType: "mutation",
|
|
104
|
-
runAt: Date.now(),
|
|
196
|
+
runAt: Date.now() + 5 * 60 * 1000,
|
|
105
197
|
config: {
|
|
106
198
|
maxParallelism: 10,
|
|
107
|
-
logLevel: "
|
|
199
|
+
logLevel: "WARN",
|
|
108
200
|
},
|
|
109
201
|
});
|
|
110
202
|
ids.push(id);
|
|
111
203
|
}
|
|
112
204
|
|
|
113
205
|
await t.mutation(api.lib.cancelAll, {
|
|
114
|
-
logLevel: "
|
|
206
|
+
logLevel: "WARN",
|
|
115
207
|
before: Date.now() + 1000,
|
|
116
208
|
});
|
|
117
209
|
|
|
@@ -125,6 +217,54 @@ describe("lib", () => {
|
|
|
125
217
|
expect(canceledIds).toEqual(expect.arrayContaining(ids));
|
|
126
218
|
});
|
|
127
219
|
});
|
|
220
|
+
|
|
221
|
+
it("should process work items in batches for cancelAll", async () => {
|
|
222
|
+
const PAGE_SIZE = 64; // Same as in lib.ts
|
|
223
|
+
|
|
224
|
+
// Create PAGE_SIZE + 1 work items to trigger pagination
|
|
225
|
+
for (let i = 0; i < PAGE_SIZE + 1; i++) {
|
|
226
|
+
await t.mutation(api.lib.enqueue, {
|
|
227
|
+
fnHandle: "testHandle",
|
|
228
|
+
fnName: "testFunction",
|
|
229
|
+
fnArgs: { test: i },
|
|
230
|
+
fnType: "mutation",
|
|
231
|
+
runAt: Date.now(),
|
|
232
|
+
config: {
|
|
233
|
+
maxParallelism: 10,
|
|
234
|
+
logLevel: "WARN",
|
|
235
|
+
},
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
await t.mutation(api.lib.cancelAll, {
|
|
240
|
+
logLevel: "WARN",
|
|
241
|
+
before: Date.now() + 1000,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// assert that cancelAll was scheduled
|
|
245
|
+
await t.run(async (ctx) => {
|
|
246
|
+
const scheduledFunctions = await ctx.db.system
|
|
247
|
+
.query("_scheduled_functions")
|
|
248
|
+
.collect();
|
|
249
|
+
expect(scheduledFunctions.length).toBeGreaterThan(0);
|
|
250
|
+
// check that one of the scheduled functions is cancelAll
|
|
251
|
+
const cancelAllScheduledFunction = scheduledFunctions.find(
|
|
252
|
+
(sf) => sf.name === "lib:cancelAll"
|
|
253
|
+
);
|
|
254
|
+
expect(cancelAllScheduledFunction).toBeDefined();
|
|
255
|
+
assert(cancelAllScheduledFunction);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// Verify the first page of cancelations was created
|
|
259
|
+
await t.run(async (ctx) => {
|
|
260
|
+
const pendingCancelations = await ctx.db
|
|
261
|
+
.query("pendingCancelation")
|
|
262
|
+
.collect();
|
|
263
|
+
|
|
264
|
+
// We should have at least PAGE_SIZE cancelations
|
|
265
|
+
expect(pendingCancelations.length).toEqual(PAGE_SIZE);
|
|
266
|
+
});
|
|
267
|
+
});
|
|
128
268
|
});
|
|
129
269
|
|
|
130
270
|
describe("status", () => {
|
|
@@ -137,7 +277,7 @@ describe("lib", () => {
|
|
|
137
277
|
runAt: Date.now(),
|
|
138
278
|
config: {
|
|
139
279
|
maxParallelism: 10,
|
|
140
|
-
logLevel: "
|
|
280
|
+
logLevel: "WARN",
|
|
141
281
|
},
|
|
142
282
|
});
|
|
143
283
|
await t.run(async (ctx) => {
|
|
@@ -157,7 +297,7 @@ describe("lib", () => {
|
|
|
157
297
|
runAt: Date.now(),
|
|
158
298
|
config: {
|
|
159
299
|
maxParallelism: 10,
|
|
160
|
-
logLevel: "
|
|
300
|
+
logLevel: "WARN",
|
|
161
301
|
},
|
|
162
302
|
});
|
|
163
303
|
|
|
@@ -171,7 +311,7 @@ describe("lib", () => {
|
|
|
171
311
|
});
|
|
172
312
|
|
|
173
313
|
const status = await t.query(api.lib.status, { id });
|
|
174
|
-
expect(status).toEqual({ state: "pending",
|
|
314
|
+
expect(status).toEqual({ state: "pending", previousAttempts: 0 });
|
|
175
315
|
});
|
|
176
316
|
|
|
177
317
|
it("should return running state when work is in progress", async () => {
|
|
@@ -183,7 +323,37 @@ describe("lib", () => {
|
|
|
183
323
|
runAt: Date.now(),
|
|
184
324
|
config: {
|
|
185
325
|
maxParallelism: 10,
|
|
186
|
-
logLevel: "
|
|
326
|
+
logLevel: "WARN",
|
|
327
|
+
},
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// Delete the pendingStart to simulate work in progress
|
|
331
|
+
await t.run(async (ctx) => {
|
|
332
|
+
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
333
|
+
expect(pendingStart).toBeDefined();
|
|
334
|
+
assert(pendingStart);
|
|
335
|
+
await ctx.db.delete(pendingStart._id);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
const status = await t.query(api.lib.status, { id });
|
|
339
|
+
expect(status).toEqual({ state: "running", previousAttempts: 0 });
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
it("should return pending state for work pending retry", async () => {
|
|
343
|
+
const id = await t.mutation(api.lib.enqueue, {
|
|
344
|
+
fnHandle: "testHandle",
|
|
345
|
+
fnName: "testFunction",
|
|
346
|
+
fnArgs: { test: true },
|
|
347
|
+
fnType: "mutation",
|
|
348
|
+
runAt: Date.now(),
|
|
349
|
+
retryBehavior: {
|
|
350
|
+
maxAttempts: 3,
|
|
351
|
+
initialBackoffMs: 100,
|
|
352
|
+
base: 2,
|
|
353
|
+
},
|
|
354
|
+
config: {
|
|
355
|
+
maxParallelism: 10,
|
|
356
|
+
logLevel: "WARN",
|
|
187
357
|
},
|
|
188
358
|
});
|
|
189
359
|
|
|
@@ -191,13 +361,88 @@ describe("lib", () => {
|
|
|
191
361
|
await t.run(async (ctx) => {
|
|
192
362
|
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
193
363
|
expect(pendingStart).toBeDefined();
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
364
|
+
assert(pendingStart);
|
|
365
|
+
await ctx.db.delete(pendingStart._id);
|
|
366
|
+
|
|
367
|
+
// Create a pendingCompletion with retry=true to simulate a failed job that will be retried
|
|
368
|
+
await ctx.db.insert("pendingCompletion", {
|
|
369
|
+
workId: id,
|
|
370
|
+
segment: 1n, // Using a simple segment value for testing
|
|
371
|
+
runResult: { kind: "failed", error: "Test error" },
|
|
372
|
+
retry: true,
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
const status = await t.query(api.lib.status, { id });
|
|
377
|
+
expect(status).toEqual({ state: "pending", previousAttempts: 0 });
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
it("should return running state for work with pendingCancelation", async () => {
|
|
381
|
+
const id = await t.mutation(api.lib.enqueue, {
|
|
382
|
+
fnHandle: "testHandle",
|
|
383
|
+
fnName: "testFunction",
|
|
384
|
+
fnArgs: { test: true },
|
|
385
|
+
fnType: "mutation",
|
|
386
|
+
runAt: Date.now(),
|
|
387
|
+
config: {
|
|
388
|
+
maxParallelism: 10,
|
|
389
|
+
logLevel: "WARN",
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Delete the pendingStart and add pendingCancelation to simulate cancellation in progress
|
|
394
|
+
await t.run(async (ctx) => {
|
|
395
|
+
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
396
|
+
expect(pendingStart).toBeDefined();
|
|
397
|
+
assert(pendingStart);
|
|
398
|
+
await ctx.db.delete(pendingStart._id);
|
|
399
|
+
|
|
400
|
+
// Create a pendingCancelation
|
|
401
|
+
await ctx.db.insert("pendingCancelation", {
|
|
402
|
+
workId: id,
|
|
403
|
+
segment: 1n, // Using a simple segment value for testing
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
// According to the implementation, a job with pendingCancelation but no pendingStart
|
|
408
|
+
// or pendingCompletion with retry=true is considered "running"
|
|
409
|
+
const status = await t.query(api.lib.status, { id });
|
|
410
|
+
expect(status).toEqual({ state: "running", previousAttempts: 0 });
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
it("should return running state for work with pendingCompletion but retry=false", async () => {
|
|
414
|
+
const id = await t.mutation(api.lib.enqueue, {
|
|
415
|
+
fnHandle: "testHandle",
|
|
416
|
+
fnName: "testFunction",
|
|
417
|
+
fnArgs: { test: true },
|
|
418
|
+
fnType: "mutation",
|
|
419
|
+
runAt: Date.now(),
|
|
420
|
+
config: {
|
|
421
|
+
maxParallelism: 10,
|
|
422
|
+
logLevel: "WARN",
|
|
423
|
+
},
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// Delete the pendingStart and add pendingCompletion with retry=false
|
|
427
|
+
await t.run(async (ctx) => {
|
|
428
|
+
const pendingStart = await ctx.db.query("pendingStart").first();
|
|
429
|
+
expect(pendingStart).toBeDefined();
|
|
430
|
+
assert(pendingStart);
|
|
431
|
+
await ctx.db.delete(pendingStart._id);
|
|
432
|
+
|
|
433
|
+
// Create a pendingCompletion with retry=false
|
|
434
|
+
await ctx.db.insert("pendingCompletion", {
|
|
435
|
+
workId: id,
|
|
436
|
+
segment: 1n, // Using a simple segment value for testing
|
|
437
|
+
runResult: { kind: "failed", error: "Test error" },
|
|
438
|
+
retry: false,
|
|
439
|
+
});
|
|
197
440
|
});
|
|
198
441
|
|
|
442
|
+
// According to the implementation, a job with pendingCompletion but retry=false
|
|
443
|
+
// is considered "running"
|
|
199
444
|
const status = await t.query(api.lib.status, { id });
|
|
200
|
-
expect(status).toEqual({ state: "running",
|
|
445
|
+
expect(status).toEqual({ state: "running", previousAttempts: 0 });
|
|
201
446
|
});
|
|
202
447
|
});
|
|
203
448
|
});
|