@convex-dev/workpool 0.3.1-alpha.0 → 0.3.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 +9 -0
- package/dist/client/index.d.ts +9 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +22 -22
- package/dist/client/index.js.map +1 -1
- 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/_generated/component.d.ts +12 -6
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/dataModel.d.ts +1 -1
- package/dist/component/complete.d.ts.map +1 -1
- package/dist/component/complete.js +2 -2
- package/dist/component/complete.js.map +1 -1
- package/dist/component/config.d.ts +16 -0
- package/dist/component/config.d.ts.map +1 -0
- package/dist/component/config.js +63 -0
- package/dist/component/config.js.map +1 -0
- package/dist/component/kick.d.ts +1 -1
- package/dist/component/kick.d.ts.map +1 -1
- package/dist/component/kick.js +4 -29
- package/dist/component/kick.js.map +1 -1
- package/dist/component/lib.d.ts +6 -6
- package/dist/component/lib.d.ts.map +1 -1
- package/dist/component/lib.js +19 -29
- package/dist/component/lib.js.map +1 -1
- package/dist/component/schema.js +4 -4
- package/dist/component/schema.js.map +1 -1
- package/dist/component/shared.d.ts +5 -6
- package/dist/component/shared.d.ts.map +1 -1
- package/dist/component/shared.js +3 -3
- package/dist/component/shared.js.map +1 -1
- package/package.json +22 -22
- package/src/client/index.ts +33 -25
- package/src/component/_generated/api.ts +2 -0
- package/src/component/_generated/component.ts +18 -6
- package/src/component/_generated/dataModel.ts +1 -1
- package/src/component/complete.ts +2 -6
- package/src/component/config.test.ts +31 -0
- package/src/component/config.ts +72 -0
- package/src/component/kick.test.ts +2 -23
- package/src/component/kick.ts +4 -32
- package/src/component/lib.test.ts +3 -3
- package/src/component/lib.ts +21 -34
- package/src/component/recovery.test.ts +122 -122
- package/src/component/schema.ts +6 -6
- package/src/component/shared.ts +5 -6
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { mutation, type MutationCtx } from "./_generated/server.js";
|
|
2
|
+
import { vConfig, DEFAULT_MAX_PARALLELISM, type Config } from "./shared.js";
|
|
3
|
+
import { createLogger, DEFAULT_LOG_LEVEL } from "./logging.js";
|
|
4
|
+
import { kickMainLoop } from "./kick.js";
|
|
5
|
+
|
|
6
|
+
export const MAX_POSSIBLE_PARALLELISM = 200;
|
|
7
|
+
export const MAX_PARALLELISM_SOFT_LIMIT = 100;
|
|
8
|
+
|
|
9
|
+
export const update = mutation({
|
|
10
|
+
args: vConfig.partial(),
|
|
11
|
+
handler: async (ctx, args) => {
|
|
12
|
+
const { globals, previousValue } = await _getOrUpdateGlobals(ctx, args);
|
|
13
|
+
if (args.maxParallelism && args.maxParallelism > previousValue) {
|
|
14
|
+
await kickMainLoop(ctx, "kick", globals);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
export function validateConfig(config: Partial<Config>) {
|
|
20
|
+
if (config.maxParallelism !== undefined) {
|
|
21
|
+
if (config.maxParallelism > MAX_POSSIBLE_PARALLELISM) {
|
|
22
|
+
throw new Error(`maxParallelism must be <= ${MAX_POSSIBLE_PARALLELISM}`);
|
|
23
|
+
} else if (config.maxParallelism > MAX_PARALLELISM_SOFT_LIMIT) {
|
|
24
|
+
createLogger(config.logLevel ?? DEFAULT_LOG_LEVEL).warn(
|
|
25
|
+
`maxParallelism should be <= ${MAX_PARALLELISM_SOFT_LIMIT}, but is set to ${config.maxParallelism}. This will be an error in a future version.`,
|
|
26
|
+
);
|
|
27
|
+
} else if (config.maxParallelism < 0) {
|
|
28
|
+
throw new Error("maxParallelism must be >= 0");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function getOrUpdateGlobals(
|
|
33
|
+
ctx: MutationCtx,
|
|
34
|
+
config?: Partial<Config>,
|
|
35
|
+
) {
|
|
36
|
+
const { globals } = await _getOrUpdateGlobals(ctx, config);
|
|
37
|
+
return globals;
|
|
38
|
+
}
|
|
39
|
+
async function _getOrUpdateGlobals(
|
|
40
|
+
ctx: MutationCtx,
|
|
41
|
+
config?: Partial<Config>,
|
|
42
|
+
) {
|
|
43
|
+
if (config) {
|
|
44
|
+
validateConfig(config);
|
|
45
|
+
}
|
|
46
|
+
const globals = await ctx.db.query("globals").unique();
|
|
47
|
+
const previousValue = globals?.maxParallelism ?? DEFAULT_MAX_PARALLELISM;
|
|
48
|
+
if (!globals) {
|
|
49
|
+
const id = await ctx.db.insert("globals", {
|
|
50
|
+
maxParallelism: config?.maxParallelism ?? DEFAULT_MAX_PARALLELISM,
|
|
51
|
+
logLevel: config?.logLevel ?? DEFAULT_LOG_LEVEL,
|
|
52
|
+
});
|
|
53
|
+
return { globals: (await ctx.db.get("globals", id))!, previousValue };
|
|
54
|
+
} else if (config) {
|
|
55
|
+
let updated = false;
|
|
56
|
+
if (
|
|
57
|
+
config.maxParallelism !== undefined &&
|
|
58
|
+
config.maxParallelism !== globals.maxParallelism
|
|
59
|
+
) {
|
|
60
|
+
globals.maxParallelism = config.maxParallelism;
|
|
61
|
+
updated = true;
|
|
62
|
+
}
|
|
63
|
+
if (config.logLevel && config.logLevel !== globals.logLevel) {
|
|
64
|
+
globals.logLevel = config.logLevel;
|
|
65
|
+
updated = true;
|
|
66
|
+
}
|
|
67
|
+
if (updated) {
|
|
68
|
+
await ctx.db.replace("globals", globals._id, globals);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return { globals, previousValue };
|
|
72
|
+
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
getNextSegment,
|
|
21
21
|
toSegment,
|
|
22
22
|
} from "./shared.js";
|
|
23
|
+
import { getOrUpdateGlobals } from "./config.js";
|
|
23
24
|
|
|
24
25
|
describe("kickMainLoop", () => {
|
|
25
26
|
beforeEach(() => {
|
|
@@ -47,27 +48,6 @@ describe("kickMainLoop", () => {
|
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
|
|
50
|
-
test("it updates the globals when they change", async () => {
|
|
51
|
-
const t = convexTest(schema, modules);
|
|
52
|
-
await t.run(async (ctx) => {
|
|
53
|
-
await kickMainLoop(ctx, "enqueue");
|
|
54
|
-
const globals = await ctx.db.query("globals").unique();
|
|
55
|
-
expect(globals).not.toBeNull();
|
|
56
|
-
assert(globals);
|
|
57
|
-
expect(globals.maxParallelism).toBe(DEFAULT_MAX_PARALLELISM);
|
|
58
|
-
expect(globals.logLevel).toBe(DEFAULT_LOG_LEVEL);
|
|
59
|
-
await kickMainLoop(ctx, "enqueue", {
|
|
60
|
-
maxParallelism: DEFAULT_MAX_PARALLELISM + 1,
|
|
61
|
-
logLevel: "ERROR",
|
|
62
|
-
});
|
|
63
|
-
const after = await ctx.db.query("globals").unique();
|
|
64
|
-
expect(after).not.toBeNull();
|
|
65
|
-
assert(after);
|
|
66
|
-
expect(after.maxParallelism).toBe(DEFAULT_MAX_PARALLELISM + 1);
|
|
67
|
-
expect(after.logLevel).toBe("ERROR");
|
|
68
|
-
});
|
|
69
|
-
});
|
|
70
|
-
|
|
71
51
|
test("does not kick when already running", async () => {
|
|
72
52
|
const t = convexTest(schema, modules);
|
|
73
53
|
await t.run(async (ctx) => {
|
|
@@ -249,8 +229,7 @@ describe("kickMainLoop", () => {
|
|
|
249
229
|
test("preserves state between kicks with different sources", async () => {
|
|
250
230
|
const t = convexTest(schema, modules);
|
|
251
231
|
await t.run(async (ctx) => {
|
|
252
|
-
|
|
253
|
-
await kickMainLoop(ctx, "enqueue", {
|
|
232
|
+
await getOrUpdateGlobals(ctx, {
|
|
254
233
|
maxParallelism: 5,
|
|
255
234
|
logLevel: "ERROR",
|
|
256
235
|
});
|
package/src/component/kick.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { internal } from "./_generated/api.js";
|
|
2
2
|
import { internalMutation, type MutationCtx } from "./_generated/server.js";
|
|
3
|
-
import {
|
|
3
|
+
import { getOrUpdateGlobals } from "./config.js";
|
|
4
|
+
import { createLogger } from "./logging.js";
|
|
4
5
|
import { INITIAL_STATE } from "./loop.js";
|
|
5
6
|
import {
|
|
6
7
|
boundScheduledTime,
|
|
7
8
|
type Config,
|
|
8
|
-
DEFAULT_MAX_PARALLELISM,
|
|
9
9
|
fromSegment,
|
|
10
10
|
getCurrentSegment,
|
|
11
11
|
getNextSegment,
|
|
@@ -20,9 +20,9 @@ import {
|
|
|
20
20
|
export async function kickMainLoop(
|
|
21
21
|
ctx: MutationCtx,
|
|
22
22
|
source: "enqueue" | "cancel" | "complete" | "kick",
|
|
23
|
-
config?:
|
|
23
|
+
config?: Config,
|
|
24
24
|
): Promise<bigint> {
|
|
25
|
-
const globals = await getOrUpdateGlobals(ctx, config);
|
|
25
|
+
const globals = config ?? (await getOrUpdateGlobals(ctx, config));
|
|
26
26
|
const console = createLogger(globals.logLevel);
|
|
27
27
|
const runStatus = await getOrCreateRunStatus(ctx);
|
|
28
28
|
const next = getNextSegment();
|
|
@@ -98,31 +98,3 @@ async function getOrCreateRunStatus(ctx: MutationCtx) {
|
|
|
98
98
|
}
|
|
99
99
|
return runStatus;
|
|
100
100
|
}
|
|
101
|
-
|
|
102
|
-
async function getOrUpdateGlobals(ctx: MutationCtx, config?: Partial<Config>) {
|
|
103
|
-
const globals = await ctx.db.query("globals").unique();
|
|
104
|
-
if (!globals) {
|
|
105
|
-
const id = await ctx.db.insert("globals", {
|
|
106
|
-
maxParallelism: config?.maxParallelism ?? DEFAULT_MAX_PARALLELISM,
|
|
107
|
-
logLevel: config?.logLevel ?? DEFAULT_LOG_LEVEL,
|
|
108
|
-
});
|
|
109
|
-
return (await ctx.db.get(id))!;
|
|
110
|
-
} else if (config) {
|
|
111
|
-
let updated = false;
|
|
112
|
-
if (
|
|
113
|
-
config.maxParallelism &&
|
|
114
|
-
config.maxParallelism !== globals.maxParallelism
|
|
115
|
-
) {
|
|
116
|
-
globals.maxParallelism = config.maxParallelism;
|
|
117
|
-
updated = true;
|
|
118
|
-
}
|
|
119
|
-
if (config.logLevel && config.logLevel !== globals.logLevel) {
|
|
120
|
-
globals.logLevel = config.logLevel;
|
|
121
|
-
updated = true;
|
|
122
|
-
}
|
|
123
|
-
if (updated) {
|
|
124
|
-
await ctx.db.replace(globals._id, globals);
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return globals;
|
|
128
|
-
}
|
|
@@ -66,7 +66,7 @@ describe("lib", () => {
|
|
|
66
66
|
logLevel: "WARN",
|
|
67
67
|
},
|
|
68
68
|
}),
|
|
69
|
-
).rejects.toThrow("maxParallelism must be <=
|
|
69
|
+
).rejects.toThrow("maxParallelism must be <= 200");
|
|
70
70
|
});
|
|
71
71
|
|
|
72
72
|
it("should throw error if maxParallelism is too low", async () => {
|
|
@@ -78,11 +78,11 @@ describe("lib", () => {
|
|
|
78
78
|
fnType: "mutation",
|
|
79
79
|
runAt: Date.now(),
|
|
80
80
|
config: {
|
|
81
|
-
maxParallelism:
|
|
81
|
+
maxParallelism: -1, // Less than minimum
|
|
82
82
|
logLevel: "WARN",
|
|
83
83
|
},
|
|
84
84
|
}),
|
|
85
|
-
).rejects.toThrow("maxParallelism must be >=
|
|
85
|
+
).rejects.toThrow("maxParallelism must be >= 0");
|
|
86
86
|
});
|
|
87
87
|
});
|
|
88
88
|
|
package/src/component/lib.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type ObjectType, v } from "convex/values";
|
|
2
2
|
import { api } from "./_generated/api.js";
|
|
3
3
|
import type { Id } from "./_generated/dataModel.js";
|
|
4
4
|
import {
|
|
@@ -16,19 +16,17 @@ import {
|
|
|
16
16
|
} from "./logging.js";
|
|
17
17
|
import {
|
|
18
18
|
boundScheduledTime,
|
|
19
|
-
|
|
19
|
+
vConfig,
|
|
20
20
|
fnType,
|
|
21
21
|
getNextSegment,
|
|
22
22
|
max,
|
|
23
|
-
|
|
23
|
+
vOnCompleteFnContext,
|
|
24
24
|
retryBehavior,
|
|
25
25
|
status as statusValidator,
|
|
26
26
|
toSegment,
|
|
27
27
|
} from "./shared.js";
|
|
28
28
|
import { recordEnqueued } from "./stats.js";
|
|
29
|
-
|
|
30
|
-
const MAX_POSSIBLE_PARALLELISM = 200;
|
|
31
|
-
const MAX_PARALLELISM_SOFT_LIMIT = 100;
|
|
29
|
+
import { getOrUpdateGlobals } from "./config.js";
|
|
32
30
|
|
|
33
31
|
const itemArgs = {
|
|
34
32
|
fnHandle: v.string(),
|
|
@@ -37,20 +35,20 @@ const itemArgs = {
|
|
|
37
35
|
fnType,
|
|
38
36
|
runAt: v.number(),
|
|
39
37
|
// TODO: annotation?
|
|
40
|
-
onComplete: v.optional(
|
|
38
|
+
onComplete: v.optional(vOnCompleteFnContext),
|
|
41
39
|
retryBehavior: v.optional(retryBehavior),
|
|
42
40
|
};
|
|
43
41
|
const enqueueArgs = {
|
|
44
42
|
...itemArgs,
|
|
45
|
-
config,
|
|
43
|
+
config: vConfig.partial(),
|
|
46
44
|
};
|
|
47
45
|
export const enqueue = mutation({
|
|
48
46
|
args: enqueueArgs,
|
|
49
47
|
returns: v.id("work"),
|
|
50
48
|
handler: async (ctx, { config, ...itemArgs }) => {
|
|
51
|
-
|
|
52
|
-
const console = createLogger(
|
|
53
|
-
const kickSegment = await kickMainLoop(ctx, "enqueue",
|
|
49
|
+
const globals = await getOrUpdateGlobals(ctx, config);
|
|
50
|
+
const console = createLogger(globals.logLevel);
|
|
51
|
+
const kickSegment = await kickMainLoop(ctx, "enqueue", globals);
|
|
54
52
|
return await enqueueHandler(ctx, console, kickSegment, itemArgs);
|
|
55
53
|
},
|
|
56
54
|
});
|
|
@@ -73,29 +71,16 @@ async function enqueueHandler(
|
|
|
73
71
|
return workId;
|
|
74
72
|
}
|
|
75
73
|
|
|
76
|
-
type Config = Infer<typeof config>;
|
|
77
|
-
function validateConfig(config: Config) {
|
|
78
|
-
if (config.maxParallelism > MAX_POSSIBLE_PARALLELISM) {
|
|
79
|
-
throw new Error(`maxParallelism must be <= ${MAX_PARALLELISM_SOFT_LIMIT}`);
|
|
80
|
-
} else if (config.maxParallelism > MAX_PARALLELISM_SOFT_LIMIT) {
|
|
81
|
-
createLogger(config.logLevel).warn(
|
|
82
|
-
`maxParallelism should be <= ${MAX_PARALLELISM_SOFT_LIMIT}, but is set to ${config.maxParallelism}. This will be an error in a future version.`,
|
|
83
|
-
);
|
|
84
|
-
} else if (config.maxParallelism < 1) {
|
|
85
|
-
throw new Error("maxParallelism must be >= 1");
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
74
|
export const enqueueBatch = mutation({
|
|
90
75
|
args: {
|
|
91
76
|
items: v.array(v.object(itemArgs)),
|
|
92
|
-
config,
|
|
77
|
+
config: vConfig.partial(),
|
|
93
78
|
},
|
|
94
79
|
returns: v.array(v.id("work")),
|
|
95
80
|
handler: async (ctx, { config, items }) => {
|
|
96
|
-
|
|
97
|
-
const console = createLogger(
|
|
98
|
-
const kickSegment = await kickMainLoop(ctx, "enqueue",
|
|
81
|
+
const globals = await getOrUpdateGlobals(ctx, config);
|
|
82
|
+
const console = createLogger(globals.logLevel);
|
|
83
|
+
const kickSegment = await kickMainLoop(ctx, "enqueue", globals);
|
|
99
84
|
return Promise.all(
|
|
100
85
|
items.map((item) => enqueueHandler(ctx, console, kickSegment, item)),
|
|
101
86
|
);
|
|
@@ -105,12 +90,13 @@ export const enqueueBatch = mutation({
|
|
|
105
90
|
export const cancel = mutation({
|
|
106
91
|
args: {
|
|
107
92
|
id: v.id("work"),
|
|
108
|
-
logLevel,
|
|
93
|
+
logLevel: v.optional(logLevel),
|
|
109
94
|
},
|
|
110
95
|
handler: async (ctx, { id, logLevel }) => {
|
|
111
|
-
const
|
|
96
|
+
const globals = await getOrUpdateGlobals(ctx, { logLevel });
|
|
97
|
+
const shouldCancel = await shouldCancelWorkItem(ctx, id, globals.logLevel);
|
|
112
98
|
if (shouldCancel) {
|
|
113
|
-
const segment = await kickMainLoop(ctx, "cancel",
|
|
99
|
+
const segment = await kickMainLoop(ctx, "cancel", globals);
|
|
114
100
|
await ctx.db.insert("pendingCancelation", {
|
|
115
101
|
workId: id,
|
|
116
102
|
segment,
|
|
@@ -122,7 +108,7 @@ export const cancel = mutation({
|
|
|
122
108
|
const PAGE_SIZE = 64;
|
|
123
109
|
export const cancelAll = mutation({
|
|
124
110
|
args: {
|
|
125
|
-
logLevel,
|
|
111
|
+
logLevel: v.optional(logLevel),
|
|
126
112
|
before: v.optional(v.number()),
|
|
127
113
|
limit: v.optional(v.number()),
|
|
128
114
|
},
|
|
@@ -134,14 +120,15 @@ export const cancelAll = mutation({
|
|
|
134
120
|
.withIndex("by_creation_time", (q) => q.lte("_creationTime", beforeTime))
|
|
135
121
|
.order("desc")
|
|
136
122
|
.take(pageSize);
|
|
123
|
+
const globals = await getOrUpdateGlobals(ctx, { logLevel });
|
|
137
124
|
const shouldCancel = await Promise.all(
|
|
138
125
|
pageOfWork.map(async ({ _id }) =>
|
|
139
|
-
shouldCancelWorkItem(ctx, _id, logLevel),
|
|
126
|
+
shouldCancelWorkItem(ctx, _id, globals.logLevel),
|
|
140
127
|
),
|
|
141
128
|
);
|
|
142
129
|
let segment = getNextSegment();
|
|
143
130
|
if (shouldCancel.some((c) => c)) {
|
|
144
|
-
segment = await kickMainLoop(ctx, "cancel",
|
|
131
|
+
segment = await kickMainLoop(ctx, "cancel", globals);
|
|
145
132
|
}
|
|
146
133
|
await Promise.all(
|
|
147
134
|
pageOfWork.map(({ _id }, index) => {
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import { convexTest } from "convex-test";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
DocumentByName,
|
|
4
|
+
GenericDatabaseReader,
|
|
5
|
+
GenericDataModel,
|
|
6
|
+
SystemDataModel,
|
|
7
|
+
SystemTableNames,
|
|
8
|
+
WithoutSystemFields,
|
|
9
|
+
} from "convex/server";
|
|
3
10
|
import {
|
|
4
11
|
afterEach,
|
|
5
12
|
assert,
|
|
@@ -14,8 +21,7 @@ import type { Doc, Id } from "./_generated/dataModel.js";
|
|
|
14
21
|
import type { MutationCtx } from "./_generated/server.js";
|
|
15
22
|
import { recoveryHandler } from "./recovery.js";
|
|
16
23
|
import schema from "./schema.js";
|
|
17
|
-
|
|
18
|
-
const modules = import.meta.glob("./**/*.ts");
|
|
24
|
+
import { modules } from "./setup.test.js";
|
|
19
25
|
|
|
20
26
|
describe("recovery", () => {
|
|
21
27
|
async function setupTest() {
|
|
@@ -196,13 +202,7 @@ describe("recovery", () => {
|
|
|
196
202
|
// Run recovery with mocked system.get
|
|
197
203
|
await t.run(async (ctx) => {
|
|
198
204
|
// Mock the system.get to return null for our scheduledId
|
|
199
|
-
|
|
200
|
-
ctx.db.system.get = async (id) => {
|
|
201
|
-
if (id === scheduledId) {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
return await originalGet(id);
|
|
205
|
-
};
|
|
205
|
+
ctx.db.system.get = patchedSystemGet(ctx.db, { [scheduledId]: null });
|
|
206
206
|
|
|
207
207
|
await recoveryHandler(ctx, {
|
|
208
208
|
jobs: [
|
|
@@ -244,31 +244,27 @@ describe("recovery", () => {
|
|
|
244
244
|
// Run recovery with mocked failed state
|
|
245
245
|
await t.run(async (ctx) => {
|
|
246
246
|
// Mock the system.get to return a failed state
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
logLevel: "WARN",
|
|
260
|
-
attempt: 0,
|
|
261
|
-
},
|
|
262
|
-
],
|
|
263
|
-
scheduledTime: Date.now(),
|
|
264
|
-
state: {
|
|
265
|
-
kind: "failed",
|
|
266
|
-
error: "Function execution failed",
|
|
247
|
+
ctx.db.system.get = patchedSystemGet(ctx.db, {
|
|
248
|
+
[scheduledId]: {
|
|
249
|
+
_id: scheduledId,
|
|
250
|
+
_creationTime: Date.now(),
|
|
251
|
+
name: "internal/worker.runActionWrapper",
|
|
252
|
+
args: [
|
|
253
|
+
{
|
|
254
|
+
workId,
|
|
255
|
+
fnHandle: "test_handle",
|
|
256
|
+
fnArgs: {},
|
|
257
|
+
logLevel: "WARN",
|
|
258
|
+
attempt: 0,
|
|
267
259
|
},
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
260
|
+
],
|
|
261
|
+
scheduledTime: Date.now(),
|
|
262
|
+
state: {
|
|
263
|
+
kind: "failed",
|
|
264
|
+
error: "Function execution failed",
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
});
|
|
272
268
|
|
|
273
269
|
await recoveryHandler(ctx, {
|
|
274
270
|
jobs: [
|
|
@@ -310,30 +306,26 @@ describe("recovery", () => {
|
|
|
310
306
|
// Run recovery with mocked system.get
|
|
311
307
|
await t.run(async (ctx) => {
|
|
312
308
|
// Mock the system.get to return a canceled state
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
logLevel: "WARN",
|
|
326
|
-
attempt: 0,
|
|
327
|
-
},
|
|
328
|
-
],
|
|
329
|
-
scheduledTime: Date.now(),
|
|
330
|
-
state: {
|
|
331
|
-
kind: "canceled",
|
|
309
|
+
ctx.db.system.get = patchedSystemGet(ctx.db, {
|
|
310
|
+
[scheduledId]: {
|
|
311
|
+
_id: scheduledId,
|
|
312
|
+
_creationTime: Date.now(),
|
|
313
|
+
name: "internal/worker.runActionWrapper",
|
|
314
|
+
args: [
|
|
315
|
+
{
|
|
316
|
+
workId,
|
|
317
|
+
fnHandle: "test_handle",
|
|
318
|
+
fnArgs: {},
|
|
319
|
+
logLevel: "WARN",
|
|
320
|
+
attempt: 0,
|
|
332
321
|
},
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
322
|
+
],
|
|
323
|
+
scheduledTime: Date.now(),
|
|
324
|
+
state: {
|
|
325
|
+
kind: "canceled",
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
});
|
|
337
329
|
|
|
338
330
|
await recoveryHandler(ctx, {
|
|
339
331
|
jobs: [
|
|
@@ -379,50 +371,45 @@ describe("recovery", () => {
|
|
|
379
371
|
// Run recovery with mocked system.get
|
|
380
372
|
await t.run(async (ctx) => {
|
|
381
373
|
// Mock the system.get to return different states for each scheduled function
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
logLevel: "WARN",
|
|
395
|
-
attempt: 0,
|
|
396
|
-
},
|
|
397
|
-
],
|
|
398
|
-
scheduledTime: Date.now(),
|
|
399
|
-
state: {
|
|
400
|
-
kind: "failed",
|
|
401
|
-
error: "Function 1 failed",
|
|
374
|
+
ctx.db.system.get = patchedSystemGet(ctx.db, {
|
|
375
|
+
[scheduledId1]: {
|
|
376
|
+
_id: scheduledId1,
|
|
377
|
+
_creationTime: Date.now(),
|
|
378
|
+
name: "internal/worker.runActionWrapper",
|
|
379
|
+
args: [
|
|
380
|
+
{
|
|
381
|
+
workId: workId1,
|
|
382
|
+
fnHandle: "test_handle",
|
|
383
|
+
fnArgs: { test: 1 },
|
|
384
|
+
logLevel: "WARN",
|
|
385
|
+
attempt: 0,
|
|
402
386
|
},
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
387
|
+
],
|
|
388
|
+
scheduledTime: Date.now(),
|
|
389
|
+
state: {
|
|
390
|
+
kind: "failed",
|
|
391
|
+
error: "Function 1 failed",
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
[scheduledId2]: {
|
|
395
|
+
_id: scheduledId2,
|
|
396
|
+
_creationTime: Date.now(),
|
|
397
|
+
name: "internal/worker.runActionWrapper",
|
|
398
|
+
args: [
|
|
399
|
+
{
|
|
400
|
+
workId: workId2,
|
|
401
|
+
fnHandle: "test_handle",
|
|
402
|
+
fnArgs: { test: 2 },
|
|
403
|
+
logLevel: "WARN",
|
|
404
|
+
attempt: 0,
|
|
421
405
|
},
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
406
|
+
],
|
|
407
|
+
scheduledTime: Date.now(),
|
|
408
|
+
state: {
|
|
409
|
+
kind: "canceled",
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
426
413
|
|
|
427
414
|
await recoveryHandler(ctx, {
|
|
428
415
|
jobs: [
|
|
@@ -486,30 +473,26 @@ describe("recovery", () => {
|
|
|
486
473
|
// Run recovery with mocked system.get
|
|
487
474
|
await t.run(async (ctx) => {
|
|
488
475
|
// Mock the system.get to return a pending state
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
logLevel: "WARN",
|
|
502
|
-
attempt: 0,
|
|
503
|
-
},
|
|
504
|
-
],
|
|
505
|
-
scheduledTime: Date.now(),
|
|
506
|
-
state: {
|
|
507
|
-
kind: "pending",
|
|
476
|
+
ctx.db.system.get = patchedSystemGet(ctx.db, {
|
|
477
|
+
[scheduledId]: {
|
|
478
|
+
_id: scheduledId,
|
|
479
|
+
_creationTime: Date.now(),
|
|
480
|
+
name: "internal/worker.runActionWrapper",
|
|
481
|
+
args: [
|
|
482
|
+
{
|
|
483
|
+
workId,
|
|
484
|
+
fnHandle: "test_handle",
|
|
485
|
+
fnArgs: {},
|
|
486
|
+
logLevel: "WARN",
|
|
487
|
+
attempt: 0,
|
|
508
488
|
},
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
489
|
+
],
|
|
490
|
+
scheduledTime: Date.now(),
|
|
491
|
+
state: {
|
|
492
|
+
kind: "pending",
|
|
493
|
+
},
|
|
494
|
+
},
|
|
495
|
+
});
|
|
513
496
|
|
|
514
497
|
await recoveryHandler(ctx, {
|
|
515
498
|
jobs: [
|
|
@@ -534,3 +517,20 @@ describe("recovery", () => {
|
|
|
534
517
|
});
|
|
535
518
|
});
|
|
536
519
|
});
|
|
520
|
+
|
|
521
|
+
function patchedSystemGet(
|
|
522
|
+
db: GenericDatabaseReader<GenericDataModel>,
|
|
523
|
+
overrides: Record<
|
|
524
|
+
string,
|
|
525
|
+
DocumentByName<SystemDataModel, "_scheduled_functions"> | null
|
|
526
|
+
>,
|
|
527
|
+
) {
|
|
528
|
+
const originalGet = db.system.get;
|
|
529
|
+
return async (
|
|
530
|
+
tableOrId: SystemTableNames | Id<SystemTableNames>,
|
|
531
|
+
maybeId?: Id<SystemTableNames>,
|
|
532
|
+
) => {
|
|
533
|
+
const id = (maybeId ?? tableOrId) as Id<"_scheduled_functions">;
|
|
534
|
+
return id in overrides ? overrides[id] : await originalGet(id);
|
|
535
|
+
};
|
|
536
|
+
}
|
package/src/component/schema.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { defineSchema, defineTable } from "convex/server";
|
|
|
2
2
|
import { v } from "convex/values";
|
|
3
3
|
import {
|
|
4
4
|
fnType,
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
vConfig,
|
|
6
|
+
vOnCompleteFnContext,
|
|
7
7
|
retryBehavior,
|
|
8
|
-
|
|
8
|
+
vResult,
|
|
9
9
|
} from "./shared.js";
|
|
10
10
|
|
|
11
11
|
// Represents a slice of time to process work.
|
|
@@ -13,7 +13,7 @@ const segment = v.int64();
|
|
|
13
13
|
|
|
14
14
|
export default defineSchema({
|
|
15
15
|
// Written from kickLoop, read everywhere.
|
|
16
|
-
globals: defineTable(
|
|
16
|
+
globals: defineTable(vConfig),
|
|
17
17
|
// Singleton, only read & written by `main`.
|
|
18
18
|
internalState: defineTable({
|
|
19
19
|
// Ensure that only one main is running at a time.
|
|
@@ -64,7 +64,7 @@ export default defineSchema({
|
|
|
64
64
|
fnName: v.string(),
|
|
65
65
|
fnArgs: v.any(),
|
|
66
66
|
attempts: v.number(), // number of completed attempts
|
|
67
|
-
onComplete: v.optional(
|
|
67
|
+
onComplete: v.optional(vOnCompleteFnContext),
|
|
68
68
|
retryBehavior: v.optional(retryBehavior),
|
|
69
69
|
canceled: v.optional(v.boolean()),
|
|
70
70
|
}),
|
|
@@ -80,7 +80,7 @@ export default defineSchema({
|
|
|
80
80
|
// Written by complete, read & deleted by `main`.
|
|
81
81
|
pendingCompletion: defineTable({
|
|
82
82
|
segment,
|
|
83
|
-
runResult:
|
|
83
|
+
runResult: vResult,
|
|
84
84
|
workId: v.id("work"),
|
|
85
85
|
retry: v.boolean(),
|
|
86
86
|
})
|