@convex-dev/workpool 0.1.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/LICENSE +201 -0
- package/README.md +183 -0
- package/dist/commonjs/client/index.d.ts +53 -0
- package/dist/commonjs/client/index.d.ts.map +1 -0
- package/dist/commonjs/client/index.js +40 -0
- package/dist/commonjs/client/index.js.map +1 -0
- package/dist/commonjs/component/_generated/api.d.ts +14 -0
- package/dist/commonjs/component/_generated/api.d.ts.map +1 -0
- package/dist/commonjs/component/_generated/api.js +22 -0
- package/dist/commonjs/component/_generated/api.js.map +1 -0
- package/dist/commonjs/component/_generated/server.d.ts +64 -0
- package/dist/commonjs/component/_generated/server.d.ts.map +1 -0
- package/dist/commonjs/component/_generated/server.js +74 -0
- package/dist/commonjs/component/_generated/server.js.map +1 -0
- package/dist/commonjs/component/convex.config.d.ts +3 -0
- package/dist/commonjs/component/convex.config.d.ts.map +1 -0
- package/dist/commonjs/component/convex.config.js +6 -0
- package/dist/commonjs/component/convex.config.js.map +1 -0
- package/dist/commonjs/component/lib.d.ts +50 -0
- package/dist/commonjs/component/lib.d.ts.map +1 -0
- package/dist/commonjs/component/lib.js +562 -0
- package/dist/commonjs/component/lib.js.map +1 -0
- package/dist/commonjs/component/logging.d.ts +13 -0
- package/dist/commonjs/component/logging.d.ts.map +1 -0
- package/dist/commonjs/component/logging.js +41 -0
- package/dist/commonjs/component/logging.js.map +1 -0
- package/dist/commonjs/component/schema.d.ts +149 -0
- package/dist/commonjs/component/schema.d.ts.map +1 -0
- package/dist/commonjs/component/schema.js +85 -0
- package/dist/commonjs/component/schema.js.map +1 -0
- package/dist/commonjs/component/stats.d.ts +37 -0
- package/dist/commonjs/component/stats.d.ts.map +1 -0
- package/dist/commonjs/component/stats.js +75 -0
- package/dist/commonjs/component/stats.js.map +1 -0
- package/dist/commonjs/package.json +3 -0
- package/dist/esm/client/index.d.ts +53 -0
- package/dist/esm/client/index.d.ts.map +1 -0
- package/dist/esm/client/index.js +40 -0
- package/dist/esm/client/index.js.map +1 -0
- package/dist/esm/component/_generated/api.d.ts +14 -0
- package/dist/esm/component/_generated/api.d.ts.map +1 -0
- package/dist/esm/component/_generated/api.js +22 -0
- package/dist/esm/component/_generated/api.js.map +1 -0
- package/dist/esm/component/_generated/server.d.ts +64 -0
- package/dist/esm/component/_generated/server.d.ts.map +1 -0
- package/dist/esm/component/_generated/server.js +74 -0
- package/dist/esm/component/_generated/server.js.map +1 -0
- package/dist/esm/component/convex.config.d.ts +3 -0
- package/dist/esm/component/convex.config.d.ts.map +1 -0
- package/dist/esm/component/convex.config.js +6 -0
- package/dist/esm/component/convex.config.js.map +1 -0
- package/dist/esm/component/lib.d.ts +50 -0
- package/dist/esm/component/lib.d.ts.map +1 -0
- package/dist/esm/component/lib.js +562 -0
- package/dist/esm/component/lib.js.map +1 -0
- package/dist/esm/component/logging.d.ts +13 -0
- package/dist/esm/component/logging.d.ts.map +1 -0
- package/dist/esm/component/logging.js +41 -0
- package/dist/esm/component/logging.js.map +1 -0
- package/dist/esm/component/schema.d.ts +149 -0
- package/dist/esm/component/schema.d.ts.map +1 -0
- package/dist/esm/component/schema.js +85 -0
- package/dist/esm/component/schema.js.map +1 -0
- package/dist/esm/component/stats.d.ts +37 -0
- package/dist/esm/component/stats.d.ts.map +1 -0
- package/dist/esm/component/stats.js +75 -0
- package/dist/esm/component/stats.js.map +1 -0
- package/dist/esm/package.json +3 -0
- package/package.json +83 -0
- package/src/client/index.ts +121 -0
- package/src/component/_generated/api.d.ts +133 -0
- package/src/component/_generated/api.js +23 -0
- package/src/component/_generated/dataModel.d.ts +60 -0
- package/src/component/_generated/server.d.ts +149 -0
- package/src/component/_generated/server.js +90 -0
- package/src/component/convex.config.ts +8 -0
- package/src/component/lib.ts +670 -0
- package/src/component/logging.ts +59 -0
- package/src/component/schema.ts +103 -0
- package/src/component/stats.ts +90 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
|
|
3
|
+
import { Infer } from "convex/values";
|
|
4
|
+
|
|
5
|
+
export const logLevel = v.union(
|
|
6
|
+
v.literal("DEBUG"),
|
|
7
|
+
v.literal("INFO"),
|
|
8
|
+
v.literal("WARN"),
|
|
9
|
+
v.literal("ERROR")
|
|
10
|
+
);
|
|
11
|
+
export type LogLevel = Infer<typeof logLevel>;
|
|
12
|
+
|
|
13
|
+
export type Logger = {
|
|
14
|
+
debug: (...args: unknown[]) => void;
|
|
15
|
+
info: (...args: unknown[]) => void;
|
|
16
|
+
warn: (...args: unknown[]) => void;
|
|
17
|
+
error: (...args: unknown[]) => void;
|
|
18
|
+
time: (label: string) => void;
|
|
19
|
+
timeEnd: (label: string) => void;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export function createLogger(level: LogLevel): Logger {
|
|
23
|
+
const levelIndex = ["DEBUG", "INFO", "WARN", "ERROR"].indexOf(level);
|
|
24
|
+
if (levelIndex === -1) {
|
|
25
|
+
throw new Error(`Invalid log level: ${level}`);
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
debug: (...args: unknown[]) => {
|
|
29
|
+
if (levelIndex <= 0) {
|
|
30
|
+
console.debug(...args);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
info: (...args: unknown[]) => {
|
|
34
|
+
if (levelIndex <= 1) {
|
|
35
|
+
console.info(...args);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
warn: (...args: unknown[]) => {
|
|
39
|
+
if (levelIndex <= 2) {
|
|
40
|
+
console.warn(...args);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
error: (...args: unknown[]) => {
|
|
44
|
+
if (levelIndex <= 3) {
|
|
45
|
+
console.error(...args);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
time: (label: string) => {
|
|
49
|
+
if (levelIndex <= 0) {
|
|
50
|
+
console.time(label);
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
timeEnd: (label: string) => {
|
|
54
|
+
if (levelIndex <= 0) {
|
|
55
|
+
console.timeEnd(label);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { Infer, v } from "convex/values";
|
|
3
|
+
import { logLevel } from "./logging";
|
|
4
|
+
|
|
5
|
+
export const completionStatus = v.union(
|
|
6
|
+
v.literal("success"),
|
|
7
|
+
v.literal("error"),
|
|
8
|
+
v.literal("canceled"),
|
|
9
|
+
v.literal("timeout")
|
|
10
|
+
);
|
|
11
|
+
export type CompletionStatus = Infer<typeof completionStatus>;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
Data flow:
|
|
15
|
+
|
|
16
|
+
- The mutation `mainLoop` runs periodically and serially.
|
|
17
|
+
- Several tables act as queues, with client-driven mutations enqueueing at high
|
|
18
|
+
timestamps and `mainLoop` popping at low timestamps:
|
|
19
|
+
pendingStart, pendingCompletion, and pendingCancelation.
|
|
20
|
+
- The `enqueue` mutation writes to pendingStart.
|
|
21
|
+
- The `cancel` mutation writes to pendingCancelation.
|
|
22
|
+
- The `saveResult` mutation, run as part of scheduled work, writes to pendingCompletion.
|
|
23
|
+
- mainLoop processes the queues:
|
|
24
|
+
- pendingStart => inProgressWork.
|
|
25
|
+
- pendingCompletion and pendingCancelation => completedWork.
|
|
26
|
+
- inProgressWork that finishes uncleanly (timeout or system failure) => completedWork.
|
|
27
|
+
- `mainLoop` schedules itself to run.
|
|
28
|
+
- `enqueue`, `cancel`, and `saveResult` mutations check when `mainLoop` is scheduled to run,
|
|
29
|
+
and if it's too far in the future, they schedule it to run sooner.
|
|
30
|
+
- `status` query reads from pendingWork and completedWork.
|
|
31
|
+
- `cleanup` mutation deletes old rows from completedWork.
|
|
32
|
+
|
|
33
|
+
To avoid OCCs, we restrict which mutations can read and write from each table:
|
|
34
|
+
- pools: read by all, written only when static Workpool options change.
|
|
35
|
+
- mainLoop (table): read by all, written mostly by `mainLoop`.
|
|
36
|
+
If `mainLoop` will not run for a while, mainLoop table is written by `enqueue`, `cancel`, or `saveResult`.
|
|
37
|
+
- pendingWork: `enqueue` inserts at high timestamps, `mainLoop` pops at low timestamps. `status` query does point-reads.
|
|
38
|
+
- pendingCompletion: `saveResult` inserts at high timestamps, `mainLoop` pops at low timestamps.
|
|
39
|
+
- pendingCancelation: `cancel` inserts at high timestamps, `mainLoop` pops at low timestamps.
|
|
40
|
+
- inProgressWork: `mainLoop` inserts, reads all, and deletes.
|
|
41
|
+
- completedWork: `mainLoop` inserts at hight timestamps, `status` query reads, `cleanup` deletes at low timestamps.
|
|
42
|
+
|
|
43
|
+
*/
|
|
44
|
+
|
|
45
|
+
export default defineSchema({
|
|
46
|
+
// Statically configured, singleton.
|
|
47
|
+
pool: defineTable({
|
|
48
|
+
maxParallelism: v.number(),
|
|
49
|
+
statusTtl: v.number(),
|
|
50
|
+
logLevel,
|
|
51
|
+
}),
|
|
52
|
+
|
|
53
|
+
mainLoop: defineTable({
|
|
54
|
+
state: v.union(
|
|
55
|
+
v.object({ kind: v.literal("running") }),
|
|
56
|
+
v.object({
|
|
57
|
+
kind: v.literal("scheduled"),
|
|
58
|
+
runAtTime: v.number(),
|
|
59
|
+
fn: v.id("_scheduled_functions"),
|
|
60
|
+
}),
|
|
61
|
+
v.object({ kind: v.literal("idle") })
|
|
62
|
+
),
|
|
63
|
+
}),
|
|
64
|
+
|
|
65
|
+
work: defineTable({
|
|
66
|
+
fnType: v.union(v.literal("action"), v.literal("mutation")),
|
|
67
|
+
fnHandle: v.string(),
|
|
68
|
+
fnName: v.string(),
|
|
69
|
+
fnArgs: v.any(),
|
|
70
|
+
}),
|
|
71
|
+
|
|
72
|
+
pendingStart: defineTable({
|
|
73
|
+
workId: v.id("work"),
|
|
74
|
+
}).index("workId", ["workId"]),
|
|
75
|
+
pendingCompletion: defineTable({
|
|
76
|
+
generation: v.number(),
|
|
77
|
+
completionStatus,
|
|
78
|
+
workId: v.id("work"),
|
|
79
|
+
})
|
|
80
|
+
.index("workId", ["workId"])
|
|
81
|
+
.index("generation", ["generation"]),
|
|
82
|
+
pendingCancelation: defineTable({
|
|
83
|
+
workId: v.id("work"),
|
|
84
|
+
}),
|
|
85
|
+
|
|
86
|
+
inProgressWork: defineTable({
|
|
87
|
+
running: v.id("_scheduled_functions"),
|
|
88
|
+
timeoutMs: v.union(v.number(), v.null()),
|
|
89
|
+
workId: v.id("work"),
|
|
90
|
+
}).index("workId", ["workId"]),
|
|
91
|
+
inProgressCount: defineTable({
|
|
92
|
+
count: v.number(),
|
|
93
|
+
}),
|
|
94
|
+
|
|
95
|
+
completedWork: defineTable({
|
|
96
|
+
completionStatus,
|
|
97
|
+
workId: v.id("work"),
|
|
98
|
+
}).index("workId", ["workId"]),
|
|
99
|
+
|
|
100
|
+
completionGeneration: defineTable({
|
|
101
|
+
generation: v.number(),
|
|
102
|
+
}),
|
|
103
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { Doc } from "./_generated/dataModel";
|
|
3
|
+
import { internalQuery } from "./_generated/server";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Record stats about work execution. Intended to be queried by Axiom or Datadog.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Sample axiom dashboard query:
|
|
11
|
+
|
|
12
|
+
workpool
|
|
13
|
+
| extend parsed_message = iff(
|
|
14
|
+
isnotnull(parse_json(trim("'", tostring(["data.message"])))),
|
|
15
|
+
parse_json(trim("'", tostring(["data.message"]))),
|
|
16
|
+
parse_json('{}')
|
|
17
|
+
)
|
|
18
|
+
| extend lagSinceEnqueued = parsed_message["lagSinceEnqueued"]
|
|
19
|
+
| extend fnName = parsed_message["fnName"]
|
|
20
|
+
| summarize avg(todouble(lagSinceEnqueued)) by bin_auto(_time), tostring(fnName)
|
|
21
|
+
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
export function recordStarted(work: Doc<"work">): string {
|
|
25
|
+
return JSON.stringify({
|
|
26
|
+
workId: work._id,
|
|
27
|
+
event: "started",
|
|
28
|
+
fnName: work.fnName,
|
|
29
|
+
enqueuedAt: work._creationTime,
|
|
30
|
+
startedAt: Date.now(),
|
|
31
|
+
lagSinceEnqueued: Date.now() - work._creationTime,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function recordCompleted(
|
|
36
|
+
work: Doc<"work">,
|
|
37
|
+
status: "success" | "error" | "canceled" | "timeout"
|
|
38
|
+
): string {
|
|
39
|
+
return JSON.stringify({
|
|
40
|
+
workId: work._id,
|
|
41
|
+
event: "completed",
|
|
42
|
+
fnName: work.fnName,
|
|
43
|
+
completedAt: Date.now(),
|
|
44
|
+
status,
|
|
45
|
+
lagSinceEnqueued: Date.now() - work._creationTime,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Warning: this should not be used from a mutation, as it will cause conflicts.
|
|
51
|
+
* Use this to debug or diagnose your queue length when it's backed up.
|
|
52
|
+
*/
|
|
53
|
+
export const queueLength = internalQuery({
|
|
54
|
+
args: {},
|
|
55
|
+
returns: v.number(),
|
|
56
|
+
handler: async (ctx) => {
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
58
|
+
return (ctx.db.query("pendingStart") as any).count();
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Warning: this should not be used from a mutation, as it will cause conflicts.
|
|
64
|
+
* Use this while developing to see the state of the queue.
|
|
65
|
+
*/
|
|
66
|
+
export const debugCounts = internalQuery({
|
|
67
|
+
args: {},
|
|
68
|
+
returns: v.any(),
|
|
69
|
+
handler: async (ctx) => {
|
|
70
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
71
|
+
const inProgressWork = await (
|
|
72
|
+
ctx.db.query("inProgressWork") as any
|
|
73
|
+
).count();
|
|
74
|
+
const pendingStart = await (ctx.db.query("pendingStart") as any).count();
|
|
75
|
+
const pendingCompletion = await (
|
|
76
|
+
ctx.db.query("pendingCompletion") as any
|
|
77
|
+
).count();
|
|
78
|
+
const pendingCancelation = await (
|
|
79
|
+
ctx.db.query("pendingCancelation") as any
|
|
80
|
+
).count();
|
|
81
|
+
return {
|
|
82
|
+
pendingStart,
|
|
83
|
+
inProgressWork,
|
|
84
|
+
pendingCompletion,
|
|
85
|
+
pendingCancelation,
|
|
86
|
+
active: inProgressWork - pendingCompletion - pendingCancelation,
|
|
87
|
+
};
|
|
88
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
89
|
+
},
|
|
90
|
+
});
|