@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/lib.ts
CHANGED
|
@@ -1,25 +1,21 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
|
+
import { mutation, MutationCtx, query } from "./_generated/server.js";
|
|
2
3
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
} from "./
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { api
|
|
14
|
-
import { createLogger
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import { completionStatus } from "./schema";
|
|
19
|
-
|
|
20
|
-
const crons = new Crons(components.crons);
|
|
21
|
-
|
|
22
|
-
const ACTION_TIMEOUT_MS = 15 * 60 * 1000;
|
|
4
|
+
nextSegment,
|
|
5
|
+
onComplete,
|
|
6
|
+
retryBehavior,
|
|
7
|
+
config,
|
|
8
|
+
status as statusValidator,
|
|
9
|
+
toSegment,
|
|
10
|
+
boundScheduledTime,
|
|
11
|
+
} from "./shared.js";
|
|
12
|
+
import { LogLevel, logLevel } from "./logging.js";
|
|
13
|
+
import { kickMainLoop } from "./kick.js";
|
|
14
|
+
import { api } from "./_generated/api.js";
|
|
15
|
+
import { createLogger } from "./logging.js";
|
|
16
|
+
import { Id } from "./_generated/dataModel.js";
|
|
17
|
+
|
|
18
|
+
const MAX_POSSIBLE_PARALLELISM = 100;
|
|
23
19
|
|
|
24
20
|
export const enqueue = mutation({
|
|
25
21
|
args: {
|
|
@@ -27,31 +23,32 @@ export const enqueue = mutation({
|
|
|
27
23
|
fnName: v.string(),
|
|
28
24
|
fnArgs: v.any(),
|
|
29
25
|
fnType: v.union(v.literal("action"), v.literal("mutation")),
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
runAt: v.number(),
|
|
27
|
+
// TODO: annotation?
|
|
28
|
+
onComplete: v.optional(onComplete),
|
|
29
|
+
retryBehavior: v.optional(retryBehavior),
|
|
30
|
+
config,
|
|
35
31
|
},
|
|
36
32
|
returns: v.id("work"),
|
|
37
|
-
handler: async (ctx, {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
);
|
|
33
|
+
handler: async (ctx, { config, runAt, ...workArgs }) => {
|
|
34
|
+
const console = createLogger(config.logLevel);
|
|
35
|
+
if (config.maxParallelism > MAX_POSSIBLE_PARALLELISM) {
|
|
36
|
+
throw new Error(`maxParallelism must be <= ${MAX_POSSIBLE_PARALLELISM}`);
|
|
37
|
+
}
|
|
38
|
+
if (config.maxParallelism < 1) {
|
|
39
|
+
throw new Error("maxParallelism must be >= 1");
|
|
40
|
+
}
|
|
41
|
+
runAt = boundScheduledTime(runAt, console);
|
|
47
42
|
const workId = await ctx.db.insert("work", {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
fnArgs,
|
|
51
|
-
fnType,
|
|
43
|
+
...workArgs,
|
|
44
|
+
attempts: 0,
|
|
52
45
|
});
|
|
53
|
-
await ctx.db.insert("pendingStart", {
|
|
54
|
-
|
|
46
|
+
await ctx.db.insert("pendingStart", {
|
|
47
|
+
workId,
|
|
48
|
+
segment: toSegment(runAt),
|
|
49
|
+
});
|
|
50
|
+
await kickMainLoop(ctx, "enqueue", config);
|
|
51
|
+
// TODO: stats event
|
|
55
52
|
return workId;
|
|
56
53
|
},
|
|
57
54
|
});
|
|
@@ -59,658 +56,99 @@ export const enqueue = mutation({
|
|
|
59
56
|
export const cancel = mutation({
|
|
60
57
|
args: {
|
|
61
58
|
id: v.id("work"),
|
|
59
|
+
logLevel,
|
|
62
60
|
},
|
|
63
|
-
handler: async (ctx, { id }) => {
|
|
64
|
-
await ctx
|
|
61
|
+
handler: async (ctx, { id, logLevel }) => {
|
|
62
|
+
const canceled = await cancelWorkItem(ctx, id, nextSegment(), logLevel);
|
|
63
|
+
if (canceled) {
|
|
64
|
+
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
65
|
+
}
|
|
66
|
+
// TODO: stats event
|
|
65
67
|
},
|
|
66
68
|
});
|
|
67
69
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
// The scheduled one is in the "mainLoop" table.
|
|
83
|
-
export const mainLoop = internalMutation({
|
|
84
|
-
args: {
|
|
85
|
-
generation: v.number(),
|
|
86
|
-
},
|
|
87
|
-
handler: async (ctx, args) => {
|
|
88
|
-
const console_ = await console(ctx);
|
|
89
|
-
|
|
90
|
-
const options = (await ctx.db.query("pool").unique())!;
|
|
91
|
-
if (!options) {
|
|
92
|
-
throw new Error("no pool in mainLoop");
|
|
93
|
-
}
|
|
94
|
-
const { maxParallelism } = options;
|
|
95
|
-
let didSomething = false;
|
|
96
|
-
|
|
97
|
-
let inProgressCountChange = 0;
|
|
98
|
-
|
|
99
|
-
// Move from pendingCompletion to completedWork, deleting from inProgressWork.
|
|
100
|
-
// Generation is used to avoid OCCs with work completing.
|
|
101
|
-
console_.time("[mainLoop] pendingCompletion");
|
|
102
|
-
const generation = await ctx.db.query("completionGeneration").unique();
|
|
103
|
-
const generationNumber = generation?.generation ?? 0;
|
|
104
|
-
if (generationNumber !== args.generation) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
`generation mismatch: ${generationNumber} !== ${args.generation}`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
// Collect all pending completions for the previous generation.
|
|
110
|
-
// This won't be too many because the jobs all correspond to being scheduled
|
|
111
|
-
// by a single mainLoop (the previous one), so they're limited by MAX_PARALLELISM.
|
|
112
|
-
const completed = await ctx.db
|
|
113
|
-
.query("pendingCompletion")
|
|
114
|
-
.withIndex("generation", (q) => q.eq("generation", generationNumber - 1))
|
|
115
|
-
.collect();
|
|
116
|
-
console_.debug(`[mainLoop] completing ${completed.length}`);
|
|
117
|
-
await Promise.all(
|
|
118
|
-
completed.map(async (pendingCompletion) => {
|
|
119
|
-
const inProgressWork = await ctx.db
|
|
120
|
-
.query("inProgressWork")
|
|
121
|
-
.withIndex("workId", (q) => q.eq("workId", pendingCompletion.workId))
|
|
122
|
-
.unique();
|
|
123
|
-
if (inProgressWork) {
|
|
124
|
-
await ctx.db.delete(inProgressWork._id);
|
|
125
|
-
inProgressCountChange--;
|
|
126
|
-
await ctx.db.insert("completedWork", {
|
|
127
|
-
completionStatus: pendingCompletion.completionStatus,
|
|
128
|
-
workId: pendingCompletion.workId,
|
|
129
|
-
});
|
|
130
|
-
const work = (await ctx.db.get(pendingCompletion.workId))!;
|
|
131
|
-
console_.info(
|
|
132
|
-
recordCompleted(work, pendingCompletion.completionStatus)
|
|
133
|
-
);
|
|
134
|
-
await ctx.db.delete(work._id);
|
|
135
|
-
}
|
|
136
|
-
await ctx.db.delete(pendingCompletion._id);
|
|
137
|
-
didSomething = true;
|
|
138
|
-
})
|
|
139
|
-
);
|
|
140
|
-
console_.timeEnd("[mainLoop] pendingCompletion");
|
|
141
|
-
|
|
142
|
-
console_.time("[mainLoop] inProgress count");
|
|
143
|
-
// This is the only function reading and writing inProgressWork,
|
|
144
|
-
// and it's bounded by MAX_POSSIBLE_PARALLELISM, so we can
|
|
145
|
-
// read it all into memory. BUT we don't have to -- we can just read
|
|
146
|
-
// the count from the inProgressCount table.
|
|
147
|
-
const inProgressCount = await ctx.db.query("inProgressCount").unique();
|
|
148
|
-
const inProgressBefore =
|
|
149
|
-
(inProgressCount?.count ?? 0) + inProgressCountChange;
|
|
150
|
-
console_.debug(`[mainLoop] ${inProgressBefore} in progress`);
|
|
151
|
-
console_.timeEnd("[mainLoop] inProgress count");
|
|
152
|
-
|
|
153
|
-
// Move from pendingStart to inProgressWork.
|
|
154
|
-
console_.time("[mainLoop] pendingStart");
|
|
155
|
-
|
|
156
|
-
// Start reading from the latest cursor _creationTime, which allows us to
|
|
157
|
-
// skip over tombstones of pendingStart documents which haven't been cleaned up yet.
|
|
158
|
-
// WARNING: this might skip over pendingStart documents if their _creationTime
|
|
159
|
-
// was assigned out of order. We handle that below.
|
|
160
|
-
const pendingStartCursorDoc = await ctx.db
|
|
161
|
-
.query("pendingStartCursor")
|
|
162
|
-
.unique();
|
|
163
|
-
const pendingStartCursor = pendingStartCursorDoc?.cursor ?? 0;
|
|
164
|
-
|
|
165
|
-
// Schedule as many as needed to reach maxParallelism.
|
|
166
|
-
const toSchedule = maxParallelism - inProgressBefore;
|
|
167
|
-
|
|
168
|
-
const pending = await ctx.db
|
|
169
|
-
.query("pendingStart")
|
|
170
|
-
.withIndex("by_creation_time", (q) =>
|
|
171
|
-
q.gt("_creationTime", pendingStartCursor)
|
|
70
|
+
const PAGE_SIZE = 64;
|
|
71
|
+
export const cancelAll = mutation({
|
|
72
|
+
args: { logLevel, before: v.optional(v.number()) },
|
|
73
|
+
handler: async (ctx, { logLevel, before }) => {
|
|
74
|
+
const beforeTime = before ?? Date.now();
|
|
75
|
+
const segment = nextSegment();
|
|
76
|
+
const pageOfWork = await ctx.db
|
|
77
|
+
.query("work")
|
|
78
|
+
.withIndex("by_creation_time", (q) => q.lte("_creationTime", beforeTime))
|
|
79
|
+
.order("desc")
|
|
80
|
+
.take(PAGE_SIZE);
|
|
81
|
+
const canceled = await Promise.all(
|
|
82
|
+
pageOfWork.map(async ({ _id }) =>
|
|
83
|
+
cancelWorkItem(ctx, _id, segment, logLevel)
|
|
172
84
|
)
|
|
173
|
-
.take(toSchedule);
|
|
174
|
-
console_.debug(`[mainLoop] scheduling ${pending.length} pending work`);
|
|
175
|
-
await Promise.all(
|
|
176
|
-
pending.map(async (pendingWork) => {
|
|
177
|
-
const { scheduledId, timeoutMs } = await beginWork(ctx, pendingWork);
|
|
178
|
-
await ctx.db.insert("inProgressWork", {
|
|
179
|
-
running: scheduledId,
|
|
180
|
-
timeoutMs,
|
|
181
|
-
workId: pendingWork.workId,
|
|
182
|
-
});
|
|
183
|
-
inProgressCountChange++;
|
|
184
|
-
await ctx.db.delete(pendingWork._id);
|
|
185
|
-
didSomething = true;
|
|
186
|
-
})
|
|
187
|
-
);
|
|
188
|
-
const newPendingStartCursor =
|
|
189
|
-
pending.length > 0
|
|
190
|
-
? pending[pending.length - 1]._creationTime
|
|
191
|
-
: pendingStartCursor;
|
|
192
|
-
if (!pendingStartCursorDoc) {
|
|
193
|
-
await ctx.db.insert("pendingStartCursor", {
|
|
194
|
-
cursor: newPendingStartCursor,
|
|
195
|
-
});
|
|
196
|
-
} else {
|
|
197
|
-
await ctx.db.patch(pendingStartCursorDoc._id, {
|
|
198
|
-
cursor: newPendingStartCursor,
|
|
199
|
-
});
|
|
200
|
-
}
|
|
201
|
-
console_.timeEnd("[mainLoop] pendingStart");
|
|
202
|
-
|
|
203
|
-
console_.time("[mainLoop] pendingCancelation");
|
|
204
|
-
const canceled = await ctx.db.query("pendingCancelation").take(BATCH_SIZE);
|
|
205
|
-
console_.debug(`[mainLoop] canceling ${canceled.length}`);
|
|
206
|
-
await Promise.all(
|
|
207
|
-
canceled.map(async (pendingCancelation) => {
|
|
208
|
-
const inProgressWork = await ctx.db
|
|
209
|
-
.query("inProgressWork")
|
|
210
|
-
.withIndex("workId", (q) => q.eq("workId", pendingCancelation.workId))
|
|
211
|
-
.unique();
|
|
212
|
-
if (inProgressWork) {
|
|
213
|
-
await ctx.scheduler.cancel(inProgressWork.running);
|
|
214
|
-
await ctx.db.delete(inProgressWork._id);
|
|
215
|
-
inProgressCountChange--;
|
|
216
|
-
await ctx.db.insert("completedWork", {
|
|
217
|
-
workId: pendingCancelation.workId,
|
|
218
|
-
completionStatus: "canceled",
|
|
219
|
-
});
|
|
220
|
-
const work = (await ctx.db.get(pendingCancelation.workId))!;
|
|
221
|
-
console_.info(recordCompleted(work, "canceled"));
|
|
222
|
-
await ctx.db.delete(work._id);
|
|
223
|
-
}
|
|
224
|
-
await ctx.db.delete(pendingCancelation._id);
|
|
225
|
-
didSomething = true;
|
|
226
|
-
})
|
|
227
85
|
);
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// In case there are more pending completions at higher generation numbers,
|
|
231
|
-
// there's more to do.
|
|
232
|
-
if (!didSomething) {
|
|
233
|
-
const nextPendingCompletion = await ctx.db
|
|
234
|
-
.query("pendingCompletion")
|
|
235
|
-
.withIndex("generation", (q) => q.eq("generation", generationNumber))
|
|
236
|
-
.first();
|
|
237
|
-
didSomething = nextPendingCompletion !== null;
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// In case there are more "pendingStart" items we missed due to out-of-order
|
|
241
|
-
// _creationTime, we need to check for them. Do that here, when we're
|
|
242
|
-
// otherwise idle so it doesn't matter if this function takes a while walking
|
|
243
|
-
// tombstones.
|
|
244
|
-
if (!didSomething && pendingStartCursorDoc) {
|
|
245
|
-
const pendingStartDoc = await ctx.db
|
|
246
|
-
.query("pendingStart")
|
|
247
|
-
.order("desc")
|
|
248
|
-
.first();
|
|
249
|
-
if (pendingStartDoc) {
|
|
250
|
-
console_.warn(`[mainLoop] missed pendingStart docs; discarding cursor`);
|
|
251
|
-
await ctx.db.delete(pendingStartCursorDoc._id);
|
|
252
|
-
didSomething = true;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (!didSomething) {
|
|
257
|
-
console_.time("[mainLoop] inProgressWork check for unclean exits");
|
|
258
|
-
// If all completions are handled, check everything in inProgressWork.
|
|
259
|
-
// This will find everything that timed out, failed ungracefully, was
|
|
260
|
-
// cancelled, or succeeded without a return value.
|
|
261
|
-
const inProgress = await ctx.db.query("inProgressWork").collect();
|
|
262
|
-
await Promise.all(
|
|
263
|
-
inProgress.map(async (inProgressWork) => {
|
|
264
|
-
const result = await checkInProgressWork(ctx, inProgressWork);
|
|
265
|
-
if (result !== null) {
|
|
266
|
-
console_.warn(
|
|
267
|
-
"[mainLoop] inProgressWork finished uncleanly",
|
|
268
|
-
inProgressWork.workId,
|
|
269
|
-
result
|
|
270
|
-
);
|
|
271
|
-
inProgressCountChange--;
|
|
272
|
-
await ctx.db.delete(inProgressWork._id);
|
|
273
|
-
await ctx.db.insert("completedWork", {
|
|
274
|
-
workId: inProgressWork.workId,
|
|
275
|
-
completionStatus: result.completionStatus,
|
|
276
|
-
});
|
|
277
|
-
const work = (await ctx.db.get(inProgressWork.workId))!;
|
|
278
|
-
console_.info(recordCompleted(work, result.completionStatus));
|
|
279
|
-
didSomething = true;
|
|
280
|
-
}
|
|
281
|
-
})
|
|
282
|
-
);
|
|
283
|
-
console_.timeEnd("[mainLoop] inProgressWork check for unclean exits");
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
if (inProgressCountChange !== 0) {
|
|
287
|
-
if (inProgressCount) {
|
|
288
|
-
await ctx.db.patch(inProgressCount._id, {
|
|
289
|
-
count: inProgressCount.count + inProgressCountChange,
|
|
290
|
-
});
|
|
291
|
-
} else {
|
|
292
|
-
await ctx.db.insert("inProgressCount", {
|
|
293
|
-
count: inProgressCountChange,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
console_.time("[mainLoop] kickMainLoop");
|
|
299
|
-
if (didSomething) {
|
|
300
|
-
// There might be more to do.
|
|
301
|
-
await loopFromMainLoop(ctx, 0);
|
|
302
|
-
} else {
|
|
303
|
-
// Decide when to wake up.
|
|
304
|
-
const allInProgressWork = await ctx.db.query("inProgressWork").collect();
|
|
305
|
-
const nextPending = await ctx.db.query("pendingStart").first();
|
|
306
|
-
const nextPendingTime = nextPending
|
|
307
|
-
? nextPending._creationTime
|
|
308
|
-
: Number.POSITIVE_INFINITY;
|
|
309
|
-
const nextInProgress = allInProgressWork.length
|
|
310
|
-
? Math.min(
|
|
311
|
-
...allInProgressWork
|
|
312
|
-
.filter((w) => w.timeoutMs !== null)
|
|
313
|
-
.map((w) => w._creationTime + w.timeoutMs!)
|
|
314
|
-
)
|
|
315
|
-
: Number.POSITIVE_INFINITY;
|
|
316
|
-
const nextTime = Math.min(nextPendingTime, nextInProgress);
|
|
317
|
-
await loopFromMainLoop(ctx, nextTime - Date.now());
|
|
318
|
-
}
|
|
319
|
-
console_.timeEnd("[mainLoop] kickMainLoop");
|
|
320
|
-
},
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
async function beginWork(
|
|
324
|
-
ctx: MutationCtx,
|
|
325
|
-
pendingStart: Doc<"pendingStart">
|
|
326
|
-
): Promise<{
|
|
327
|
-
scheduledId: Id<"_scheduled_functions">;
|
|
328
|
-
timeoutMs: number | null;
|
|
329
|
-
}> {
|
|
330
|
-
const console_ = await console(ctx);
|
|
331
|
-
const work = await ctx.db.get(pendingStart.workId);
|
|
332
|
-
if (!work) {
|
|
333
|
-
throw new Error("work not found");
|
|
334
|
-
}
|
|
335
|
-
console_.info(recordStarted(work));
|
|
336
|
-
if (work.fnType === "action") {
|
|
337
|
-
return {
|
|
338
|
-
scheduledId: await ctx.scheduler.runAfter(
|
|
339
|
-
0,
|
|
340
|
-
internal.lib.runActionWrapper,
|
|
341
|
-
{
|
|
342
|
-
workId: work._id,
|
|
343
|
-
fnHandle: work.fnHandle,
|
|
344
|
-
fnArgs: work.fnArgs,
|
|
345
|
-
}
|
|
346
|
-
),
|
|
347
|
-
timeoutMs: ACTION_TIMEOUT_MS,
|
|
348
|
-
};
|
|
349
|
-
} else if (work.fnType === "mutation") {
|
|
350
|
-
return {
|
|
351
|
-
scheduledId: await ctx.scheduler.runAfter(
|
|
352
|
-
0,
|
|
353
|
-
internal.lib.runMutationWrapper,
|
|
354
|
-
{
|
|
355
|
-
workId: work._id,
|
|
356
|
-
fnHandle: work.fnHandle,
|
|
357
|
-
fnArgs: work.fnArgs,
|
|
358
|
-
}
|
|
359
|
-
),
|
|
360
|
-
timeoutMs: null, // Mutations cannot timeout
|
|
361
|
-
};
|
|
362
|
-
} else {
|
|
363
|
-
throw new Error(`Unexpected fnType ${work.fnType}`);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
async function checkInProgressWork(
|
|
368
|
-
ctx: MutationCtx,
|
|
369
|
-
doc: Doc<"inProgressWork">
|
|
370
|
-
): Promise<{
|
|
371
|
-
completionStatus: "success" | "error" | "canceled" | "timeout";
|
|
372
|
-
} | null> {
|
|
373
|
-
const workStatus = await ctx.db.system.get(doc.running);
|
|
374
|
-
if (workStatus === null) {
|
|
375
|
-
return { completionStatus: "timeout" };
|
|
376
|
-
} else if (
|
|
377
|
-
workStatus.state.kind === "pending" ||
|
|
378
|
-
workStatus.state.kind === "inProgress"
|
|
379
|
-
) {
|
|
380
|
-
if (
|
|
381
|
-
doc.timeoutMs !== null &&
|
|
382
|
-
Date.now() - workStatus._creationTime > doc.timeoutMs
|
|
383
|
-
) {
|
|
384
|
-
await ctx.scheduler.cancel(doc.running);
|
|
385
|
-
return { completionStatus: "timeout" };
|
|
386
|
-
}
|
|
387
|
-
} else if (workStatus.state.kind === "success") {
|
|
388
|
-
// Usually this would be handled by pendingCompletion, but for "unknown"
|
|
389
|
-
// functions, this is how we know that they're done, and we can't get their
|
|
390
|
-
// return values.
|
|
391
|
-
return { completionStatus: "success" };
|
|
392
|
-
} else if (workStatus.state.kind === "canceled") {
|
|
393
|
-
return { completionStatus: "canceled" };
|
|
394
|
-
} else if (workStatus.state.kind === "failed") {
|
|
395
|
-
return { completionStatus: "error" };
|
|
396
|
-
}
|
|
397
|
-
return null;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
export const runActionWrapper = internalAction({
|
|
401
|
-
args: {
|
|
402
|
-
workId: v.id("work"),
|
|
403
|
-
fnHandle: v.string(),
|
|
404
|
-
fnArgs: v.any(),
|
|
405
|
-
},
|
|
406
|
-
handler: async (ctx, { workId, fnHandle: handleStr, fnArgs }) => {
|
|
407
|
-
const console_ = await console(ctx);
|
|
408
|
-
const fnHandle = handleStr as FunctionHandle<"action">;
|
|
409
|
-
try {
|
|
410
|
-
await ctx.runAction(fnHandle, fnArgs);
|
|
411
|
-
// NOTE: we could run `ctx.runMutation`, but we want to guarantee execution,
|
|
412
|
-
// and `ctx.scheduler.runAfter` won't OCC.
|
|
413
|
-
await ctx.scheduler.runAfter(0, internal.lib.saveResult, {
|
|
414
|
-
workId,
|
|
415
|
-
completionStatus: "success",
|
|
416
|
-
});
|
|
417
|
-
} catch (e: unknown) {
|
|
418
|
-
console_.error(e);
|
|
419
|
-
await ctx.scheduler.runAfter(0, internal.lib.saveResult, {
|
|
420
|
-
workId,
|
|
421
|
-
completionStatus: "error",
|
|
422
|
-
});
|
|
423
|
-
}
|
|
424
|
-
},
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
export const saveResult = internalMutation({
|
|
428
|
-
args: {
|
|
429
|
-
workId: v.id("work"),
|
|
430
|
-
completionStatus,
|
|
431
|
-
},
|
|
432
|
-
handler: async (ctx, args) => {
|
|
433
|
-
const currentGeneration = await ctx.db
|
|
434
|
-
.query("completionGeneration")
|
|
435
|
-
.unique();
|
|
436
|
-
const generation = currentGeneration?.generation ?? 0;
|
|
437
|
-
await ctx.db.insert("pendingCompletion", {
|
|
438
|
-
completionStatus: args.completionStatus,
|
|
439
|
-
workId: args.workId,
|
|
440
|
-
generation,
|
|
441
|
-
});
|
|
442
|
-
await kickMainLoop(ctx, "saveResult");
|
|
443
|
-
},
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
export const bumpGeneration = internalMutation({
|
|
447
|
-
args: {},
|
|
448
|
-
handler: async (ctx) => {
|
|
449
|
-
const currentGeneration = await ctx.db
|
|
450
|
-
.query("completionGeneration")
|
|
451
|
-
.unique();
|
|
452
|
-
const generation = (currentGeneration?.generation ?? 0) + 1;
|
|
453
|
-
if (!currentGeneration) {
|
|
454
|
-
await ctx.db.insert("completionGeneration", { generation });
|
|
455
|
-
} else {
|
|
456
|
-
await ctx.db.patch(currentGeneration._id, { generation });
|
|
86
|
+
if (canceled.some((c) => c)) {
|
|
87
|
+
await kickMainLoop(ctx, "cancel", { logLevel });
|
|
457
88
|
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
export const runMutationWrapper = internalMutation({
|
|
463
|
-
args: {
|
|
464
|
-
workId: v.id("work"),
|
|
465
|
-
fnHandle: v.string(),
|
|
466
|
-
fnArgs: v.any(),
|
|
467
|
-
},
|
|
468
|
-
handler: async (ctx, { workId, fnHandle: handleStr, fnArgs }) => {
|
|
469
|
-
const console_ = await console(ctx);
|
|
470
|
-
const fnHandle = handleStr as FunctionHandle<"mutation">;
|
|
471
|
-
try {
|
|
472
|
-
await ctx.runMutation(fnHandle, fnArgs);
|
|
473
|
-
// NOTE: we could run the `saveResult` handler here, or call `ctx.runMutation`,
|
|
474
|
-
// but we want the mutation to be a separate transaction to reduce the window for OCCs.
|
|
475
|
-
await ctx.scheduler.runAfter(0, internal.lib.saveResult, {
|
|
476
|
-
workId,
|
|
477
|
-
completionStatus: "success",
|
|
478
|
-
});
|
|
479
|
-
} catch (e: unknown) {
|
|
480
|
-
console_.error(e);
|
|
481
|
-
await ctx.scheduler.runAfter(0, internal.lib.saveResult, {
|
|
482
|
-
workId,
|
|
483
|
-
completionStatus: "error",
|
|
89
|
+
if (pageOfWork.length === PAGE_SIZE) {
|
|
90
|
+
await ctx.scheduler.runAfter(0, api.lib.cancelAll, {
|
|
91
|
+
logLevel,
|
|
92
|
+
before: pageOfWork[pageOfWork.length - 1]._creationTime,
|
|
484
93
|
});
|
|
485
94
|
}
|
|
486
95
|
},
|
|
487
96
|
});
|
|
488
97
|
|
|
489
|
-
async function getMainLoop(ctx: QueryCtx) {
|
|
490
|
-
const mainLoop = await ctx.db.query("mainLoop").unique();
|
|
491
|
-
if (!mainLoop) {
|
|
492
|
-
throw new Error("mainLoop doesn't exist");
|
|
493
|
-
}
|
|
494
|
-
return mainLoop;
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
export const stopCleanup = mutation({
|
|
498
|
-
args: {},
|
|
499
|
-
handler: async (ctx) => {
|
|
500
|
-
const cron = await crons.get(ctx, { name: CLEANUP_CRON_NAME });
|
|
501
|
-
if (cron) {
|
|
502
|
-
await crons.delete(ctx, { id: cron.id });
|
|
503
|
-
}
|
|
504
|
-
},
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
async function loopFromMainLoop(ctx: MutationCtx, delayMs: number) {
|
|
508
|
-
const console_ = await console(ctx);
|
|
509
|
-
const mainLoop = await getMainLoop(ctx);
|
|
510
|
-
if (mainLoop.state.kind === "idle") {
|
|
511
|
-
throw new Error("mainLoop is idle but `loopFromMainLoop` was called");
|
|
512
|
-
}
|
|
513
|
-
if (delayMs <= 0) {
|
|
514
|
-
console_.debug(
|
|
515
|
-
"[mainLoop] mainLoop is actively running and wants to keep running"
|
|
516
|
-
);
|
|
517
|
-
await ctx.scheduler.runAfter(0, internal.lib.bumpGeneration, {});
|
|
518
|
-
if (mainLoop.state.kind !== "running") {
|
|
519
|
-
await ctx.db.patch(mainLoop._id, { state: { kind: "running" } });
|
|
520
|
-
}
|
|
521
|
-
} else if (delayMs < Number.POSITIVE_INFINITY) {
|
|
522
|
-
console_.debug(`[mainLoop] mainLoop wants to run after ${delayMs}ms`);
|
|
523
|
-
const runAtTime = Date.now() + delayMs;
|
|
524
|
-
const fn = await ctx.scheduler.runAt(
|
|
525
|
-
runAtTime,
|
|
526
|
-
internal.lib.bumpGeneration,
|
|
527
|
-
{}
|
|
528
|
-
);
|
|
529
|
-
await ctx.db.patch(mainLoop._id, {
|
|
530
|
-
state: {
|
|
531
|
-
kind: "scheduled",
|
|
532
|
-
fn,
|
|
533
|
-
runAtTime,
|
|
534
|
-
},
|
|
535
|
-
});
|
|
536
|
-
} else {
|
|
537
|
-
console_.debug("[mainLoop] mainLoop wants to become idle");
|
|
538
|
-
await ctx.db.patch(mainLoop._id, { state: { kind: "idle" } });
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
async function kickMainLoop(
|
|
543
|
-
ctx: MutationCtx,
|
|
544
|
-
source: "saveResult" | "enqueue"
|
|
545
|
-
): Promise<void> {
|
|
546
|
-
const console_ = await console(ctx);
|
|
547
|
-
|
|
548
|
-
// Look for mainLoop documents that we want to reschedule.
|
|
549
|
-
// Only kick to run now if we're scheduled or idle.
|
|
550
|
-
const mainLoop = await getMainLoop(ctx);
|
|
551
|
-
if (mainLoop.state.kind === "running") {
|
|
552
|
-
console_.debug(
|
|
553
|
-
`[${source}] mainLoop is actively running, so we don't need to do anything`
|
|
554
|
-
);
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
// mainLoop is scheduled to run later, so we should cancel it and reschedule.
|
|
558
|
-
if (mainLoop.state.kind === "scheduled") {
|
|
559
|
-
await ctx.scheduler.cancel(mainLoop.state.fn);
|
|
560
|
-
}
|
|
561
|
-
const currentGeneration = await ctx.db.query("completionGeneration").unique();
|
|
562
|
-
const generation = currentGeneration?.generation ?? 0;
|
|
563
|
-
await ctx.scheduler.runAfter(0, internal.lib.mainLoop, { generation });
|
|
564
|
-
console_.debug(
|
|
565
|
-
`[${source}] mainLoop was scheduled later, so reschedule it to run now`
|
|
566
|
-
);
|
|
567
|
-
await ctx.db.patch(mainLoop._id, { state: { kind: "running" } });
|
|
568
|
-
}
|
|
569
|
-
|
|
570
98
|
export const status = query({
|
|
571
|
-
args: {
|
|
572
|
-
|
|
573
|
-
},
|
|
574
|
-
returns: v.union(
|
|
575
|
-
v.object({
|
|
576
|
-
kind: v.literal("pending"),
|
|
577
|
-
}),
|
|
578
|
-
v.object({
|
|
579
|
-
kind: v.literal("inProgress"),
|
|
580
|
-
}),
|
|
581
|
-
v.object({
|
|
582
|
-
kind: v.literal("completed"),
|
|
583
|
-
completionStatus,
|
|
584
|
-
})
|
|
585
|
-
),
|
|
99
|
+
args: { id: v.id("work") },
|
|
100
|
+
returns: statusValidator,
|
|
586
101
|
handler: async (ctx, { id }) => {
|
|
587
|
-
const
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
.unique();
|
|
591
|
-
if (completedWork) {
|
|
592
|
-
return {
|
|
593
|
-
kind: "completed",
|
|
594
|
-
completionStatus: completedWork.completionStatus,
|
|
595
|
-
} as const;
|
|
102
|
+
const work = await ctx.db.get(id);
|
|
103
|
+
if (!work) {
|
|
104
|
+
return { state: "finished" } as const;
|
|
596
105
|
}
|
|
597
106
|
const pendingStart = await ctx.db
|
|
598
107
|
.query("pendingStart")
|
|
599
108
|
.withIndex("workId", (q) => q.eq("workId", id))
|
|
600
109
|
.unique();
|
|
601
110
|
if (pendingStart) {
|
|
602
|
-
return {
|
|
111
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
603
112
|
}
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
export const MAX_CLEANUP_DOCS = 1000;
|
|
612
|
-
|
|
613
|
-
export const cleanup = mutation({
|
|
614
|
-
args: {
|
|
615
|
-
maxAgeMs: v.number(),
|
|
616
|
-
},
|
|
617
|
-
handler: async (ctx, { maxAgeMs }) => {
|
|
618
|
-
const old = Date.now() - maxAgeMs;
|
|
619
|
-
const docs = await ctx.db
|
|
620
|
-
.query("completedWork")
|
|
621
|
-
.withIndex("by_creation_time", (q) => q.lte("_creationTime", old))
|
|
622
|
-
.order("desc")
|
|
623
|
-
.take(MAX_CLEANUP_DOCS);
|
|
624
|
-
await Promise.all(
|
|
625
|
-
docs.map(async (doc) => {
|
|
626
|
-
await ctx.db.delete(doc._id);
|
|
627
|
-
const work = await ctx.db.get(doc.workId);
|
|
628
|
-
if (work) {
|
|
629
|
-
await ctx.db.delete(work._id);
|
|
630
|
-
}
|
|
631
|
-
})
|
|
632
|
-
);
|
|
633
|
-
if (docs.length === MAX_CLEANUP_DOCS) {
|
|
634
|
-
// Schedule the next cleanup to run starting from the oldest document.
|
|
635
|
-
await ctx.scheduler.runAfter(0, api.lib.cleanup, {
|
|
636
|
-
maxAgeMs: docs[docs.length - 1]._creationTime,
|
|
637
|
-
});
|
|
113
|
+
const pendingCompletion = await ctx.db
|
|
114
|
+
.query("pendingCompletion")
|
|
115
|
+
.withIndex("workId", (q) => q.eq("workId", id))
|
|
116
|
+
.unique();
|
|
117
|
+
if (pendingCompletion?.retry) {
|
|
118
|
+
return { state: "pending", previousAttempts: work.attempts } as const;
|
|
638
119
|
}
|
|
120
|
+
// Assume it's in progress. It could be pending cancelation
|
|
121
|
+
return { state: "running", previousAttempts: work.attempts } as const;
|
|
639
122
|
},
|
|
640
123
|
});
|
|
641
124
|
|
|
642
|
-
|
|
643
|
-
const CLEANUP_CRON_NAME = "cleanup";
|
|
644
|
-
|
|
645
|
-
async function ensurePoolAndLoopExist(
|
|
125
|
+
async function cancelWorkItem(
|
|
646
126
|
ctx: MutationCtx,
|
|
647
|
-
|
|
648
|
-
|
|
127
|
+
workId: Id<"work">,
|
|
128
|
+
segment: bigint,
|
|
129
|
+
logLevel: LogLevel
|
|
649
130
|
) {
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
console_.debug(`[${source}] starting mainLoop`);
|
|
671
|
-
const exists = await ctx.db.query("mainLoop").unique();
|
|
672
|
-
if (exists) {
|
|
673
|
-
throw new Error("mainLoop already exists");
|
|
674
|
-
}
|
|
675
|
-
await ctx.db.insert("mainLoop", { state: { kind: "running" } });
|
|
676
|
-
const currentGeneration = await ctx.db
|
|
677
|
-
.query("completionGeneration")
|
|
678
|
-
.unique();
|
|
679
|
-
if (currentGeneration) {
|
|
680
|
-
throw new Error("completionGeneration already exists");
|
|
681
|
-
}
|
|
682
|
-
await ctx.db.insert("completionGeneration", { generation: 0 });
|
|
683
|
-
await ctx.scheduler.runAfter(0, internal.lib.mainLoop, { generation: 0 });
|
|
684
|
-
}
|
|
685
|
-
await ensureCleanupCron(ctx, opts.statusTtl);
|
|
131
|
+
const console = createLogger(logLevel);
|
|
132
|
+
// No-op if the work doesn't exist or has completed.
|
|
133
|
+
const work = await ctx.db.get(workId);
|
|
134
|
+
if (!work) {
|
|
135
|
+
console.warn(`[cancel] work ${workId} doesn't exist`);
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const pendingCancelation = await ctx.db
|
|
139
|
+
.query("pendingCancelation")
|
|
140
|
+
.withIndex("workId", (q) => q.eq("workId", workId))
|
|
141
|
+
.unique();
|
|
142
|
+
if (pendingCancelation) {
|
|
143
|
+
console.warn(`[cancel] work ${workId} has already been canceled`);
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
await ctx.db.insert("pendingCancelation", {
|
|
147
|
+
workId,
|
|
148
|
+
segment,
|
|
149
|
+
});
|
|
150
|
+
return true;
|
|
686
151
|
}
|
|
687
152
|
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
if (ttl === Number.POSITIVE_INFINITY) {
|
|
691
|
-
if (cleanupCron) {
|
|
692
|
-
await crons.delete(ctx, { id: cleanupCron.id });
|
|
693
|
-
}
|
|
694
|
-
return;
|
|
695
|
-
}
|
|
696
|
-
const cronFrequencyMs = Math.min(ttl, 24 * 60 * 60 * 1000);
|
|
697
|
-
if (
|
|
698
|
-
cleanupCron !== null &&
|
|
699
|
-
!(
|
|
700
|
-
cleanupCron.schedule.kind === "interval" &&
|
|
701
|
-
cleanupCron.schedule.ms === cronFrequencyMs
|
|
702
|
-
)
|
|
703
|
-
) {
|
|
704
|
-
await crons.delete(ctx, { id: cleanupCron.id });
|
|
705
|
-
cleanupCron = null;
|
|
706
|
-
}
|
|
707
|
-
if (cleanupCron === null) {
|
|
708
|
-
await crons.register(
|
|
709
|
-
ctx,
|
|
710
|
-
{ kind: "interval", ms: ttl },
|
|
711
|
-
api.lib.cleanup,
|
|
712
|
-
{ maxAgeMs: ttl },
|
|
713
|
-
CLEANUP_CRON_NAME
|
|
714
|
-
);
|
|
715
|
-
}
|
|
716
|
-
}
|
|
153
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
154
|
+
const console = "THIS IS A REMINDER TO USE createLogger";
|