@convex-dev/workpool 0.1.2 → 0.2.0-alpha.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 +155 -17
- package/dist/commonjs/client/index.d.ts +123 -35
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +122 -15
- package/dist/commonjs/client/index.js.map +1 -1
- package/dist/commonjs/client/utils.d.ts +16 -0
- package/dist/commonjs/client/utils.d.ts.map +1 -0
- package/dist/commonjs/client/utils.js +2 -0
- package/dist/commonjs/client/utils.js.map +1 -0
- 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/convex.config.d.ts.map +1 -1
- package/dist/commonjs/component/convex.config.js +0 -2
- package/dist/commonjs/component/convex.config.js.map +1 -1
- package/dist/commonjs/component/kick.d.ts +9 -0
- package/dist/commonjs/component/kick.d.ts.map +1 -0
- package/dist/commonjs/component/kick.js +97 -0
- package/dist/commonjs/component/kick.js.map +1 -0
- package/dist/commonjs/component/lib.d.ts +23 -32
- package/dist/commonjs/component/lib.d.ts.map +1 -1
- package/dist/commonjs/component/lib.js +91 -563
- package/dist/commonjs/component/lib.js.map +1 -1
- package/dist/commonjs/component/logging.d.ts +5 -3
- package/dist/commonjs/component/logging.d.ts.map +1 -1
- package/dist/commonjs/component/logging.js +13 -2
- package/dist/commonjs/component/logging.js.map +1 -1
- package/dist/commonjs/component/loop.d.ts +13 -0
- package/dist/commonjs/component/loop.d.ts.map +1 -0
- package/dist/commonjs/component/loop.js +482 -0
- package/dist/commonjs/component/loop.js.map +1 -0
- package/dist/commonjs/component/recovery.d.ts +24 -0
- package/dist/commonjs/component/recovery.d.ts.map +1 -0
- package/dist/commonjs/component/recovery.js +94 -0
- package/dist/commonjs/component/recovery.js.map +1 -0
- package/dist/commonjs/component/schema.d.ts +167 -93
- package/dist/commonjs/component/schema.d.ts.map +1 -1
- package/dist/commonjs/component/schema.js +56 -65
- package/dist/commonjs/component/schema.js.map +1 -1
- package/dist/commonjs/component/shared.d.ts +138 -0
- package/dist/commonjs/component/shared.d.ts.map +1 -0
- package/dist/commonjs/component/shared.js +77 -0
- package/dist/commonjs/component/shared.js.map +1 -0
- package/dist/commonjs/component/stats.d.ts +6 -3
- package/dist/commonjs/component/stats.d.ts.map +1 -1
- package/dist/commonjs/component/stats.js +23 -4
- package/dist/commonjs/component/stats.js.map +1 -1
- package/dist/commonjs/component/worker.d.ts +15 -0
- package/dist/commonjs/component/worker.d.ts.map +1 -0
- package/dist/commonjs/component/worker.js +73 -0
- package/dist/commonjs/component/worker.js.map +1 -0
- package/dist/esm/client/index.d.ts +123 -35
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +122 -15
- package/dist/esm/client/index.js.map +1 -1
- package/dist/esm/client/utils.d.ts +16 -0
- package/dist/esm/client/utils.d.ts.map +1 -0
- package/dist/esm/client/utils.js +2 -0
- package/dist/esm/client/utils.js.map +1 -0
- 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/convex.config.d.ts.map +1 -1
- package/dist/esm/component/convex.config.js +0 -2
- package/dist/esm/component/convex.config.js.map +1 -1
- package/dist/esm/component/kick.d.ts +9 -0
- package/dist/esm/component/kick.d.ts.map +1 -0
- package/dist/esm/component/kick.js +97 -0
- package/dist/esm/component/kick.js.map +1 -0
- package/dist/esm/component/lib.d.ts +23 -32
- package/dist/esm/component/lib.d.ts.map +1 -1
- package/dist/esm/component/lib.js +91 -563
- package/dist/esm/component/lib.js.map +1 -1
- package/dist/esm/component/logging.d.ts +5 -3
- package/dist/esm/component/logging.d.ts.map +1 -1
- package/dist/esm/component/logging.js +13 -2
- package/dist/esm/component/logging.js.map +1 -1
- package/dist/esm/component/loop.d.ts +13 -0
- package/dist/esm/component/loop.d.ts.map +1 -0
- package/dist/esm/component/loop.js +482 -0
- package/dist/esm/component/loop.js.map +1 -0
- package/dist/esm/component/recovery.d.ts +24 -0
- package/dist/esm/component/recovery.d.ts.map +1 -0
- package/dist/esm/component/recovery.js +94 -0
- package/dist/esm/component/recovery.js.map +1 -0
- package/dist/esm/component/schema.d.ts +167 -93
- package/dist/esm/component/schema.d.ts.map +1 -1
- package/dist/esm/component/schema.js +56 -65
- package/dist/esm/component/schema.js.map +1 -1
- package/dist/esm/component/shared.d.ts +138 -0
- package/dist/esm/component/shared.d.ts.map +1 -0
- package/dist/esm/component/shared.js +77 -0
- package/dist/esm/component/shared.js.map +1 -0
- package/dist/esm/component/stats.d.ts +6 -3
- package/dist/esm/component/stats.d.ts.map +1 -1
- package/dist/esm/component/stats.js +23 -4
- package/dist/esm/component/stats.js.map +1 -1
- package/dist/esm/component/worker.d.ts +15 -0
- package/dist/esm/component/worker.d.ts.map +1 -0
- package/dist/esm/component/worker.js +73 -0
- package/dist/esm/component/worker.js.map +1 -0
- package/package.json +6 -5
- package/src/client/index.ts +232 -68
- package/src/client/utils.ts +45 -0
- package/src/component/README.md +73 -0
- package/src/component/_generated/api.d.ts +38 -66
- package/src/component/complete.test.ts +508 -0
- package/src/component/complete.ts +98 -0
- package/src/component/convex.config.ts +0 -3
- package/src/component/kick.test.ts +285 -0
- package/src/component/kick.ts +118 -0
- package/src/component/lib.test.ts +448 -0
- package/src/component/lib.ts +105 -667
- package/src/component/logging.ts +24 -12
- package/src/component/loop.test.ts +1204 -0
- package/src/component/loop.ts +637 -0
- package/src/component/recovery.test.ts +541 -0
- package/src/component/recovery.ts +96 -0
- package/src/component/schema.ts +61 -77
- package/src/component/setup.test.ts +5 -0
- package/src/component/shared.ts +141 -0
- package/src/component/stats.ts +26 -8
- package/src/component/worker.ts +81 -0
package/src/component/schema.ts
CHANGED
|
@@ -1,107 +1,91 @@
|
|
|
1
1
|
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import { config, onComplete, retryBehavior, runResult } from "./shared.js";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
*/
|
|
5
|
+
// Represents a slice of time to process work.
|
|
6
|
+
const segment = v.int64();
|
|
44
7
|
|
|
45
8
|
export default defineSchema({
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
9
|
+
// Written from kickLoop, read everywhere.
|
|
10
|
+
globals: defineTable(config),
|
|
11
|
+
// Singleton, only read & written by `main`.
|
|
12
|
+
internalState: defineTable({
|
|
13
|
+
// Ensure that only one main is running at a time.
|
|
14
|
+
generation: v.int64(),
|
|
15
|
+
segmentCursors: v.object({
|
|
16
|
+
incoming: segment,
|
|
17
|
+
completion: segment,
|
|
18
|
+
cancelation: segment,
|
|
19
|
+
}),
|
|
20
|
+
lastRecovery: segment,
|
|
21
|
+
report: v.object({
|
|
22
|
+
completed: v.number(), // finished running, counts retries & failures
|
|
23
|
+
succeeded: v.number(), // finished successfully, regardless of retries
|
|
24
|
+
failed: v.number(), // failed after all retries
|
|
25
|
+
retries: v.number(), // failure that turned into a retry
|
|
26
|
+
canceled: v.number(), // cancelations processed
|
|
27
|
+
lastReportTs: v.number(),
|
|
28
|
+
}),
|
|
29
|
+
running: v.array(
|
|
30
|
+
v.object({
|
|
31
|
+
workId: v.id("work"),
|
|
32
|
+
scheduledId: v.id("_scheduled_functions"),
|
|
33
|
+
started: v.number(),
|
|
34
|
+
})
|
|
35
|
+
),
|
|
51
36
|
}),
|
|
52
37
|
|
|
53
|
-
|
|
38
|
+
// Singleton, written by `updateRunStatus` when running, by client or worker otherwise.
|
|
39
|
+
// Safe to read from kickLoop, since it should update infrequently.
|
|
40
|
+
runStatus: defineTable({
|
|
54
41
|
state: v.union(
|
|
55
42
|
v.object({ kind: v.literal("running") }),
|
|
56
43
|
v.object({
|
|
57
44
|
kind: v.literal("scheduled"),
|
|
58
|
-
|
|
59
|
-
|
|
45
|
+
segment,
|
|
46
|
+
scheduledId: v.id("_scheduled_functions"),
|
|
47
|
+
saturated: v.boolean(),
|
|
48
|
+
generation: v.int64(),
|
|
60
49
|
}),
|
|
61
|
-
v.object({ kind: v.literal("idle") })
|
|
50
|
+
v.object({ kind: v.literal("idle"), generation: v.int64() })
|
|
62
51
|
),
|
|
63
52
|
}),
|
|
64
53
|
|
|
54
|
+
// Written on enqueue. Deleted by `complete` for success, failure, canceled.
|
|
65
55
|
work: defineTable({
|
|
66
56
|
fnType: v.union(v.literal("action"), v.literal("mutation")),
|
|
67
57
|
fnHandle: v.string(),
|
|
68
58
|
fnName: v.string(),
|
|
69
59
|
fnArgs: v.any(),
|
|
60
|
+
attempts: v.number(), // number of completed attempts
|
|
61
|
+
onComplete: v.optional(onComplete),
|
|
62
|
+
retryBehavior: v.optional(retryBehavior),
|
|
63
|
+
canceled: v.optional(v.boolean()),
|
|
70
64
|
}),
|
|
71
65
|
|
|
66
|
+
// Written on enqueue & rescheduled for retry, read & deleted by `main`.
|
|
72
67
|
pendingStart: defineTable({
|
|
73
68
|
workId: v.id("work"),
|
|
74
|
-
|
|
75
|
-
pendingCompletion: defineTable({
|
|
76
|
-
generation: v.number(),
|
|
77
|
-
completionStatus,
|
|
78
|
-
workId: v.id("work"),
|
|
69
|
+
segment,
|
|
79
70
|
})
|
|
80
71
|
.index("workId", ["workId"])
|
|
81
|
-
.index("
|
|
82
|
-
pendingCancelation: defineTable({
|
|
83
|
-
workId: v.id("work"),
|
|
84
|
-
}),
|
|
72
|
+
.index("segment", ["segment"]),
|
|
85
73
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
74
|
+
// Written by complete, read & deleted by `main`.
|
|
75
|
+
pendingCompletion: defineTable({
|
|
76
|
+
segment,
|
|
77
|
+
runResult,
|
|
89
78
|
workId: v.id("work"),
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
79
|
+
retry: v.boolean(),
|
|
80
|
+
})
|
|
81
|
+
.index("workId", ["workId"])
|
|
82
|
+
.index("segment", ["segment"]),
|
|
94
83
|
|
|
95
|
-
|
|
96
|
-
|
|
84
|
+
// Written on cancelation, read & deleted by `main`.
|
|
85
|
+
pendingCancelation: defineTable({
|
|
86
|
+
segment,
|
|
97
87
|
workId: v.id("work"),
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
generation: v.number(),
|
|
102
|
-
}),
|
|
103
|
-
|
|
104
|
-
pendingStartCursor: defineTable({
|
|
105
|
-
cursor: v.number(),
|
|
106
|
-
}),
|
|
88
|
+
})
|
|
89
|
+
.index("workId", ["workId"])
|
|
90
|
+
.index("segment", ["segment"]),
|
|
107
91
|
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { Infer } from "convex/values";
|
|
2
|
+
|
|
3
|
+
import { v } from "convex/values";
|
|
4
|
+
import { Logger, logLevel } from "./logging.js";
|
|
5
|
+
|
|
6
|
+
const SEGMENT_MS = 100;
|
|
7
|
+
export const SECOND = 1000;
|
|
8
|
+
export const MINUTE = 60 * SECOND;
|
|
9
|
+
export const HOUR = 60 * MINUTE;
|
|
10
|
+
export const DAY = 24 * HOUR;
|
|
11
|
+
export const YEAR = 365 * DAY;
|
|
12
|
+
|
|
13
|
+
export function toSegment(ms: number): bigint {
|
|
14
|
+
return BigInt(Math.floor(ms / SEGMENT_MS));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function currentSegment(): bigint {
|
|
18
|
+
return toSegment(Date.now());
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function nextSegment(): bigint {
|
|
22
|
+
return toSegment(Date.now()) + 1n;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function fromSegment(segment: bigint): number {
|
|
26
|
+
return Number(segment) * SEGMENT_MS;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const config = v.object({
|
|
30
|
+
maxParallelism: v.number(),
|
|
31
|
+
logLevel,
|
|
32
|
+
});
|
|
33
|
+
export type Config = Infer<typeof config>;
|
|
34
|
+
|
|
35
|
+
export const retryBehavior = v.object({
|
|
36
|
+
maxAttempts: v.number(),
|
|
37
|
+
initialBackoffMs: v.number(),
|
|
38
|
+
base: v.number(),
|
|
39
|
+
});
|
|
40
|
+
export type RetryBehavior = {
|
|
41
|
+
/**
|
|
42
|
+
* The maximum number of attempts to make. 2 means one retry.
|
|
43
|
+
*/
|
|
44
|
+
maxAttempts: number;
|
|
45
|
+
/**
|
|
46
|
+
* The initial backoff time in milliseconds. 100 means wait 100ms before the
|
|
47
|
+
* first retry.
|
|
48
|
+
*/
|
|
49
|
+
initialBackoffMs: number;
|
|
50
|
+
/**
|
|
51
|
+
* The base for the backoff. 2 means double the backoff each time.
|
|
52
|
+
* e.g. if the initial backoff is 100ms, and the base is 2, then the first
|
|
53
|
+
* retry will wait 200ms, the second will wait 400ms, etc.
|
|
54
|
+
*/
|
|
55
|
+
base: number;
|
|
56
|
+
};
|
|
57
|
+
// This ensures that the type satisfies the schema.
|
|
58
|
+
const _ = {} as RetryBehavior satisfies Infer<typeof retryBehavior>;
|
|
59
|
+
|
|
60
|
+
export const runResult = v.union(
|
|
61
|
+
v.object({
|
|
62
|
+
kind: v.literal("success"),
|
|
63
|
+
returnValue: v.any(),
|
|
64
|
+
}),
|
|
65
|
+
v.object({
|
|
66
|
+
kind: v.literal("failed"),
|
|
67
|
+
error: v.string(),
|
|
68
|
+
}),
|
|
69
|
+
v.object({
|
|
70
|
+
kind: v.literal("canceled"),
|
|
71
|
+
})
|
|
72
|
+
);
|
|
73
|
+
export type RunResult = Infer<typeof runResult>;
|
|
74
|
+
|
|
75
|
+
export const onComplete = v.object({
|
|
76
|
+
fnHandle: v.string(), // mutation
|
|
77
|
+
context: v.optional(v.any()),
|
|
78
|
+
});
|
|
79
|
+
export type OnComplete = Infer<typeof onComplete>;
|
|
80
|
+
|
|
81
|
+
export type OnCompleteArgs = {
|
|
82
|
+
/**
|
|
83
|
+
* The ID of the work that completed.
|
|
84
|
+
*/
|
|
85
|
+
workId: string;
|
|
86
|
+
/**
|
|
87
|
+
* The context object passed when enqueuing the work.
|
|
88
|
+
* Useful for passing data from the enqueue site to the onComplete site.
|
|
89
|
+
*/
|
|
90
|
+
context: unknown;
|
|
91
|
+
/**
|
|
92
|
+
* The result of the run that completed.
|
|
93
|
+
*/
|
|
94
|
+
result: RunResult;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const status = v.union(
|
|
98
|
+
v.union(
|
|
99
|
+
v.object({
|
|
100
|
+
state: v.literal("pending"),
|
|
101
|
+
previousAttempts: v.number(),
|
|
102
|
+
}),
|
|
103
|
+
v.object({
|
|
104
|
+
state: v.literal("running"),
|
|
105
|
+
previousAttempts: v.number(),
|
|
106
|
+
}),
|
|
107
|
+
v.object({
|
|
108
|
+
state: v.literal("finished"),
|
|
109
|
+
})
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
export type Status = Infer<typeof status>;
|
|
113
|
+
|
|
114
|
+
export function boundScheduledTime(ms: number, console: Logger): number {
|
|
115
|
+
if (ms < Date.now() - YEAR) {
|
|
116
|
+
console.error("scheduled time is too old, defaulting to now", ms);
|
|
117
|
+
return Date.now();
|
|
118
|
+
}
|
|
119
|
+
if (ms > Date.now() + 4 * YEAR) {
|
|
120
|
+
console.error(
|
|
121
|
+
"scheduled time is too far in the future, defaulting to 1 year from now",
|
|
122
|
+
ms
|
|
123
|
+
);
|
|
124
|
+
return Date.now() + YEAR;
|
|
125
|
+
}
|
|
126
|
+
return ms;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Returns the smaller of two bigint values.
|
|
131
|
+
*/
|
|
132
|
+
export function min<T extends bigint>(a: T, b: T): T {
|
|
133
|
+
return a > b ? b : a;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Returns the larger of two bigint values.
|
|
138
|
+
*/
|
|
139
|
+
export function max<T extends bigint>(a: T, b: T): T {
|
|
140
|
+
return a < b ? b : a;
|
|
141
|
+
}
|
package/src/component/stats.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
-
import { Doc } from "./_generated/dataModel";
|
|
3
|
-
import { internalQuery } from "./_generated/server";
|
|
2
|
+
import { Doc } from "./_generated/dataModel.js";
|
|
3
|
+
import { internalQuery } from "./_generated/server.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Record stats about work execution. Intended to be queried by Axiom or Datadog.
|
|
@@ -34,18 +34,34 @@ export function recordStarted(work: Doc<"work">): string {
|
|
|
34
34
|
|
|
35
35
|
export function recordCompleted(
|
|
36
36
|
work: Doc<"work">,
|
|
37
|
-
status: "success" | "
|
|
37
|
+
status: "success" | "failed" | "canceled" | "retrying"
|
|
38
38
|
): string {
|
|
39
39
|
return JSON.stringify({
|
|
40
40
|
workId: work._id,
|
|
41
41
|
event: "completed",
|
|
42
42
|
fnName: work.fnName,
|
|
43
43
|
completedAt: Date.now(),
|
|
44
|
+
attempts: work.attempts,
|
|
44
45
|
status,
|
|
45
46
|
lagSinceEnqueued: Date.now() - work._creationTime,
|
|
46
47
|
});
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
export function recordReport(state: Doc<"internalState">): string {
|
|
51
|
+
const { completed, succeeded, failed, retries, canceled } = state.report;
|
|
52
|
+
const withoutRetries = completed - retries;
|
|
53
|
+
return JSON.stringify({
|
|
54
|
+
event: "report",
|
|
55
|
+
completed,
|
|
56
|
+
succeeded,
|
|
57
|
+
failed,
|
|
58
|
+
retries,
|
|
59
|
+
canceled,
|
|
60
|
+
failureRate: completed ? (failed + retries) / completed : 0,
|
|
61
|
+
permanentFailureRate: withoutRetries ? failed / withoutRetries : 0,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* Warning: this should not be used from a mutation, as it will cause conflicts.
|
|
51
67
|
* Use this to debug or diagnose your queue length when it's backed up.
|
|
@@ -67,10 +83,9 @@ export const debugCounts = internalQuery({
|
|
|
67
83
|
args: {},
|
|
68
84
|
returns: v.any(),
|
|
69
85
|
handler: async (ctx) => {
|
|
86
|
+
const internalState = await ctx.db.query("internalState").unique();
|
|
87
|
+
const inProgressWork = internalState?.running.length ?? 0;
|
|
70
88
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
71
|
-
const inProgressWork = await (
|
|
72
|
-
ctx.db.query("inProgressWork") as any
|
|
73
|
-
).count();
|
|
74
89
|
const pendingStart = await (ctx.db.query("pendingStart") as any).count();
|
|
75
90
|
const pendingCompletion = await (
|
|
76
91
|
ctx.db.query("pendingCompletion") as any
|
|
@@ -78,13 +93,16 @@ export const debugCounts = internalQuery({
|
|
|
78
93
|
const pendingCancelation = await (
|
|
79
94
|
ctx.db.query("pendingCancelation") as any
|
|
80
95
|
).count();
|
|
96
|
+
const runStatus = await ctx.db.query("runStatus").unique();
|
|
97
|
+
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
81
98
|
return {
|
|
82
99
|
pendingStart,
|
|
83
100
|
inProgressWork,
|
|
84
101
|
pendingCompletion,
|
|
85
102
|
pendingCancelation,
|
|
86
|
-
active: inProgressWork - pendingCompletion
|
|
103
|
+
active: inProgressWork - pendingCompletion,
|
|
104
|
+
runStatus: runStatus?.state.kind,
|
|
105
|
+
generation: internalState?.generation,
|
|
87
106
|
};
|
|
88
|
-
/* eslint-enable @typescript-eslint/no-explicit-any */
|
|
89
107
|
},
|
|
90
108
|
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Responsible for all the functions around doing the work.
|
|
3
|
+
* Should not touch any of loop's tables other than writing to `pendingCompletion`.
|
|
4
|
+
* It is not responsible for handling retries.
|
|
5
|
+
*/
|
|
6
|
+
import type { FunctionHandle } from "convex/server";
|
|
7
|
+
import { v } from "convex/values";
|
|
8
|
+
import { internal } from "./_generated/api.js";
|
|
9
|
+
import { internalAction, internalMutation } from "./_generated/server.js";
|
|
10
|
+
import { createLogger, logLevel } from "./logging.js";
|
|
11
|
+
import type { RunResult } from "./shared.js";
|
|
12
|
+
|
|
13
|
+
export const runMutationWrapper = internalMutation({
|
|
14
|
+
args: {
|
|
15
|
+
workId: v.id("work"),
|
|
16
|
+
fnHandle: v.string(),
|
|
17
|
+
fnArgs: v.any(),
|
|
18
|
+
logLevel,
|
|
19
|
+
attempt: v.number(),
|
|
20
|
+
},
|
|
21
|
+
handler: async (ctx, { workId, attempt, ...args }) => {
|
|
22
|
+
const console = createLogger(args.logLevel);
|
|
23
|
+
const fnHandle = args.fnHandle as FunctionHandle<"mutation">;
|
|
24
|
+
try {
|
|
25
|
+
const returnValue = await ctx.runMutation(fnHandle, args.fnArgs);
|
|
26
|
+
// NOTE: we could run the `saveResult` handler here, or call `ctx.runMutation`,
|
|
27
|
+
// but we want the mutation to be a separate transaction to reduce the window for OCCs.
|
|
28
|
+
await ctx.scheduler.runAfter(0, internal.complete.complete, {
|
|
29
|
+
jobs: [
|
|
30
|
+
{ workId, runResult: { kind: "success", returnValue }, attempt },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
} catch (e: unknown) {
|
|
34
|
+
console.error(e);
|
|
35
|
+
const runResult = { kind: "failed" as const, error: formatError(e) };
|
|
36
|
+
await ctx.scheduler.runAfter(0, internal.complete.complete, {
|
|
37
|
+
jobs: [{ workId, runResult, attempt }],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
function formatError(e: unknown) {
|
|
44
|
+
if (e instanceof Error) {
|
|
45
|
+
return e.message;
|
|
46
|
+
}
|
|
47
|
+
return String(e);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const runActionWrapper = internalAction({
|
|
51
|
+
args: {
|
|
52
|
+
workId: v.id("work"),
|
|
53
|
+
fnHandle: v.string(),
|
|
54
|
+
fnArgs: v.any(),
|
|
55
|
+
logLevel,
|
|
56
|
+
attempt: v.number(),
|
|
57
|
+
},
|
|
58
|
+
handler: async (ctx, { workId, attempt, ...args }) => {
|
|
59
|
+
const console = createLogger(args.logLevel);
|
|
60
|
+
const fnHandle = args.fnHandle as FunctionHandle<"action">;
|
|
61
|
+
try {
|
|
62
|
+
const returnValue = await ctx.runAction(fnHandle, args.fnArgs);
|
|
63
|
+
// NOTE: we could run `ctx.runMutation`, but we want to guarantee execution,
|
|
64
|
+
// and `ctx.scheduler.runAfter` won't OCC.
|
|
65
|
+
const runResult: RunResult = { kind: "success", returnValue };
|
|
66
|
+
await ctx.scheduler.runAfter(0, internal.complete.complete, {
|
|
67
|
+
jobs: [{ workId, runResult, attempt }],
|
|
68
|
+
});
|
|
69
|
+
} catch (e: unknown) {
|
|
70
|
+
console.error(e);
|
|
71
|
+
// We let the main loop handle the retries.
|
|
72
|
+
const runResult: RunResult = { kind: "failed", error: formatError(e) };
|
|
73
|
+
await ctx.scheduler.runAfter(0, internal.complete.complete, {
|
|
74
|
+
jobs: [{ workId, runResult, attempt }],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
81
|
+
const console = "THIS IS A REMINDER TO USE createLogger";
|