@convex-dev/workpool 0.2.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 +81 -3
- package/dist/commonjs/client/index.d.ts +30 -5
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +27 -2
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/commonjs/component/complete.d.ts.map +1 -1
- package/dist/commonjs/component/complete.js +9 -7
- package/dist/commonjs/component/complete.js.map +1 -1
- package/dist/commonjs/component/kick.d.ts +3 -2
- package/dist/commonjs/component/kick.d.ts.map +1 -1
- package/dist/commonjs/component/kick.js +12 -9
- 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 +25 -19
- 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 -15
- package/dist/commonjs/component/logging.js.map +1 -1
- package/dist/commonjs/component/loop.js +10 -10
- package/dist/commonjs/component/loop.js.map +1 -1
- package/dist/commonjs/component/recovery.d.ts +29 -0
- package/dist/commonjs/component/recovery.d.ts.map +1 -1
- package/dist/commonjs/component/recovery.js +69 -66
- package/dist/commonjs/component/recovery.js.map +1 -1
- package/dist/commonjs/component/schema.d.ts +11 -11
- package/dist/commonjs/component/shared.d.ts +4 -4
- package/dist/commonjs/component/shared.d.ts.map +1 -1
- package/dist/commonjs/component/shared.js +2 -2
- package/dist/commonjs/component/shared.js.map +1 -1
- package/dist/commonjs/component/stats.d.ts +20 -21
- package/dist/commonjs/component/stats.d.ts.map +1 -1
- package/dist/commonjs/component/stats.js +86 -38
- package/dist/commonjs/component/stats.js.map +1 -1
- package/dist/commonjs/component/worker.d.ts +2 -2
- package/dist/esm/client/index.d.ts +30 -5
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +27 -2
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/component/complete.d.ts.map +1 -1
- package/dist/esm/component/complete.js +9 -7
- package/dist/esm/component/complete.js.map +1 -1
- package/dist/esm/component/kick.d.ts +3 -2
- package/dist/esm/component/kick.d.ts.map +1 -1
- package/dist/esm/component/kick.js +12 -9
- 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 +25 -19
- 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 -15
- package/dist/esm/component/logging.js.map +1 -1
- package/dist/esm/component/loop.js +10 -10
- package/dist/esm/component/loop.js.map +1 -1
- package/dist/esm/component/recovery.d.ts +29 -0
- package/dist/esm/component/recovery.d.ts.map +1 -1
- package/dist/esm/component/recovery.js +69 -66
- package/dist/esm/component/recovery.js.map +1 -1
- package/dist/esm/component/schema.d.ts +11 -11
- package/dist/esm/component/shared.d.ts +4 -4
- package/dist/esm/component/shared.d.ts.map +1 -1
- package/dist/esm/component/shared.js +2 -2
- package/dist/esm/component/shared.js.map +1 -1
- package/dist/esm/component/stats.d.ts +20 -21
- package/dist/esm/component/stats.d.ts.map +1 -1
- package/dist/esm/component/stats.js +86 -38
- package/dist/esm/component/stats.js.map +1 -1
- package/dist/esm/component/worker.d.ts +2 -2
- package/package.json +6 -7
- package/src/client/index.ts +64 -35
- package/src/component/_generated/api.d.ts +6 -6
- package/src/component/complete.ts +18 -7
- package/src/component/kick.test.ts +17 -7
- package/src/component/kick.ts +14 -11
- package/src/component/lib.ts +33 -26
- package/src/component/logging.test.ts +16 -0
- package/src/component/logging.ts +45 -23
- package/src/component/loop.test.ts +12 -12
- package/src/component/loop.ts +11 -11
- package/src/component/recovery.test.ts +6 -11
- package/src/component/recovery.ts +77 -69
- package/src/component/shared.ts +2 -2
- package/src/component/stats.test.ts +345 -0
- package/src/component/stats.ts +111 -41
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { FunctionHandle } from "convex/server";
|
|
2
2
|
import { Infer, v } from "convex/values";
|
|
3
|
+
import { Id } from "./_generated/dataModel.js";
|
|
3
4
|
import { internalMutation, MutationCtx } from "./_generated/server.js";
|
|
4
5
|
import { kickMainLoop } from "./kick.js";
|
|
5
6
|
import { createLogger } from "./logging.js";
|
|
6
|
-
import {
|
|
7
|
+
import { OnCompleteArgs, RunResult, runResult } from "./shared.js";
|
|
7
8
|
import { recordCompleted } from "./stats.js";
|
|
8
9
|
|
|
9
10
|
export type CompleteJob = Infer<typeof completeArgs.fields.jobs.element>;
|
|
@@ -23,7 +24,11 @@ export async function completeHandler(
|
|
|
23
24
|
) {
|
|
24
25
|
const globals = await ctx.db.query("globals").unique();
|
|
25
26
|
const console = createLogger(globals?.logLevel);
|
|
26
|
-
|
|
27
|
+
const pendingCompletions: {
|
|
28
|
+
runResult: RunResult;
|
|
29
|
+
workId: Id<"work">;
|
|
30
|
+
retry: boolean;
|
|
31
|
+
}[] = [];
|
|
27
32
|
await Promise.all(
|
|
28
33
|
args.jobs.map(async (job) => {
|
|
29
34
|
const work = await ctx.db.get(job.workId);
|
|
@@ -77,18 +82,24 @@ export async function completeHandler(
|
|
|
77
82
|
await ctx.db.delete(job.workId);
|
|
78
83
|
}
|
|
79
84
|
if (job.runResult.kind !== "canceled") {
|
|
80
|
-
|
|
85
|
+
pendingCompletions.push({
|
|
81
86
|
runResult: job.runResult,
|
|
82
87
|
workId: job.workId,
|
|
83
|
-
segment: nextSegment(),
|
|
84
88
|
retry,
|
|
85
89
|
});
|
|
86
|
-
anyPendingCompletions = true;
|
|
87
90
|
}
|
|
88
91
|
})
|
|
89
92
|
);
|
|
90
|
-
if (
|
|
91
|
-
await kickMainLoop(ctx, "complete");
|
|
93
|
+
if (pendingCompletions.length > 0) {
|
|
94
|
+
const segment = await kickMainLoop(ctx, "complete");
|
|
95
|
+
await Promise.all(
|
|
96
|
+
pendingCompletions.map((completion) =>
|
|
97
|
+
ctx.db.insert("pendingCompletion", {
|
|
98
|
+
...completion,
|
|
99
|
+
segment,
|
|
100
|
+
})
|
|
101
|
+
)
|
|
102
|
+
);
|
|
92
103
|
}
|
|
93
104
|
}
|
|
94
105
|
|
|
@@ -14,7 +14,12 @@ import { kickMainLoop } from "./kick.js";
|
|
|
14
14
|
import { DEFAULT_LOG_LEVEL } from "./logging.js";
|
|
15
15
|
import schema from "./schema.js";
|
|
16
16
|
import { modules } from "./setup.test.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
fromSegment,
|
|
19
|
+
getCurrentSegment,
|
|
20
|
+
getNextSegment,
|
|
21
|
+
toSegment,
|
|
22
|
+
} from "./shared";
|
|
18
23
|
import { DEFAULT_MAX_PARALLELISM } from "./shared.js";
|
|
19
24
|
|
|
20
25
|
describe("kickMainLoop", () => {
|
|
@@ -74,11 +79,12 @@ describe("kickMainLoop", () => {
|
|
|
74
79
|
expect(runStatus.state.kind).toBe("running");
|
|
75
80
|
|
|
76
81
|
// Second kick should not change state
|
|
77
|
-
await kickMainLoop(ctx, "enqueue");
|
|
82
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
78
83
|
const afterStatus = await ctx.db.query("runStatus").unique();
|
|
79
84
|
assert(afterStatus);
|
|
80
85
|
expect(afterStatus.state.kind).toBe("running");
|
|
81
86
|
expect(afterStatus._id).toBe(runStatus._id);
|
|
87
|
+
expect(segment).toBe(getNextSegment());
|
|
82
88
|
});
|
|
83
89
|
});
|
|
84
90
|
|
|
@@ -115,7 +121,8 @@ describe("kickMainLoop", () => {
|
|
|
115
121
|
});
|
|
116
122
|
|
|
117
123
|
// Kick should reschedule to run sooner
|
|
118
|
-
await kickMainLoop(ctx, "enqueue");
|
|
124
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
125
|
+
expect(segment).toBe(getCurrentSegment());
|
|
119
126
|
|
|
120
127
|
const afterStatus = await ctx.db.query("runStatus").unique();
|
|
121
128
|
assert(afterStatus);
|
|
@@ -156,7 +163,8 @@ describe("kickMainLoop", () => {
|
|
|
156
163
|
});
|
|
157
164
|
|
|
158
165
|
// Kick should not change state when saturated
|
|
159
|
-
await kickMainLoop(ctx, "enqueue");
|
|
166
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
167
|
+
expect(segment).toBe(getNextSegment());
|
|
160
168
|
const afterStatus = await ctx.db.query("runStatus").unique();
|
|
161
169
|
assert(afterStatus);
|
|
162
170
|
expect(afterStatus.state.kind).toBe("scheduled");
|
|
@@ -209,13 +217,15 @@ describe("kickMainLoop", () => {
|
|
|
209
217
|
test("handles race conditions between multiple kicks", async () => {
|
|
210
218
|
const t = convexTest(schema, modules);
|
|
211
219
|
// Run kicks in separate transactions to simulate concurrent access
|
|
212
|
-
await Promise.all(
|
|
220
|
+
const segments = await Promise.all(
|
|
213
221
|
Array.from({ length: 10 }, () =>
|
|
214
222
|
t.run(async (ctx) => {
|
|
215
|
-
await kickMainLoop(ctx, "enqueue");
|
|
223
|
+
const segment = await kickMainLoop(ctx, "enqueue");
|
|
224
|
+
return segment;
|
|
216
225
|
})
|
|
217
226
|
)
|
|
218
227
|
);
|
|
228
|
+
expect(segments.filter((s) => s === getCurrentSegment())).toHaveLength(1);
|
|
219
229
|
|
|
220
230
|
// Check final state in a new transaction
|
|
221
231
|
await t.run(async (ctx) => {
|
|
@@ -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
|
@@ -7,43 +7,44 @@ import {
|
|
|
7
7
|
Config,
|
|
8
8
|
DEFAULT_MAX_PARALLELISM,
|
|
9
9
|
fromSegment,
|
|
10
|
-
|
|
10
|
+
getCurrentSegment,
|
|
11
|
+
getNextSegment,
|
|
11
12
|
} from "./shared.js";
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* Called from outside the loop
|
|
15
|
+
* Called from outside the loop.
|
|
16
|
+
* Returns the soonest segment to enqueue work for the main loop.
|
|
15
17
|
*/
|
|
16
|
-
|
|
17
18
|
export async function kickMainLoop(
|
|
18
19
|
ctx: MutationCtx,
|
|
19
20
|
source: "enqueue" | "cancel" | "complete",
|
|
20
21
|
config?: Partial<Config>
|
|
21
|
-
): Promise<
|
|
22
|
+
): Promise<bigint> {
|
|
22
23
|
const globals = await getOrUpdateGlobals(ctx, config);
|
|
23
24
|
const console = createLogger(globals.logLevel);
|
|
24
25
|
const runStatus = await getOrCreateRunStatus(ctx);
|
|
26
|
+
const next = getNextSegment();
|
|
25
27
|
|
|
26
28
|
// Only kick to run now if we're scheduled or idle.
|
|
27
29
|
if (runStatus.state.kind === "running") {
|
|
28
30
|
console.debug(
|
|
29
31
|
`[${source}] main is actively running, so we don't need to kick it`
|
|
30
32
|
);
|
|
31
|
-
return;
|
|
33
|
+
return next;
|
|
32
34
|
}
|
|
33
|
-
const segment = nextSegment();
|
|
34
35
|
// main is scheduled to run later, so we should cancel it and reschedule.
|
|
35
36
|
if (runStatus.state.kind === "scheduled") {
|
|
36
37
|
if (source === "enqueue" && runStatus.state.saturated) {
|
|
37
38
|
console.debug(
|
|
38
39
|
`[${source}] main is saturated, so we don't need to kick it`
|
|
39
40
|
);
|
|
40
|
-
return;
|
|
41
|
+
return next;
|
|
41
42
|
}
|
|
42
|
-
if (runStatus.state.segment <=
|
|
43
|
+
if (runStatus.state.segment <= next) {
|
|
43
44
|
console.debug(
|
|
44
45
|
`[${source}] main is scheduled to run soon enough, so we don't need to kick it`
|
|
45
46
|
);
|
|
46
|
-
return;
|
|
47
|
+
return next;
|
|
47
48
|
}
|
|
48
49
|
console.debug(
|
|
49
50
|
`[${source}] main is scheduled to run later, so reschedule it to run now`
|
|
@@ -60,11 +61,13 @@ export async function kickMainLoop(
|
|
|
60
61
|
console.debug(`[${source}] main was idle, so run it now`);
|
|
61
62
|
}
|
|
62
63
|
await ctx.db.patch(runStatus._id, { state: { kind: "running" } });
|
|
63
|
-
const
|
|
64
|
+
const current = getCurrentSegment();
|
|
65
|
+
const scheduledTime = boundScheduledTime(fromSegment(current), console);
|
|
64
66
|
await ctx.scheduler.runAt(scheduledTime, internal.loop.main, {
|
|
65
67
|
generation: runStatus.state.generation,
|
|
66
|
-
segment,
|
|
68
|
+
segment: current,
|
|
67
69
|
});
|
|
70
|
+
return current;
|
|
68
71
|
}
|
|
69
72
|
|
|
70
73
|
export const forceKick = internalMutation({
|
package/src/component/lib.ts
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
+
import { api } from "./_generated/api.js";
|
|
3
|
+
import { Id } from "./_generated/dataModel.js";
|
|
2
4
|
import { mutation, MutationCtx, query } from "./_generated/server.js";
|
|
5
|
+
import { kickMainLoop } from "./kick.js";
|
|
6
|
+
import { createLogger, LogLevel, logLevel } from "./logging.js";
|
|
3
7
|
import {
|
|
4
|
-
|
|
8
|
+
boundScheduledTime,
|
|
9
|
+
config,
|
|
10
|
+
getNextSegment,
|
|
11
|
+
max,
|
|
5
12
|
onComplete,
|
|
6
13
|
retryBehavior,
|
|
7
|
-
config,
|
|
8
14
|
status as statusValidator,
|
|
9
15
|
toSegment,
|
|
10
|
-
boundScheduledTime,
|
|
11
|
-
max,
|
|
12
16
|
} from "./shared.js";
|
|
13
|
-
import { LogLevel, logLevel } from "./logging.js";
|
|
14
|
-
import { kickMainLoop } from "./kick.js";
|
|
15
|
-
import { api } from "./_generated/api.js";
|
|
16
|
-
import { createLogger } from "./logging.js";
|
|
17
|
-
import { Id } from "./_generated/dataModel.js";
|
|
18
17
|
import { recordEnqueued } from "./stats.js";
|
|
19
18
|
|
|
20
19
|
const MAX_POSSIBLE_PARALLELISM = 100;
|
|
@@ -45,11 +44,11 @@ export const enqueue = mutation({
|
|
|
45
44
|
...workArgs,
|
|
46
45
|
attempts: 0,
|
|
47
46
|
});
|
|
47
|
+
const limit = await kickMainLoop(ctx, "enqueue", config);
|
|
48
48
|
await ctx.db.insert("pendingStart", {
|
|
49
49
|
workId,
|
|
50
|
-
segment: max(toSegment(runAt),
|
|
50
|
+
segment: max(toSegment(runAt), limit),
|
|
51
51
|
});
|
|
52
|
-
await kickMainLoop(ctx, "enqueue", config);
|
|
53
52
|
recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
|
|
54
53
|
return workId;
|
|
55
54
|
},
|
|
@@ -61,11 +60,14 @@ export const cancel = mutation({
|
|
|
61
60
|
logLevel,
|
|
62
61
|
},
|
|
63
62
|
handler: async (ctx, { id, logLevel }) => {
|
|
64
|
-
const
|
|
65
|
-
if (
|
|
66
|
-
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
63
|
+
const shouldCancel = await shouldCancelWorkItem(ctx, id, logLevel);
|
|
64
|
+
if (shouldCancel) {
|
|
65
|
+
const segment = await kickMainLoop(ctx, "cancel", { logLevel });
|
|
66
|
+
await ctx.db.insert("pendingCancelation", {
|
|
67
|
+
workId: id,
|
|
68
|
+
segment,
|
|
69
|
+
});
|
|
67
70
|
}
|
|
68
|
-
// TODO: stats event
|
|
69
71
|
},
|
|
70
72
|
});
|
|
71
73
|
|
|
@@ -74,20 +76,30 @@ export const cancelAll = mutation({
|
|
|
74
76
|
args: { logLevel, before: v.optional(v.number()) },
|
|
75
77
|
handler: async (ctx, { logLevel, before }) => {
|
|
76
78
|
const beforeTime = before ?? Date.now();
|
|
77
|
-
const segment = nextSegment();
|
|
78
79
|
const pageOfWork = await ctx.db
|
|
79
80
|
.query("work")
|
|
80
81
|
.withIndex("by_creation_time", (q) => q.lte("_creationTime", beforeTime))
|
|
81
82
|
.order("desc")
|
|
82
83
|
.take(PAGE_SIZE);
|
|
83
|
-
const
|
|
84
|
+
const shouldCancel = await Promise.all(
|
|
84
85
|
pageOfWork.map(async ({ _id }) =>
|
|
85
|
-
|
|
86
|
+
shouldCancelWorkItem(ctx, _id, logLevel)
|
|
86
87
|
)
|
|
87
88
|
);
|
|
88
|
-
|
|
89
|
-
|
|
89
|
+
let segment = getNextSegment();
|
|
90
|
+
if (shouldCancel.some((c) => c)) {
|
|
91
|
+
segment = await kickMainLoop(ctx, "cancel", { logLevel });
|
|
90
92
|
}
|
|
93
|
+
await Promise.all(
|
|
94
|
+
pageOfWork.map(({ _id }, index) => {
|
|
95
|
+
if (shouldCancel[index]) {
|
|
96
|
+
return ctx.db.insert("pendingCancelation", {
|
|
97
|
+
workId: _id,
|
|
98
|
+
segment,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
);
|
|
91
103
|
if (pageOfWork.length === PAGE_SIZE) {
|
|
92
104
|
await ctx.scheduler.runAfter(0, api.lib.cancelAll, {
|
|
93
105
|
logLevel,
|
|
@@ -124,10 +136,9 @@ export const status = query({
|
|
|
124
136
|
},
|
|
125
137
|
});
|
|
126
138
|
|
|
127
|
-
async function
|
|
139
|
+
async function shouldCancelWorkItem(
|
|
128
140
|
ctx: MutationCtx,
|
|
129
141
|
workId: Id<"work">,
|
|
130
|
-
segment: bigint,
|
|
131
142
|
logLevel: LogLevel
|
|
132
143
|
) {
|
|
133
144
|
const console = createLogger(logLevel);
|
|
@@ -145,10 +156,6 @@ async function cancelWorkItem(
|
|
|
145
156
|
console.warn(`[cancel] work ${workId} has already been canceled`);
|
|
146
157
|
return false;
|
|
147
158
|
}
|
|
148
|
-
await ctx.db.insert("pendingCancelation", {
|
|
149
|
-
workId,
|
|
150
|
-
segment,
|
|
151
|
-
});
|
|
152
159
|
return true;
|
|
153
160
|
}
|
|
154
161
|
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { shouldLog } from "./logging";
|
|
3
|
+
|
|
4
|
+
describe("logging", () => {
|
|
5
|
+
describe("shouldLog", () => {
|
|
6
|
+
it("should return true if the log level is above the config level", () => {
|
|
7
|
+
expect(shouldLog("INFO", "DEBUG")).toBe(false);
|
|
8
|
+
});
|
|
9
|
+
it("should return false if the log level is below the config level", () => {
|
|
10
|
+
expect(shouldLog("INFO", "WARN")).toBe(true);
|
|
11
|
+
});
|
|
12
|
+
it("should return true if the log level is equal to the config level", () => {
|
|
13
|
+
expect(shouldLog("INFO", "INFO")).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
});
|
package/src/component/logging.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import { v, Infer } from "convex/values";
|
|
2
2
|
|
|
3
|
-
export const DEFAULT_LOG_LEVEL: LogLevel = "
|
|
3
|
+
export const DEFAULT_LOG_LEVEL: LogLevel = "REPORT";
|
|
4
|
+
|
|
5
|
+
// NOTE: the ordering here is important! A config level of "INFO" will log
|
|
6
|
+
// "INFO", "REPORT", "WARN",and "ERROR" events.
|
|
7
|
+
export const logLevel = v.union(
|
|
8
|
+
v.literal("DEBUG"),
|
|
9
|
+
v.literal("INFO"),
|
|
10
|
+
v.literal("REPORT"),
|
|
11
|
+
v.literal("WARN"),
|
|
12
|
+
v.literal("ERROR")
|
|
13
|
+
);
|
|
14
|
+
export type LogLevel = Infer<typeof logLevel>;
|
|
4
15
|
|
|
5
16
|
export type Logger = {
|
|
6
17
|
debug: (...args: unknown[]) => void;
|
|
@@ -12,59 +23,70 @@ export type Logger = {
|
|
|
12
23
|
event: (event: string, payload: Record<string, unknown>) => void;
|
|
13
24
|
};
|
|
14
25
|
|
|
26
|
+
const logLevelOrder = logLevel.members.map((l) => l.value);
|
|
27
|
+
const logLevelByName = logLevelOrder.reduce(
|
|
28
|
+
(acc, l, i) => {
|
|
29
|
+
acc[l] = i;
|
|
30
|
+
return acc;
|
|
31
|
+
},
|
|
32
|
+
{} as Record<LogLevel, number>
|
|
33
|
+
);
|
|
34
|
+
export function shouldLog(config: LogLevel, level: LogLevel) {
|
|
35
|
+
return logLevelByName[config] <= logLevelByName[level];
|
|
36
|
+
}
|
|
37
|
+
const DEBUG = logLevelByName["DEBUG"];
|
|
38
|
+
const INFO = logLevelByName["INFO"];
|
|
39
|
+
const REPORT = logLevelByName["REPORT"];
|
|
40
|
+
const WARN = logLevelByName["WARN"];
|
|
41
|
+
const ERROR = logLevelByName["ERROR"];
|
|
42
|
+
|
|
15
43
|
export function createLogger(level?: LogLevel): Logger {
|
|
16
|
-
const levelIndex = [
|
|
17
|
-
|
|
18
|
-
);
|
|
19
|
-
if (levelIndex === -1) {
|
|
44
|
+
const levelIndex = logLevelByName[level ?? DEFAULT_LOG_LEVEL];
|
|
45
|
+
if (levelIndex === undefined) {
|
|
20
46
|
throw new Error(`Invalid log level: ${level}`);
|
|
21
47
|
}
|
|
22
48
|
return {
|
|
23
49
|
debug: (...args: unknown[]) => {
|
|
24
|
-
if (levelIndex <=
|
|
50
|
+
if (levelIndex <= DEBUG) {
|
|
25
51
|
console.debug(...args);
|
|
26
52
|
}
|
|
27
53
|
},
|
|
28
54
|
info: (...args: unknown[]) => {
|
|
29
|
-
if (levelIndex <=
|
|
55
|
+
if (levelIndex <= INFO) {
|
|
30
56
|
console.info(...args);
|
|
31
57
|
}
|
|
32
58
|
},
|
|
33
59
|
warn: (...args: unknown[]) => {
|
|
34
|
-
if (levelIndex <=
|
|
60
|
+
if (levelIndex <= WARN) {
|
|
35
61
|
console.warn(...args);
|
|
36
62
|
}
|
|
37
63
|
},
|
|
38
64
|
error: (...args: unknown[]) => {
|
|
39
|
-
if (levelIndex <=
|
|
65
|
+
if (levelIndex <= ERROR) {
|
|
40
66
|
console.error(...args);
|
|
41
67
|
}
|
|
42
68
|
},
|
|
43
69
|
time: (label: string) => {
|
|
44
|
-
if (levelIndex <=
|
|
70
|
+
if (levelIndex <= DEBUG) {
|
|
45
71
|
console.time(label);
|
|
46
72
|
}
|
|
47
73
|
},
|
|
48
74
|
timeEnd: (label: string) => {
|
|
49
|
-
if (levelIndex <=
|
|
75
|
+
if (levelIndex <= DEBUG) {
|
|
50
76
|
console.timeEnd(label);
|
|
51
77
|
}
|
|
52
78
|
},
|
|
53
79
|
event: (event: string, payload: Record<string, unknown>) => {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
80
|
+
const fullPayload = {
|
|
81
|
+
component: "workpool",
|
|
82
|
+
event,
|
|
83
|
+
...payload,
|
|
84
|
+
};
|
|
85
|
+
if (levelIndex === REPORT && event === "report") {
|
|
86
|
+
console.info(JSON.stringify(fullPayload));
|
|
87
|
+
} else if (levelIndex <= INFO) {
|
|
59
88
|
console.info(JSON.stringify(fullPayload));
|
|
60
89
|
}
|
|
61
90
|
},
|
|
62
91
|
};
|
|
63
92
|
}
|
|
64
|
-
export const logLevel = v.union(
|
|
65
|
-
v.literal("DEBUG"),
|
|
66
|
-
v.literal("INFO"),
|
|
67
|
-
v.literal("WARN"),
|
|
68
|
-
v.literal("ERROR")
|
|
69
|
-
);
|
|
70
|
-
export type LogLevel = Infer<typeof logLevel>;
|
|
@@ -14,9 +14,9 @@ import { Doc, Id } from "./_generated/dataModel";
|
|
|
14
14
|
import { MutationCtx } from "./_generated/server";
|
|
15
15
|
import schema from "./schema";
|
|
16
16
|
import {
|
|
17
|
-
currentSegment,
|
|
18
17
|
DEFAULT_MAX_PARALLELISM,
|
|
19
|
-
|
|
18
|
+
getCurrentSegment,
|
|
19
|
+
getNextSegment,
|
|
20
20
|
toSegment,
|
|
21
21
|
} from "./shared";
|
|
22
22
|
|
|
@@ -72,7 +72,7 @@ describe("loop", () => {
|
|
|
72
72
|
await ctx.db.insert("internalState", {
|
|
73
73
|
generation: 1n,
|
|
74
74
|
segmentCursors: { incoming: 0n, completion: 0n, cancelation: 0n },
|
|
75
|
-
lastRecovery:
|
|
75
|
+
lastRecovery: getCurrentSegment(),
|
|
76
76
|
report: {
|
|
77
77
|
completed: 0,
|
|
78
78
|
succeeded: 0,
|
|
@@ -281,7 +281,7 @@ describe("loop", () => {
|
|
|
281
281
|
// Run main loop to process pendingCompletion -> pendingStart
|
|
282
282
|
await t.mutation(internal.loop.main, {
|
|
283
283
|
generation: 1n,
|
|
284
|
-
segment:
|
|
284
|
+
segment: getNextSegment(),
|
|
285
285
|
});
|
|
286
286
|
|
|
287
287
|
// Verify work is now in pendingStart for retry
|
|
@@ -364,13 +364,13 @@ describe("loop", () => {
|
|
|
364
364
|
// Run main loop to process the work
|
|
365
365
|
await t.mutation(internal.loop.main, {
|
|
366
366
|
generation: 1n,
|
|
367
|
-
segment:
|
|
367
|
+
segment: getNextSegment(),
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
// Run updateRunStatus to transition to scheduled
|
|
371
371
|
await t.mutation(internal.loop.updateRunStatus, {
|
|
372
372
|
generation: 2n,
|
|
373
|
-
segment:
|
|
373
|
+
segment: getNextSegment(),
|
|
374
374
|
});
|
|
375
375
|
|
|
376
376
|
// Verify state transition to scheduled
|
|
@@ -387,7 +387,7 @@ describe("loop", () => {
|
|
|
387
387
|
it("should transition from running to saturated when maxed out", async () => {
|
|
388
388
|
// Setup initial running state with max capacity
|
|
389
389
|
await setMaxParallelism(1);
|
|
390
|
-
const segment =
|
|
390
|
+
const segment = getCurrentSegment();
|
|
391
391
|
await t.run(async (ctx) => {
|
|
392
392
|
// Create work item
|
|
393
393
|
const workId = await makeDummyWork(ctx);
|
|
@@ -441,14 +441,14 @@ describe("loop", () => {
|
|
|
441
441
|
const scheduledId = await ctx.scheduler.runAfter(
|
|
442
442
|
1000,
|
|
443
443
|
internal.loop.main,
|
|
444
|
-
{ generation: 1n, segment:
|
|
444
|
+
{ generation: 1n, segment: getNextSegment() + 10n }
|
|
445
445
|
);
|
|
446
446
|
|
|
447
447
|
// Create scheduled runStatus
|
|
448
448
|
await ctx.db.insert("runStatus", {
|
|
449
449
|
state: {
|
|
450
450
|
kind: "scheduled",
|
|
451
|
-
segment:
|
|
451
|
+
segment: getNextSegment() + 10n,
|
|
452
452
|
scheduledId,
|
|
453
453
|
saturated: false,
|
|
454
454
|
generation: 1n,
|
|
@@ -481,7 +481,7 @@ describe("loop", () => {
|
|
|
481
481
|
});
|
|
482
482
|
|
|
483
483
|
it("should transition from running to idle when all work is done", async () => {
|
|
484
|
-
const segment =
|
|
484
|
+
const segment = getNextSegment();
|
|
485
485
|
// Setup initial running state with work
|
|
486
486
|
const workId = await t.run<Id<"work">>(async (ctx) => {
|
|
487
487
|
// Create internal state
|
|
@@ -537,7 +537,7 @@ describe("loop", () => {
|
|
|
537
537
|
});
|
|
538
538
|
});
|
|
539
539
|
it("should transition from scheduled to running when main loop runs", async () => {
|
|
540
|
-
const segment =
|
|
540
|
+
const segment = getNextSegment();
|
|
541
541
|
await t.run(async (ctx) => {
|
|
542
542
|
await insertInternalState(ctx);
|
|
543
543
|
|
|
@@ -1037,7 +1037,7 @@ describe("loop", () => {
|
|
|
1037
1037
|
|
|
1038
1038
|
it("should set saturated flag when at max capacity", async () => {
|
|
1039
1039
|
// Setup state with running jobs at max capacity
|
|
1040
|
-
const now =
|
|
1040
|
+
const now = getCurrentSegment();
|
|
1041
1041
|
const later = now + 10n;
|
|
1042
1042
|
await setMaxParallelism(10);
|
|
1043
1043
|
await t.run(async (ctx) => {
|
package/src/component/loop.ts
CHANGED
|
@@ -13,15 +13,15 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
boundScheduledTime,
|
|
15
15
|
Config,
|
|
16
|
-
currentSegment,
|
|
17
16
|
DEFAULT_MAX_PARALLELISM,
|
|
18
17
|
fromSegment,
|
|
18
|
+
getCurrentSegment,
|
|
19
|
+
getNextSegment,
|
|
19
20
|
max,
|
|
20
|
-
nextSegment,
|
|
21
21
|
RunResult,
|
|
22
22
|
toSegment,
|
|
23
23
|
} from "./shared.js";
|
|
24
|
-
import { recordCompleted,
|
|
24
|
+
import { recordCompleted, generateReport, recordStarted } from "./stats.js";
|
|
25
25
|
|
|
26
26
|
const CANCELLATION_BATCH_SIZE = 64; // the only queue that can get unbounded.
|
|
27
27
|
const SECOND = 1000;
|
|
@@ -100,7 +100,7 @@ export const main = internalMutation({
|
|
|
100
100
|
// It's been a while, let's start fresh.
|
|
101
101
|
lastReportTs = Date.now();
|
|
102
102
|
}
|
|
103
|
-
|
|
103
|
+
await generateReport(ctx, console, state, globals);
|
|
104
104
|
state.report = {
|
|
105
105
|
completed: 0,
|
|
106
106
|
succeeded: 0,
|
|
@@ -149,22 +149,22 @@ export const updateRunStatus = internalMutation({
|
|
|
149
149
|
|
|
150
150
|
// TODO: check for current segment (or from args) first, to avoid OCCs.
|
|
151
151
|
console.time("[updateRunStatus] nextSegmentIsActionable");
|
|
152
|
-
const
|
|
152
|
+
const nextSegment = max(segment + 1n, getCurrentSegment());
|
|
153
153
|
const nextIsActionable = await nextSegmentIsActionable(
|
|
154
154
|
ctx,
|
|
155
155
|
state,
|
|
156
156
|
maxParallelism,
|
|
157
|
-
|
|
157
|
+
nextSegment
|
|
158
158
|
);
|
|
159
159
|
console.timeEnd("[updateRunStatus] nextSegmentIsActionable");
|
|
160
160
|
|
|
161
161
|
if (nextIsActionable) {
|
|
162
162
|
await ctx.scheduler.runAt(
|
|
163
|
-
boundScheduledTime(fromSegment(
|
|
163
|
+
boundScheduledTime(fromSegment(nextSegment), console),
|
|
164
164
|
internal.loop.main,
|
|
165
165
|
{
|
|
166
166
|
generation,
|
|
167
|
-
segment:
|
|
167
|
+
segment: nextSegment,
|
|
168
168
|
}
|
|
169
169
|
);
|
|
170
170
|
return;
|
|
@@ -187,7 +187,7 @@ export const updateRunStatus = internalMutation({
|
|
|
187
187
|
});
|
|
188
188
|
await ctx.scheduler.runAfter(0, internal.loop.main, {
|
|
189
189
|
generation,
|
|
190
|
-
segment:
|
|
190
|
+
segment: getCurrentSegment(),
|
|
191
191
|
});
|
|
192
192
|
return;
|
|
193
193
|
}
|
|
@@ -204,7 +204,7 @@ export const updateRunStatus = internalMutation({
|
|
|
204
204
|
}
|
|
205
205
|
const docs = await Promise.all(
|
|
206
206
|
actionableTables.map(async (tableName) =>
|
|
207
|
-
getNextUp(ctx, tableName, { start:
|
|
207
|
+
getNextUp(ctx, tableName, { start: nextSegment })
|
|
208
208
|
)
|
|
209
209
|
);
|
|
210
210
|
console.timeEnd("[updateRunStatus] findNextSegment");
|
|
@@ -223,7 +223,7 @@ export const updateRunStatus = internalMutation({
|
|
|
223
223
|
internal.loop.main,
|
|
224
224
|
{ generation, segment: targetSegment }
|
|
225
225
|
);
|
|
226
|
-
if (targetSegment >
|
|
226
|
+
if (targetSegment > getNextSegment()) {
|
|
227
227
|
await ctx.db.patch(runStatus._id, {
|
|
228
228
|
state: {
|
|
229
229
|
kind: "scheduled",
|
|
@@ -13,12 +13,7 @@ import { internal } from "./_generated/api";
|
|
|
13
13
|
import { Id, Doc } from "./_generated/dataModel";
|
|
14
14
|
import { MutationCtx } from "./_generated/server";
|
|
15
15
|
import { WithoutSystemFields } from "convex/server";
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
// We call it directly so we can mock the system.get. A hack for now.
|
|
19
|
-
// But this avoids the warning about calling directly.
|
|
20
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
-
const recover = (recoverMutation as any)._handler as typeof recoverMutation;
|
|
16
|
+
import { recoveryHandler } from "./recovery";
|
|
22
17
|
|
|
23
18
|
const modules = import.meta.glob("./**/*.ts");
|
|
24
19
|
|
|
@@ -209,7 +204,7 @@ describe("recovery", () => {
|
|
|
209
204
|
return await originalGet(id);
|
|
210
205
|
};
|
|
211
206
|
|
|
212
|
-
await
|
|
207
|
+
await recoveryHandler(ctx, {
|
|
213
208
|
jobs: [
|
|
214
209
|
{
|
|
215
210
|
scheduledId,
|
|
@@ -275,7 +270,7 @@ describe("recovery", () => {
|
|
|
275
270
|
return await originalGet(id);
|
|
276
271
|
};
|
|
277
272
|
|
|
278
|
-
await
|
|
273
|
+
await recoveryHandler(ctx, {
|
|
279
274
|
jobs: [
|
|
280
275
|
{
|
|
281
276
|
scheduledId,
|
|
@@ -340,7 +335,7 @@ describe("recovery", () => {
|
|
|
340
335
|
return await originalGet(id);
|
|
341
336
|
};
|
|
342
337
|
|
|
343
|
-
await
|
|
338
|
+
await recoveryHandler(ctx, {
|
|
344
339
|
jobs: [
|
|
345
340
|
{
|
|
346
341
|
scheduledId,
|
|
@@ -429,7 +424,7 @@ describe("recovery", () => {
|
|
|
429
424
|
return await originalGet(id);
|
|
430
425
|
};
|
|
431
426
|
|
|
432
|
-
await
|
|
427
|
+
await recoveryHandler(ctx, {
|
|
433
428
|
jobs: [
|
|
434
429
|
{
|
|
435
430
|
scheduledId: scheduledId1,
|
|
@@ -516,7 +511,7 @@ describe("recovery", () => {
|
|
|
516
511
|
return await originalGet(id);
|
|
517
512
|
};
|
|
518
513
|
|
|
519
|
-
await
|
|
514
|
+
await recoveryHandler(ctx, {
|
|
520
515
|
jobs: [
|
|
521
516
|
{
|
|
522
517
|
scheduledId,
|