@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
package/src/component/lib.ts
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import {
|
|
2
|
+
import { api } from "./_generated/api.js";
|
|
3
|
+
import { Id } from "./_generated/dataModel.js";
|
|
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
16
|
} from "./shared.js";
|
|
12
|
-
import {
|
|
13
|
-
import { kickMainLoop } from "./kick.js";
|
|
14
|
-
import { api } from "./_generated/api.js";
|
|
15
|
-
import { createLogger } from "./logging.js";
|
|
17
|
+
import { recordEnqueued } from "./stats.js";
|
|
16
18
|
|
|
17
19
|
const MAX_POSSIBLE_PARALLELISM = 100;
|
|
18
20
|
|
|
@@ -42,12 +44,12 @@ export const enqueue = mutation({
|
|
|
42
44
|
...workArgs,
|
|
43
45
|
attempts: 0,
|
|
44
46
|
});
|
|
47
|
+
const limit = await kickMainLoop(ctx, "enqueue", config);
|
|
45
48
|
await ctx.db.insert("pendingStart", {
|
|
46
49
|
workId,
|
|
47
|
-
segment: toSegment(runAt),
|
|
50
|
+
segment: max(toSegment(runAt), limit),
|
|
48
51
|
});
|
|
49
|
-
|
|
50
|
-
// TODO: stats event
|
|
52
|
+
recordEnqueued(console, { workId, fnName: workArgs.fnName, runAt });
|
|
51
53
|
return workId;
|
|
52
54
|
},
|
|
53
55
|
});
|
|
@@ -58,12 +60,14 @@ export const cancel = mutation({
|
|
|
58
60
|
logLevel,
|
|
59
61
|
},
|
|
60
62
|
handler: async (ctx, { id, logLevel }) => {
|
|
61
|
-
await ctx
|
|
62
|
-
|
|
63
|
-
segment
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
+
});
|
|
70
|
+
}
|
|
67
71
|
},
|
|
68
72
|
});
|
|
69
73
|
|
|
@@ -72,23 +76,28 @@ export const cancelAll = mutation({
|
|
|
72
76
|
args: { logLevel, before: v.optional(v.number()) },
|
|
73
77
|
handler: async (ctx, { logLevel, before }) => {
|
|
74
78
|
const beforeTime = before ?? Date.now();
|
|
75
|
-
const segment = nextSegment();
|
|
76
79
|
const pageOfWork = await ctx.db
|
|
77
80
|
.query("work")
|
|
78
81
|
.withIndex("by_creation_time", (q) => q.lte("_creationTime", beforeTime))
|
|
79
82
|
.order("desc")
|
|
80
83
|
.take(PAGE_SIZE);
|
|
84
|
+
const shouldCancel = await Promise.all(
|
|
85
|
+
pageOfWork.map(async ({ _id }) =>
|
|
86
|
+
shouldCancelWorkItem(ctx, _id, logLevel)
|
|
87
|
+
)
|
|
88
|
+
);
|
|
89
|
+
let segment = getNextSegment();
|
|
90
|
+
if (shouldCancel.some((c) => c)) {
|
|
91
|
+
segment = await kickMainLoop(ctx, "cancel", { logLevel });
|
|
92
|
+
}
|
|
81
93
|
await Promise.all(
|
|
82
|
-
pageOfWork.map(
|
|
83
|
-
if (
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
) {
|
|
89
|
-
return;
|
|
94
|
+
pageOfWork.map(({ _id }, index) => {
|
|
95
|
+
if (shouldCancel[index]) {
|
|
96
|
+
return ctx.db.insert("pendingCancelation", {
|
|
97
|
+
workId: _id,
|
|
98
|
+
segment,
|
|
99
|
+
});
|
|
90
100
|
}
|
|
91
|
-
await ctx.db.insert("pendingCancelation", { workId: _id, segment });
|
|
92
101
|
})
|
|
93
102
|
);
|
|
94
103
|
if (pageOfWork.length === PAGE_SIZE) {
|
|
@@ -97,7 +106,6 @@ export const cancelAll = mutation({
|
|
|
97
106
|
before: pageOfWork[pageOfWork.length - 1]._creationTime,
|
|
98
107
|
});
|
|
99
108
|
}
|
|
100
|
-
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
101
109
|
},
|
|
102
110
|
});
|
|
103
111
|
|
|
@@ -114,12 +122,42 @@ export const status = query({
|
|
|
114
122
|
.withIndex("workId", (q) => q.eq("workId", id))
|
|
115
123
|
.unique();
|
|
116
124
|
if (pendingStart) {
|
|
117
|
-
return { state: "pending",
|
|
125
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
126
|
+
}
|
|
127
|
+
const pendingCompletion = await ctx.db
|
|
128
|
+
.query("pendingCompletion")
|
|
129
|
+
.withIndex("workId", (q) => q.eq("workId", id))
|
|
130
|
+
.unique();
|
|
131
|
+
if (pendingCompletion?.retry) {
|
|
132
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
118
133
|
}
|
|
119
134
|
// Assume it's in progress. It could be pending cancelation
|
|
120
|
-
return { state: "running",
|
|
135
|
+
return { state: "running", previousAttempts: work.attempts } as const;
|
|
121
136
|
},
|
|
122
137
|
});
|
|
123
138
|
|
|
139
|
+
async function shouldCancelWorkItem(
|
|
140
|
+
ctx: MutationCtx,
|
|
141
|
+
workId: Id<"work">,
|
|
142
|
+
logLevel: LogLevel
|
|
143
|
+
) {
|
|
144
|
+
const console = createLogger(logLevel);
|
|
145
|
+
// No-op if the work doesn't exist or has completed.
|
|
146
|
+
const work = await ctx.db.get(workId);
|
|
147
|
+
if (!work) {
|
|
148
|
+
console.warn(`[cancel] work ${workId} doesn't exist`);
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
const pendingCancelation = await ctx.db
|
|
152
|
+
.query("pendingCancelation")
|
|
153
|
+
.withIndex("workId", (q) => q.eq("workId", workId))
|
|
154
|
+
.unique();
|
|
155
|
+
if (pendingCancelation) {
|
|
156
|
+
console.warn(`[cancel] work ${workId} has already been canceled`);
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
124
162
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
125
163
|
const console = "THIS IS A REMINDER TO USE createLogger";
|
|
@@ -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,60 +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
|
-
|
|
59
|
-
|
|
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) {
|
|
60
88
|
console.info(JSON.stringify(fullPayload));
|
|
61
89
|
}
|
|
62
90
|
},
|
|
63
91
|
};
|
|
64
92
|
}
|
|
65
|
-
export const logLevel = v.union(
|
|
66
|
-
v.literal("DEBUG"),
|
|
67
|
-
v.literal("INFO"),
|
|
68
|
-
v.literal("WARN"),
|
|
69
|
-
v.literal("ERROR")
|
|
70
|
-
);
|
|
71
|
-
export type LogLevel = Infer<typeof logLevel>;
|