@convex-dev/workflow
Version:
Convex component for durably executing workflows.
112 lines • 4.33 kB
JavaScript
import { createFunctionHandle, } from "convex/server";
import { convexToJson } from "convex/values";
import { journalEntrySize, valueSize, } from "../component/schema.js";
const MAX_JOURNAL_SIZE = 1 << 20;
export class StepExecutor {
workflowId;
generationNumber;
ctx;
component;
journalEntries;
receiver;
originalEnv;
workpoolOptions;
journalEntrySize;
constructor(workflowId, generationNumber, ctx, component, journalEntries, receiver, originalEnv, workpoolOptions) {
this.workflowId = workflowId;
this.generationNumber = generationNumber;
this.ctx = ctx;
this.component = component;
this.journalEntries = journalEntries;
this.receiver = receiver;
this.originalEnv = originalEnv;
this.workpoolOptions = workpoolOptions;
this.journalEntrySize = journalEntries.reduce((size, entry) => size + journalEntrySize(entry), 0);
}
async run() {
// eslint-disable-next-line no-constant-condition
while (true) {
const message = await this.receiver.get();
const entry = this.journalEntries.shift();
// why not to run queries inline: they fetch too much data internally
if (entry) {
this.completeMessage(message, entry);
continue;
}
// TODO: is this too late?
if (this.journalEntrySize > MAX_JOURNAL_SIZE) {
message.reject(journalSizeError(this.journalEntrySize));
continue;
}
const messages = [message];
const size = this.receiver.bufferSize;
for (let i = 0; i < size; i++) {
const message = await this.receiver.get();
messages.push(message);
}
for (const message of messages) {
await this.startStep(message);
}
return {
type: "executorBlocked",
};
}
}
completeMessage(message, entry) {
if (entry.step.inProgress) {
throw new Error(`Assertion failed: not blocked but have in-progress journal entry`);
}
const stepArgsJson = JSON.stringify(convexToJson(entry.step.args));
const messageArgsJson = JSON.stringify(convexToJson(message.args));
if (stepArgsJson !== messageArgsJson) {
throw new Error(`Journal entry mismatch: ${entry.step.args} !== ${message.args}`);
}
if (entry.step.runResult === undefined) {
throw new Error(`Assertion failed: no outcome for completed function call`);
}
switch (entry.step.runResult.kind) {
case "success":
message.resolve(entry.step.runResult.returnValue);
break;
case "failed":
message.reject(new Error(entry.step.runResult.error));
break;
case "canceled":
message.reject(new Error("Canceled"));
break;
}
}
async startStep(message) {
const step = {
inProgress: true,
name: message.name,
functionType: message.functionType,
handle: await createFunctionHandle(message.function),
args: message.args,
argsSize: valueSize(message.args),
outcome: undefined,
startedAt: this.originalEnv.Date.now(),
completedAt: undefined,
};
const entry = (await this.ctx.runMutation(this.component.journal.startStep, {
workflowId: this.workflowId,
generationNumber: this.generationNumber,
step,
name: message.name,
retry: message.retry,
workpoolOptions: this.workpoolOptions,
schedulerOptions: message.schedulerOptions,
}));
this.journalEntrySize += journalEntrySize(entry);
return entry;
}
}
function journalSizeError(size) {
const lines = [
`Workflow journal size limit exceeded (${size} bytes > ${MAX_JOURNAL_SIZE} bytes).`,
"Consider breaking up the workflow into multiple runs, using smaller step \
arguments or return values, or using fewer steps.",
];
return new Error(lines.join("\n"));
}
//# sourceMappingURL=step.js.map