@convex-dev/workpool
Version:
A Convex component for managing async work.
129 lines • 4.82 kB
JavaScript
import { v } from "convex/values";
import { internalMutation, internalQuery, } from "./_generated/server.js";
import { DEFAULT_MAX_PARALLELISM, getCurrentSegment, } from "./shared.js";
import { createLogger, logLevel, shouldLog } from "./logging.js";
import { internal } from "./_generated/api.js";
import schema from "./schema.js";
import { paginator } from "convex-helpers/server/pagination";
/**
* Record stats about work execution. Intended to be queried by Axiom or Datadog.
* See the [README](https://github.com/get-convex/workpool) for example queries.
*/
export function recordEnqueued(console, data) {
console.event("enqueued", {
...data,
enqueuedAt: Date.now(),
});
}
export function recordStarted(console, work, lagMs) {
console.event("started", {
workId: work._id,
fnName: work.fnName,
enqueuedAt: work._creationTime,
startedAt: Date.now(),
startLag: lagMs,
});
}
export function recordCompleted(console, work, status) {
console.event("completed", {
workId: work._id,
fnName: work.fnName,
completedAt: Date.now(),
attempts: work.attempts,
status,
});
}
export async function generateReport(ctx, console, state, { maxParallelism, logLevel }) {
if (!shouldLog(logLevel, "REPORT")) {
// Don't waste time if we're not going to log.
return;
}
const currentSegment = getCurrentSegment();
const pendingStart = await paginator(ctx.db, schema)
.query("pendingStart")
.withIndex("segment", (q) => q
.gte("segment", state.segmentCursors.incoming)
.lt("segment", currentSegment))
.paginate({
numItems: maxParallelism,
cursor: null,
});
if (pendingStart.isDone) {
recordReport(console, {
...state.report,
running: state.running.length,
backlog: pendingStart.page.length,
});
}
else {
await ctx.scheduler.runAfter(0, internal.stats.calculateBacklogAndReport, {
startSegment: 0n,
endSegment: currentSegment,
cursor: pendingStart.continueCursor,
report: state.report,
running: state.running.length,
logLevel,
});
}
}
export const calculateBacklogAndReport = internalMutation({
args: {
startSegment: v.int64(),
endSegment: v.int64(),
cursor: v.string(),
report: schema.tables.internalState.validator.fields.report,
running: v.number(),
logLevel,
},
handler: async (ctx, args) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pendingStart = await ctx.db.query("pendingStart").count();
const console = createLogger(args.logLevel);
recordReport(console, {
...args.report,
running: args.running,
backlog: pendingStart,
});
},
});
function recordReport(console, report) {
const { completed, failed, retries } = report;
const withoutRetries = completed - retries;
const failureRate = completed ? (failed + retries) / completed : 0;
const permanentFailureRate = withoutRetries ? failed / withoutRetries : 0;
console.event("report", {
...report,
failureRate: Number(failureRate.toFixed(4)),
permanentFailureRate: Number(permanentFailureRate.toFixed(4)),
});
}
/**
* Warning: this should not be used from a mutation, as it will cause conflicts.
* Use this while developing to see the state of the queue.
*/
export const diagnostics = internalQuery({
args: {},
returns: v.any(),
handler: async (ctx) => {
const global = await ctx.db.query("globals").unique();
const internalState = await ctx.db.query("internalState").unique();
const inProgressWork = internalState?.running.length ?? 0;
const maxParallelism = global?.maxParallelism ?? DEFAULT_MAX_PARALLELISM;
/* eslint-disable @typescript-eslint/no-explicit-any */
const pendingStart = await ctx.db.query("pendingStart").count();
const pendingCompletion = await ctx.db.query("pendingCompletion").count();
const pendingCancelation = await ctx.db.query("pendingCancelation").count();
const runStatus = await ctx.db.query("runStatus").unique();
/* eslint-enable @typescript-eslint/no-explicit-any */
return {
canceling: pendingCancelation,
waiting: pendingStart,
running: inProgressWork - pendingCompletion,
completing: pendingCompletion,
spareCapacity: maxParallelism - inProgressWork,
runStatus: runStatus?.state.kind,
generation: internalState?.generation,
};
},
});
//# sourceMappingURL=stats.js.map