@convex-dev/workpool 0.2.0-beta.0 → 0.2.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 +7 -16
- package/dist/commonjs/client/index.d.ts +3 -3
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +10 -5
- 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 +80 -0
- package/dist/commonjs/component/complete.js.map +1 -0
- package/dist/commonjs/component/kick.d.ts +1 -2
- package/dist/commonjs/component/kick.d.ts.map +1 -1
- package/dist/commonjs/component/kick.js +7 -5
- package/dist/commonjs/component/kick.js.map +1 -1
- package/dist/commonjs/component/lib.d.ts +3 -3
- package/dist/commonjs/component/lib.d.ts.map +1 -1
- package/dist/commonjs/component/lib.js +43 -20
- package/dist/commonjs/component/lib.js.map +1 -1
- package/dist/commonjs/component/logging.d.ts.map +1 -1
- package/dist/commonjs/component/logging.js +1 -2
- 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 +215 -178
- package/dist/commonjs/component/loop.js.map +1 -1
- package/dist/commonjs/component/recovery.d.ts +16 -0
- package/dist/commonjs/component/recovery.d.ts.map +1 -1
- package/dist/commonjs/component/recovery.js +64 -44
- package/dist/commonjs/component/recovery.js.map +1 -1
- package/dist/commonjs/component/schema.d.ts +6 -2
- 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 +20 -11
- package/dist/commonjs/component/shared.d.ts.map +1 -1
- package/dist/commonjs/component/shared.js +18 -5
- package/dist/commonjs/component/shared.js.map +1 -1
- package/dist/commonjs/component/stats.d.ts +21 -13
- package/dist/commonjs/component/stats.d.ts.map +1 -1
- package/dist/commonjs/component/stats.js +32 -22
- package/dist/commonjs/component/stats.js.map +1 -1
- package/dist/commonjs/component/worker.d.ts +2 -12
- 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 +3 -3
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +10 -5
- 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 +80 -0
- package/dist/esm/component/complete.js.map +1 -0
- package/dist/esm/component/kick.d.ts +1 -2
- package/dist/esm/component/kick.d.ts.map +1 -1
- package/dist/esm/component/kick.js +7 -5
- package/dist/esm/component/kick.js.map +1 -1
- package/dist/esm/component/lib.d.ts +3 -3
- package/dist/esm/component/lib.d.ts.map +1 -1
- package/dist/esm/component/lib.js +43 -20
- package/dist/esm/component/lib.js.map +1 -1
- package/dist/esm/component/logging.d.ts.map +1 -1
- package/dist/esm/component/logging.js +1 -2
- 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 +215 -178
- package/dist/esm/component/loop.js.map +1 -1
- package/dist/esm/component/recovery.d.ts +16 -0
- package/dist/esm/component/recovery.d.ts.map +1 -1
- package/dist/esm/component/recovery.js +64 -44
- package/dist/esm/component/recovery.js.map +1 -1
- package/dist/esm/component/schema.d.ts +6 -2
- 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 +20 -11
- package/dist/esm/component/shared.d.ts.map +1 -1
- package/dist/esm/component/shared.js +18 -5
- package/dist/esm/component/shared.js.map +1 -1
- package/dist/esm/component/stats.d.ts +21 -13
- package/dist/esm/component/stats.d.ts.map +1 -1
- package/dist/esm/component/stats.js +32 -22
- package/dist/esm/component/stats.js.map +1 -1
- package/dist/esm/component/worker.d.ts +2 -12
- 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 +7 -6
- package/src/client/index.ts +18 -8
- package/src/component/README.md +15 -15
- package/src/component/_generated/api.d.ts +7 -2
- package/src/component/complete.test.ts +508 -0
- package/src/component/complete.ts +98 -0
- package/src/component/kick.test.ts +13 -13
- package/src/component/kick.ts +13 -8
- package/src/component/lib.test.ts +262 -17
- package/src/component/lib.ts +55 -24
- package/src/component/logging.ts +1 -2
- package/src/component/loop.test.ts +1158 -0
- package/src/component/loop.ts +289 -221
- package/src/component/recovery.test.ts +541 -0
- package/src/component/recovery.ts +80 -63
- package/src/component/schema.ts +6 -4
- package/src/component/shared.ts +21 -6
- package/src/component/stats.ts +48 -25
- package/src/component/worker.ts +25 -38
package/src/component/kick.ts
CHANGED
|
@@ -2,16 +2,21 @@ 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
|
+
nextSegment,
|
|
11
|
+
} from "./shared.js";
|
|
6
12
|
|
|
7
|
-
export const DEFAULT_MAX_PARALLELISM = 10;
|
|
8
13
|
/**
|
|
9
14
|
* Called from outside the loop:
|
|
10
15
|
*/
|
|
11
16
|
|
|
12
17
|
export async function kickMainLoop(
|
|
13
18
|
ctx: MutationCtx,
|
|
14
|
-
source: "enqueue" | "cancel" | "
|
|
19
|
+
source: "enqueue" | "cancel" | "complete",
|
|
15
20
|
config?: Partial<Config>
|
|
16
21
|
): Promise<void> {
|
|
17
22
|
const globals = await getOrUpdateGlobals(ctx, config);
|
|
@@ -51,12 +56,12 @@ export async function kickMainLoop(
|
|
|
51
56
|
`[${source}] main is marked as scheduled, but it's status is ${scheduled?.state.kind}`
|
|
52
57
|
);
|
|
53
58
|
}
|
|
59
|
+
} else if (runStatus.state.kind === "idle") {
|
|
60
|
+
console.debug(`[${source}] main was idle, so run it now`);
|
|
54
61
|
}
|
|
55
|
-
console.debug(
|
|
56
|
-
`[${source}] main was scheduled later, so reschedule it to run now`
|
|
57
|
-
);
|
|
58
62
|
await ctx.db.patch(runStatus._id, { state: { kind: "running" } });
|
|
59
|
-
|
|
63
|
+
const scheduledTime = boundScheduledTime(fromSegment(segment), console);
|
|
64
|
+
await ctx.scheduler.runAt(scheduledTime, internal.loop.main, {
|
|
60
65
|
generation: runStatus.state.generation,
|
|
61
66
|
segment,
|
|
62
67
|
});
|
|
@@ -67,7 +72,7 @@ export const forceKick = internalMutation({
|
|
|
67
72
|
handler: async (ctx) => {
|
|
68
73
|
const runStatus = await getOrCreateRunStatus(ctx);
|
|
69
74
|
await ctx.db.delete(runStatus._id);
|
|
70
|
-
await kickMainLoop(ctx, "
|
|
75
|
+
await kickMainLoop(ctx, "complete");
|
|
71
76
|
},
|
|
72
77
|
});
|
|
73
78
|
|
|
@@ -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
|
});
|
package/src/component/lib.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { mutation, query } from "./_generated/server.js";
|
|
2
|
+
import { mutation, MutationCtx, query } from "./_generated/server.js";
|
|
3
3
|
import {
|
|
4
4
|
nextSegment,
|
|
5
5
|
onComplete,
|
|
@@ -8,11 +8,14 @@ import {
|
|
|
8
8
|
status as statusValidator,
|
|
9
9
|
toSegment,
|
|
10
10
|
boundScheduledTime,
|
|
11
|
+
max,
|
|
11
12
|
} from "./shared.js";
|
|
12
|
-
import { logLevel } from "./logging.js";
|
|
13
|
+
import { LogLevel, logLevel } from "./logging.js";
|
|
13
14
|
import { kickMainLoop } from "./kick.js";
|
|
14
15
|
import { api } from "./_generated/api.js";
|
|
15
16
|
import { createLogger } from "./logging.js";
|
|
17
|
+
import { Id } from "./_generated/dataModel.js";
|
|
18
|
+
import { recordEnqueued } from "./stats.js";
|
|
16
19
|
|
|
17
20
|
const MAX_POSSIBLE_PARALLELISM = 100;
|
|
18
21
|
|
|
@@ -44,10 +47,10 @@ export const enqueue = mutation({
|
|
|
44
47
|
});
|
|
45
48
|
await ctx.db.insert("pendingStart", {
|
|
46
49
|
workId,
|
|
47
|
-
segment: toSegment(runAt),
|
|
50
|
+
segment: max(toSegment(runAt), nextSegment()),
|
|
48
51
|
});
|
|
49
52
|
await kickMainLoop(ctx, "enqueue", config);
|
|
50
|
-
|
|
53
|
+
recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
|
|
51
54
|
return workId;
|
|
52
55
|
},
|
|
53
56
|
});
|
|
@@ -58,11 +61,10 @@ export const cancel = mutation({
|
|
|
58
61
|
logLevel,
|
|
59
62
|
},
|
|
60
63
|
handler: async (ctx, { id, logLevel }) => {
|
|
61
|
-
await ctx
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
65
|
-
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
64
|
+
const canceled = await cancelWorkItem(ctx, id, nextSegment(), logLevel);
|
|
65
|
+
if (canceled) {
|
|
66
|
+
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
67
|
+
}
|
|
66
68
|
// TODO: stats event
|
|
67
69
|
},
|
|
68
70
|
});
|
|
@@ -78,26 +80,20 @@ export const cancelAll = mutation({
|
|
|
78
80
|
.withIndex("by_creation_time", (q) => q.lte("_creationTime", beforeTime))
|
|
79
81
|
.order("desc")
|
|
80
82
|
.take(PAGE_SIZE);
|
|
81
|
-
await Promise.all(
|
|
82
|
-
pageOfWork.map(async ({ _id }) =>
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
.query("pendingCancelation")
|
|
86
|
-
.withIndex("workId", (q) => q.eq("workId", _id))
|
|
87
|
-
.first()
|
|
88
|
-
) {
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
await ctx.db.insert("pendingCancelation", { workId: _id, segment });
|
|
92
|
-
})
|
|
83
|
+
const canceled = await Promise.all(
|
|
84
|
+
pageOfWork.map(async ({ _id }) =>
|
|
85
|
+
cancelWorkItem(ctx, _id, segment, logLevel)
|
|
86
|
+
)
|
|
93
87
|
);
|
|
88
|
+
if (canceled.some((c) => c)) {
|
|
89
|
+
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
90
|
+
}
|
|
94
91
|
if (pageOfWork.length === PAGE_SIZE) {
|
|
95
92
|
await ctx.scheduler.runAfter(0, api.lib.cancelAll, {
|
|
96
93
|
logLevel,
|
|
97
94
|
before: pageOfWork[pageOfWork.length - 1]._creationTime,
|
|
98
95
|
});
|
|
99
96
|
}
|
|
100
|
-
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
101
97
|
},
|
|
102
98
|
});
|
|
103
99
|
|
|
@@ -114,12 +110,47 @@ export const status = query({
|
|
|
114
110
|
.withIndex("workId", (q) => q.eq("workId", id))
|
|
115
111
|
.unique();
|
|
116
112
|
if (pendingStart) {
|
|
117
|
-
return { state: "pending",
|
|
113
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
114
|
+
}
|
|
115
|
+
const pendingCompletion = await ctx.db
|
|
116
|
+
.query("pendingCompletion")
|
|
117
|
+
.withIndex("workId", (q) => q.eq("workId", id))
|
|
118
|
+
.unique();
|
|
119
|
+
if (pendingCompletion?.retry) {
|
|
120
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
118
121
|
}
|
|
119
122
|
// Assume it's in progress. It could be pending cancelation
|
|
120
|
-
return { state: "running",
|
|
123
|
+
return { state: "running", previousAttempts: work.attempts } as const;
|
|
121
124
|
},
|
|
122
125
|
});
|
|
123
126
|
|
|
127
|
+
async function cancelWorkItem(
|
|
128
|
+
ctx: MutationCtx,
|
|
129
|
+
workId: Id<"work">,
|
|
130
|
+
segment: bigint,
|
|
131
|
+
logLevel: LogLevel
|
|
132
|
+
) {
|
|
133
|
+
const console = createLogger(logLevel);
|
|
134
|
+
// No-op if the work doesn't exist or has completed.
|
|
135
|
+
const work = await ctx.db.get(workId);
|
|
136
|
+
if (!work) {
|
|
137
|
+
console.warn(`[cancel] work ${workId} doesn't exist`);
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
const pendingCancelation = await ctx.db
|
|
141
|
+
.query("pendingCancelation")
|
|
142
|
+
.withIndex("workId", (q) => q.eq("workId", workId))
|
|
143
|
+
.unique();
|
|
144
|
+
if (pendingCancelation) {
|
|
145
|
+
console.warn(`[cancel] work ${workId} has already been canceled`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
await ctx.db.insert("pendingCancelation", {
|
|
149
|
+
workId,
|
|
150
|
+
segment,
|
|
151
|
+
});
|
|
152
|
+
return true;
|
|
153
|
+
}
|
|
154
|
+
|
|
124
155
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
125
156
|
const console = "THIS IS A REMINDER TO USE createLogger";
|
package/src/component/logging.ts
CHANGED
|
@@ -53,9 +53,8 @@ export function createLogger(level?: LogLevel): Logger {
|
|
|
53
53
|
event: (event: string, payload: Record<string, unknown>) => {
|
|
54
54
|
if (levelIndex <= 1) {
|
|
55
55
|
const fullPayload = {
|
|
56
|
-
system: "idempotent-workpool-component",
|
|
57
56
|
event,
|
|
58
|
-
payload,
|
|
57
|
+
...payload,
|
|
59
58
|
};
|
|
60
59
|
console.info(JSON.stringify(fullPayload));
|
|
61
60
|
}
|