@convex-dev/workpool
Version:
A Convex component for managing async work.
82 lines • 3.16 kB
JavaScript
import { v } from "convex/values";
import { internalMutation } from "./_generated/server.js";
import { kickMainLoop } from "./kick.js";
import { createLogger } from "./logging.js";
import { vResultValidator } from "./shared.js";
import { recordCompleted } from "./stats.js";
export const completeArgs = v.object({
jobs: v.array(v.object({
runResult: vResultValidator,
workId: v.id("work"),
attempt: v.number(),
})),
});
export async function completeHandler(ctx, args) {
const globals = await ctx.db.query("globals").unique();
const console = createLogger(globals?.logLevel);
const pendingCompletions = [];
await Promise.all(args.jobs.map(async (job) => {
const work = await ctx.db.get(job.workId);
if (!work) {
console.warn(`[complete] ${job.workId} is done, but its work is gone`);
return;
}
if (work.attempts !== job.attempt) {
console.warn(`[complete] ${job.workId} mismatched attempt number`);
return;
}
work.attempts++;
await ctx.db.patch(work._id, { attempts: work.attempts });
const pendingCompletion = await ctx.db
.query("pendingCompletion")
.withIndex("workId", (q) => q.eq("workId", job.workId))
.unique();
if (pendingCompletion) {
console.warn(`[complete] ${job.workId} already in pendingCompletion`);
return;
}
const maxAttempts = work.retryBehavior?.maxAttempts;
const retry = job.runResult.kind === "failed" &&
!!maxAttempts &&
work.attempts < maxAttempts;
if (!retry) {
if (work.onComplete) {
try {
const handle = work.onComplete.fnHandle;
await ctx.runMutation(handle, {
workId: work._id,
context: work.onComplete.context,
result: job.runResult,
});
console.debug(`[complete] onComplete for ${job.workId} completed`);
}
catch (e) {
console.error(`[complete] error running onComplete for ${job.workId}`, e);
// TODO: store failures in a table for later debugging
}
}
recordCompleted(console, work, job.runResult.kind);
// This is the terminating state for work.
await ctx.db.delete(job.workId);
}
if (job.runResult.kind !== "canceled") {
pendingCompletions.push({
runResult: job.runResult,
workId: job.workId,
retry,
});
}
}));
if (pendingCompletions.length > 0) {
const segment = await kickMainLoop(ctx, "complete");
await Promise.all(pendingCompletions.map((completion) => ctx.db.insert("pendingCompletion", {
...completion,
segment,
})));
}
}
export const complete = internalMutation({
args: completeArgs,
handler: completeHandler,
});
//# sourceMappingURL=complete.js.map