@convex-dev/workpool 0.2.0 → 0.2.2
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 +86 -3
- package/dist/commonjs/client/index.d.ts +32 -6
- package/dist/commonjs/client/index.d.ts.map +1 -1
- package/dist/commonjs/client/index.js +28 -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 +32 -6
- package/dist/esm/client/index.d.ts.map +1 -1
- package/dist/esm/client/index.js +28 -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 +66 -36
- 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,8 +1,19 @@
|
|
|
1
1
|
import { Infer, v } from "convex/values";
|
|
2
|
-
import { internalMutation } from "./_generated/server.js";
|
|
2
|
+
import { internalMutation, MutationCtx } from "./_generated/server.js";
|
|
3
3
|
import { completeArgs, completeHandler } from "./complete.js";
|
|
4
4
|
import { createLogger } from "./logging.js";
|
|
5
5
|
|
|
6
|
+
const recoveryArgs = v.object({
|
|
7
|
+
jobs: v.array(
|
|
8
|
+
v.object({
|
|
9
|
+
scheduledId: v.id("_scheduled_functions"),
|
|
10
|
+
workId: v.id("work"),
|
|
11
|
+
attempt: v.number(),
|
|
12
|
+
started: v.number(),
|
|
13
|
+
})
|
|
14
|
+
),
|
|
15
|
+
});
|
|
16
|
+
|
|
6
17
|
/**
|
|
7
18
|
* This can run when things fail because of server failures / restarts, or when
|
|
8
19
|
* the user cancels scheduled jobs (from the dashboard).
|
|
@@ -19,78 +30,75 @@ import { createLogger } from "./logging.js";
|
|
|
19
30
|
* -> check work.attempts
|
|
20
31
|
*/
|
|
21
32
|
export const recover = internalMutation({
|
|
22
|
-
args:
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
33
|
+
args: recoveryArgs,
|
|
34
|
+
handler: recoveryHandler,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// only exported for testing
|
|
38
|
+
export async function recoveryHandler(
|
|
39
|
+
ctx: MutationCtx,
|
|
40
|
+
{ jobs }: Infer<typeof recoveryArgs>
|
|
41
|
+
) {
|
|
42
|
+
const globals = await ctx.db.query("globals").unique();
|
|
43
|
+
const console = createLogger(globals?.logLevel);
|
|
44
|
+
const toComplete: Infer<typeof completeArgs.fields.jobs> = [];
|
|
45
|
+
for (let i = 0; i < jobs.length; i++) {
|
|
46
|
+
const job = jobs[i];
|
|
47
|
+
const preamble = `[recovery] Scheduled job ${job.scheduledId} for work ${job.workId}`;
|
|
48
|
+
const pendingCompletion = await ctx.db
|
|
49
|
+
.query("pendingCompletion")
|
|
50
|
+
.withIndex("workId", (q) => q.eq("workId", job.workId))
|
|
51
|
+
.first();
|
|
52
|
+
if (pendingCompletion) {
|
|
53
|
+
// Completion already pending, no need to do anything.
|
|
54
|
+
console.debug(`${preamble} already in pendingCompletion, skipping`);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const work = await ctx.db.get(job.workId);
|
|
58
|
+
if (work === null) {
|
|
59
|
+
// Completion already executed w/o retries, no need to do anything.
|
|
60
|
+
console.warn(`${preamble} work not found, skipping`);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (work.attempts !== job.attempt) {
|
|
64
|
+
// Retry already started, no need to do anything.
|
|
65
|
+
console.warn(`${preamble} attempts mismatch, skipping`);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const scheduled = await ctx.db.system.get(job.scheduledId);
|
|
69
|
+
if (scheduled === null) {
|
|
70
|
+
console.warn(`${preamble} not found in _scheduled_functions`);
|
|
71
|
+
toComplete.push({
|
|
72
|
+
workId: job.workId,
|
|
73
|
+
runResult: { kind: "failed", error: `Scheduled job not found` },
|
|
74
|
+
attempt: job.attempt,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
// This will find everything that timed out, failed ungracefully, was
|
|
79
|
+
// canceled, or succeeded without a return value.
|
|
80
|
+
switch (scheduled.state.kind) {
|
|
81
|
+
case "failed": {
|
|
82
|
+
console.debug(`${preamble} failed and detected in recovery`);
|
|
62
83
|
toComplete.push({
|
|
63
84
|
workId: job.workId,
|
|
64
|
-
runResult:
|
|
85
|
+
runResult: scheduled.state,
|
|
65
86
|
attempt: job.attempt,
|
|
66
87
|
});
|
|
67
|
-
|
|
88
|
+
break;
|
|
68
89
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
attempt: job.attempt,
|
|
78
|
-
});
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
case "canceled": {
|
|
82
|
-
console.debug(`${preamble} was canceled and detected in recovery`);
|
|
83
|
-
toComplete.push({
|
|
84
|
-
workId: job.workId,
|
|
85
|
-
runResult: { kind: "failed", error: "Canceled via scheduler" },
|
|
86
|
-
attempt: job.attempt,
|
|
87
|
-
});
|
|
88
|
-
break;
|
|
89
|
-
}
|
|
90
|
+
case "canceled": {
|
|
91
|
+
console.debug(`${preamble} was canceled and detected in recovery`);
|
|
92
|
+
toComplete.push({
|
|
93
|
+
workId: job.workId,
|
|
94
|
+
runResult: { kind: "failed", error: "Canceled via scheduler" },
|
|
95
|
+
attempt: job.attempt,
|
|
96
|
+
});
|
|
97
|
+
break;
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
100
|
+
}
|
|
101
|
+
if (toComplete.length > 0) {
|
|
102
|
+
await completeHandler(ctx, { jobs: toComplete });
|
|
103
|
+
}
|
|
104
|
+
}
|
package/src/component/shared.ts
CHANGED
|
@@ -15,11 +15,11 @@ export function toSegment(ms: number): bigint {
|
|
|
15
15
|
return BigInt(Math.floor(ms / SEGMENT_MS));
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export function
|
|
18
|
+
export function getCurrentSegment(): bigint {
|
|
19
19
|
return toSegment(Date.now());
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function
|
|
22
|
+
export function getNextSegment(): bigint {
|
|
23
23
|
return toSegment(Date.now()) + 1n;
|
|
24
24
|
}
|
|
25
25
|
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import { convexTest } from "convex-test";
|
|
2
|
+
import {
|
|
3
|
+
describe,
|
|
4
|
+
expect,
|
|
5
|
+
it,
|
|
6
|
+
beforeEach,
|
|
7
|
+
afterEach,
|
|
8
|
+
vi,
|
|
9
|
+
assert,
|
|
10
|
+
} from "vitest";
|
|
11
|
+
import schema from "./schema";
|
|
12
|
+
import { internal } from "./_generated/api";
|
|
13
|
+
import { Logger } from "./logging";
|
|
14
|
+
import { getCurrentSegment } from "./shared";
|
|
15
|
+
import { paginator } from "convex-helpers/server/pagination";
|
|
16
|
+
|
|
17
|
+
const modules = import.meta.glob("./**/*.ts");
|
|
18
|
+
|
|
19
|
+
// Create a proper Logger mock
|
|
20
|
+
function createLoggerMock(): Logger {
|
|
21
|
+
return {
|
|
22
|
+
event: vi.fn(),
|
|
23
|
+
debug: vi.fn(),
|
|
24
|
+
info: vi.fn(),
|
|
25
|
+
warn: vi.fn(),
|
|
26
|
+
error: vi.fn(),
|
|
27
|
+
time: vi.fn(),
|
|
28
|
+
timeEnd: vi.fn(),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("stats", () => {
|
|
33
|
+
async function setupTest() {
|
|
34
|
+
const t = convexTest(schema, modules);
|
|
35
|
+
return t;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let t: Awaited<ReturnType<typeof setupTest>>;
|
|
39
|
+
|
|
40
|
+
beforeEach(async () => {
|
|
41
|
+
vi.useFakeTimers();
|
|
42
|
+
t = await setupTest();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
vi.useRealTimers();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe("generateReport", () => {
|
|
50
|
+
it("should not generate a report when log level is above REPORT", async () => {
|
|
51
|
+
// Setup internal state
|
|
52
|
+
const stateId = await t.run(async (ctx) => {
|
|
53
|
+
return await ctx.db.insert("internalState", {
|
|
54
|
+
generation: 1n,
|
|
55
|
+
segmentCursors: {
|
|
56
|
+
incoming: 0n,
|
|
57
|
+
completion: 0n,
|
|
58
|
+
cancelation: 0n,
|
|
59
|
+
},
|
|
60
|
+
lastRecovery: 0n,
|
|
61
|
+
report: {
|
|
62
|
+
completed: 0,
|
|
63
|
+
succeeded: 0,
|
|
64
|
+
failed: 0,
|
|
65
|
+
retries: 0,
|
|
66
|
+
canceled: 0,
|
|
67
|
+
lastReportTs: 0,
|
|
68
|
+
},
|
|
69
|
+
running: [],
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Mock the console.event function to track if it's called
|
|
74
|
+
const consoleMock = createLoggerMock();
|
|
75
|
+
|
|
76
|
+
// Get the state document
|
|
77
|
+
const state = await t.run(async (ctx) => {
|
|
78
|
+
return await ctx.db.get(stateId);
|
|
79
|
+
});
|
|
80
|
+
assert(state);
|
|
81
|
+
|
|
82
|
+
// Call generateReport with a log level that won't trigger reporting
|
|
83
|
+
await t.run(async (ctx) => {
|
|
84
|
+
const { generateReport } = await import("./stats");
|
|
85
|
+
await generateReport(ctx, consoleMock, state, {
|
|
86
|
+
maxParallelism: 10,
|
|
87
|
+
logLevel: "WARN", // Above REPORT level
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Verify that console.event was not called
|
|
92
|
+
expect(consoleMock.event).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("should generate a report when backlog is small enough", async () => {
|
|
96
|
+
// Setup internal state
|
|
97
|
+
const stateId = await t.run(async (ctx) => {
|
|
98
|
+
return await ctx.db.insert("internalState", {
|
|
99
|
+
generation: 1n,
|
|
100
|
+
segmentCursors: {
|
|
101
|
+
incoming: 0n,
|
|
102
|
+
completion: 0n,
|
|
103
|
+
cancelation: 0n,
|
|
104
|
+
},
|
|
105
|
+
lastRecovery: 0n,
|
|
106
|
+
report: {
|
|
107
|
+
completed: 10,
|
|
108
|
+
succeeded: 6,
|
|
109
|
+
failed: 2,
|
|
110
|
+
retries: 2,
|
|
111
|
+
canceled: 0,
|
|
112
|
+
lastReportTs: 0,
|
|
113
|
+
},
|
|
114
|
+
running: [],
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Create a few pending start items
|
|
119
|
+
await t.run(async (ctx) => {
|
|
120
|
+
// Create a work item
|
|
121
|
+
const workId = await ctx.db.insert("work", {
|
|
122
|
+
fnType: "mutation",
|
|
123
|
+
fnHandle: "testHandle",
|
|
124
|
+
fnName: "testFunction",
|
|
125
|
+
fnArgs: { test: true },
|
|
126
|
+
attempts: 0,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// Create a pendingStart for the work
|
|
130
|
+
await ctx.db.insert("pendingStart", {
|
|
131
|
+
workId,
|
|
132
|
+
segment: 5n, // Some segment between 0 and currentSegment
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Mock the console.event function to track if it's called
|
|
137
|
+
const consoleMock = createLoggerMock();
|
|
138
|
+
|
|
139
|
+
// Get the state document
|
|
140
|
+
const state = await t.run(async (ctx) => {
|
|
141
|
+
return await ctx.db.get(stateId);
|
|
142
|
+
});
|
|
143
|
+
assert(state);
|
|
144
|
+
|
|
145
|
+
// Call generateReport with REPORT log level
|
|
146
|
+
await t.run(async (ctx) => {
|
|
147
|
+
const { generateReport } = await import("./stats");
|
|
148
|
+
await generateReport(ctx, consoleMock, state, {
|
|
149
|
+
maxParallelism: 10,
|
|
150
|
+
logLevel: "REPORT", // This should trigger reporting
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Verify that console.event was called with the correct data
|
|
155
|
+
expect(consoleMock.event).toHaveBeenCalledWith("report", {
|
|
156
|
+
backlog: 1, // We created one pendingStart
|
|
157
|
+
running: 0,
|
|
158
|
+
completed: 10,
|
|
159
|
+
succeeded: 6,
|
|
160
|
+
failed: 2,
|
|
161
|
+
retries: 2,
|
|
162
|
+
canceled: 0,
|
|
163
|
+
failureRate: 0.4, // (failed + retries) / completed = (2 + 2) / 10 = 0.4
|
|
164
|
+
permanentFailureRate: 0.25, // failed / (completed - retries) = 2 / (10 - 2) = 2/8
|
|
165
|
+
lastReportTs: expect.any(Number),
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("should schedule calculateBacklogAndReport when backlog is large", async () => {
|
|
170
|
+
// Setup internal state
|
|
171
|
+
const stateId = await t.run(async (ctx) => {
|
|
172
|
+
return await ctx.db.insert("internalState", {
|
|
173
|
+
generation: 1n,
|
|
174
|
+
segmentCursors: {
|
|
175
|
+
incoming: 0n,
|
|
176
|
+
completion: 0n,
|
|
177
|
+
cancelation: 0n,
|
|
178
|
+
},
|
|
179
|
+
lastRecovery: 0n,
|
|
180
|
+
report: {
|
|
181
|
+
completed: 10,
|
|
182
|
+
succeeded: 8,
|
|
183
|
+
failed: 1,
|
|
184
|
+
retries: 1,
|
|
185
|
+
canceled: 0,
|
|
186
|
+
lastReportTs: 0,
|
|
187
|
+
},
|
|
188
|
+
running: [],
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// Create more pending start items than maxParallelism
|
|
193
|
+
const maxParallelism = 5;
|
|
194
|
+
|
|
195
|
+
// Create maxParallelism + 1 work items to trigger pagination
|
|
196
|
+
for (let i = 0; i < maxParallelism + 1; i++) {
|
|
197
|
+
await t.run(async (ctx) => {
|
|
198
|
+
// Create a work item
|
|
199
|
+
const workId = await ctx.db.insert("work", {
|
|
200
|
+
fnType: "mutation",
|
|
201
|
+
fnHandle: "testHandle",
|
|
202
|
+
fnName: `testFunction${i}`,
|
|
203
|
+
fnArgs: { test: i },
|
|
204
|
+
attempts: 0,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Create a pendingStart for the work
|
|
208
|
+
await ctx.db.insert("pendingStart", {
|
|
209
|
+
workId,
|
|
210
|
+
segment: 5n, // Some segment between 0 and currentSegment
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Mock the console.event function
|
|
216
|
+
const consoleMock = createLoggerMock();
|
|
217
|
+
|
|
218
|
+
// Get the state document
|
|
219
|
+
const state = await t.run(async (ctx) => {
|
|
220
|
+
return await ctx.db.get(stateId);
|
|
221
|
+
});
|
|
222
|
+
assert(state);
|
|
223
|
+
|
|
224
|
+
// Call generateReport with REPORT log level
|
|
225
|
+
await t.run(async (ctx) => {
|
|
226
|
+
const { generateReport } = await import("./stats");
|
|
227
|
+
await generateReport(ctx, consoleMock, state, {
|
|
228
|
+
maxParallelism,
|
|
229
|
+
logLevel: "REPORT", // This should trigger reporting
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
// Verify that calculateBacklogAndReport was scheduled
|
|
234
|
+
await t.run(async (ctx) => {
|
|
235
|
+
const scheduledFunctions = await ctx.db.system
|
|
236
|
+
.query("_scheduled_functions")
|
|
237
|
+
.collect();
|
|
238
|
+
|
|
239
|
+
expect(scheduledFunctions.length).toBeGreaterThan(0);
|
|
240
|
+
|
|
241
|
+
// Check that one of the scheduled functions is calculateBacklogAndReport
|
|
242
|
+
const calculateBacklogScheduled = scheduledFunctions.find(
|
|
243
|
+
(sf) => sf.name === "stats:calculateBacklogAndReport"
|
|
244
|
+
);
|
|
245
|
+
expect(calculateBacklogScheduled).toBeDefined();
|
|
246
|
+
assert(calculateBacklogScheduled);
|
|
247
|
+
|
|
248
|
+
// Verify console.event was not called yet (will be called by calculateBacklogAndReport)
|
|
249
|
+
expect(consoleMock.event).not.toHaveBeenCalled();
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("should calculate backlog and report correctly", async () => {
|
|
254
|
+
// Setup internal state
|
|
255
|
+
const stateId = await t.run(async (ctx) => {
|
|
256
|
+
return await ctx.db.insert("internalState", {
|
|
257
|
+
generation: 1n,
|
|
258
|
+
segmentCursors: {
|
|
259
|
+
incoming: 0n,
|
|
260
|
+
completion: 0n,
|
|
261
|
+
cancelation: 0n,
|
|
262
|
+
},
|
|
263
|
+
lastRecovery: 0n,
|
|
264
|
+
report: {
|
|
265
|
+
completed: 10,
|
|
266
|
+
succeeded: 8,
|
|
267
|
+
failed: 1,
|
|
268
|
+
retries: 1,
|
|
269
|
+
canceled: 0,
|
|
270
|
+
lastReportTs: 0,
|
|
271
|
+
},
|
|
272
|
+
running: [],
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Create some pending start items
|
|
277
|
+
const currentSegment = getCurrentSegment();
|
|
278
|
+
|
|
279
|
+
// Create 3 work items
|
|
280
|
+
for (let i = 0; i < 3; i++) {
|
|
281
|
+
await t.run(async (ctx) => {
|
|
282
|
+
// Create a work item
|
|
283
|
+
const workId = await ctx.db.insert("work", {
|
|
284
|
+
fnType: "mutation",
|
|
285
|
+
fnHandle: "testHandle",
|
|
286
|
+
fnName: `testFunction${i}`,
|
|
287
|
+
fnArgs: { test: i },
|
|
288
|
+
attempts: 0,
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// Create a pendingStart for the work
|
|
292
|
+
await ctx.db.insert("pendingStart", {
|
|
293
|
+
workId,
|
|
294
|
+
segment: 5n, // Some segment between 0 and currentSegment
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Get the state document
|
|
300
|
+
const state = await t.run(async (ctx) => {
|
|
301
|
+
return await ctx.db.get(stateId);
|
|
302
|
+
});
|
|
303
|
+
assert(state);
|
|
304
|
+
|
|
305
|
+
const cursor = await t.run(async (ctx) => {
|
|
306
|
+
return await paginator(ctx.db, schema)
|
|
307
|
+
.query("pendingStart")
|
|
308
|
+
.withIndex("segment", (q) =>
|
|
309
|
+
q.gte("segment", 0n).lt("segment", currentSegment)
|
|
310
|
+
)
|
|
311
|
+
.paginate({
|
|
312
|
+
numItems: 1,
|
|
313
|
+
cursor: null,
|
|
314
|
+
});
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// Call calculateBacklogAndReport directly
|
|
318
|
+
await t.mutation(internal.stats.calculateBacklogAndReport, {
|
|
319
|
+
startSegment: 0n,
|
|
320
|
+
endSegment: currentSegment,
|
|
321
|
+
cursor: cursor.continueCursor,
|
|
322
|
+
report: state.report,
|
|
323
|
+
running: state.running.length,
|
|
324
|
+
logLevel: "REPORT",
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
// Verify that console.event was called with the correct data
|
|
328
|
+
// Note: We can't directly check the mock since it's created inside the mutation
|
|
329
|
+
// Instead, we can check if the function completed successfully
|
|
330
|
+
|
|
331
|
+
// We can verify the function was executed by checking if any scheduled functions were created
|
|
332
|
+
await t.run(async (ctx) => {
|
|
333
|
+
const scheduledFunctions = await ctx.db.system
|
|
334
|
+
.query("_scheduled_functions")
|
|
335
|
+
.collect();
|
|
336
|
+
|
|
337
|
+
// Since our backlog is small, no additional scheduled functions should be created
|
|
338
|
+
const calculateBacklogScheduled = scheduledFunctions.find(
|
|
339
|
+
(sf) => sf.name === "stats:calculateBacklogAndReport"
|
|
340
|
+
);
|
|
341
|
+
expect(calculateBacklogScheduled).toBeUndefined();
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
});
|