@convex-dev/workpool
Version:
A Convex component for managing async work.
102 lines • 4.17 kB
JavaScript
import { internal } from "./_generated/api.js";
import { internalMutation } from "./_generated/server.js";
import { createLogger, DEFAULT_LOG_LEVEL } from "./logging.js";
import { INITIAL_STATE } from "./loop.js";
import { boundScheduledTime, DEFAULT_MAX_PARALLELISM, fromSegment, getCurrentSegment, getNextSegment, SECOND, toSegment, } from "./shared.js";
/**
* Called from outside the loop.
* Returns the soonest segment to enqueue work for the main loop.
*/
export async function kickMainLoop(ctx, source, config) {
const globals = await getOrUpdateGlobals(ctx, config);
const console = createLogger(globals.logLevel);
const runStatus = await getOrCreateRunStatus(ctx);
const next = getNextSegment();
// Only kick to run now if we're scheduled or idle.
if (runStatus.state.kind === "running") {
console.debug(`[${source}] main is actively running, so we don't need to kick it`);
return next;
}
// main is scheduled to run later, so we should cancel it and reschedule.
if (runStatus.state.kind === "scheduled") {
if (source === "enqueue" && runStatus.state.saturated) {
console.debug(`[${source}] main is saturated, so we don't need to kick it`);
return next;
}
if (runStatus.state.segment <= toSegment(Date.now() + SECOND)) {
console.debug(`[${source}] main is scheduled to run soon enough, so we don't need to kick it`);
return next;
}
console.debug(`[${source}] main is scheduled to run later, so reschedule it to run now`);
const scheduled = await ctx.db.system.get(runStatus.state.scheduledId);
if (scheduled && scheduled.state.kind === "pending") {
await ctx.scheduler.cancel(runStatus.state.scheduledId);
}
else {
console.warn(`[${source}] main is marked as scheduled, but it's status is ${scheduled?.state.kind}`);
}
}
else if (runStatus.state.kind === "idle") {
console.debug(`[${source}] main was idle, so run it now`);
}
await ctx.db.patch(runStatus._id, { state: { kind: "running" } });
const current = getCurrentSegment();
const scheduledTime = boundScheduledTime(fromSegment(current), console);
await ctx.scheduler.runAt(scheduledTime, internal.loop.main, {
generation: runStatus.state.generation,
segment: current,
});
return current;
}
export const forceKick = internalMutation({
args: {},
handler: async (ctx) => {
const runStatus = await getOrCreateRunStatus(ctx);
await ctx.db.delete(runStatus._id);
await kickMainLoop(ctx, "kick");
},
});
async function getOrCreateRunStatus(ctx) {
let runStatus = await ctx.db.query("runStatus").unique();
if (!runStatus) {
const state = await ctx.db.query("internalState").unique();
const id = await ctx.db.insert("runStatus", {
state: {
kind: "idle",
generation: state?.generation ?? INITIAL_STATE.generation,
},
});
runStatus = (await ctx.db.get(id));
if (!state) {
await ctx.db.insert("internalState", INITIAL_STATE);
}
}
return runStatus;
}
async function getOrUpdateGlobals(ctx, config) {
const globals = await ctx.db.query("globals").unique();
if (!globals) {
const id = await ctx.db.insert("globals", {
maxParallelism: config?.maxParallelism ?? DEFAULT_MAX_PARALLELISM,
logLevel: config?.logLevel ?? DEFAULT_LOG_LEVEL,
});
return (await ctx.db.get(id));
}
else if (config) {
let updated = false;
if (config.maxParallelism &&
config.maxParallelism !== globals.maxParallelism) {
globals.maxParallelism = config.maxParallelism;
updated = true;
}
if (config.logLevel && config.logLevel !== globals.logLevel) {
globals.logLevel = config.logLevel;
updated = true;
}
if (updated) {
await ctx.db.replace(globals._id, globals);
}
}
return globals;
}
//# sourceMappingURL=kick.js.map