@convex-dev/action-retrier
Version:
Convex component for retrying idempotent actions.
156 lines • 6.65 kB
JavaScript
import { createFunctionHandle, } from "convex/server";
import { v } from "convex/values";
import { runResult } from "../component/schema.js";
export const runIdValidator = v.string();
export const onCompleteValidator = v.object({
runId: runIdValidator,
result: runResult,
});
const DEFAULT_INITIAL_BACKOFF_MS = 250;
const DEFAULT_BASE = 2;
const DEFAULT_MAX_FAILURES = 4;
export class ActionRetrier {
component;
options;
/**
* Create a new ActionRetrier, which retries failed actions with exponential backoff.
* ```ts
* import { components } from "./_generated/server"
* const actionRetrier = new ActionRetrier(components.actionRetrier)
*
* // In a mutation or action...
* await actionRetrier.run(ctx, internal.module.myAction, { arg: 123 });
* ```
*
* @param component - The registered action retrier from `components`.
* @param options - Optional overrides for the default backoff and retry behavior.
*/
constructor(component, options) {
this.component = component;
let DEFAULT_LOG_LEVEL = "INFO";
if (process.env.ACTION_RETRIER_LOG_LEVEL) {
if (!["DEBUG", "INFO", "WARN", "ERROR"].includes(process.env.ACTION_RETRIER_LOG_LEVEL)) {
console.warn(`Invalid log level (${process.env.ACTION_RETRIER_LOG_LEVEL}), defaulting to "INFO"`);
}
DEFAULT_LOG_LEVEL = process.env.ACTION_RETRIER_LOG_LEVEL;
}
this.options = {
initialBackoffMs: options?.initialBackoffMs ?? DEFAULT_INITIAL_BACKOFF_MS,
base: options?.base ?? DEFAULT_BASE,
maxFailures: options?.maxFailures ?? DEFAULT_MAX_FAILURES,
logLevel: options?.logLevel ?? DEFAULT_LOG_LEVEL,
};
}
/**
* Run an action with retries, optionally with an `onComplete` mutation callback.
*
* @param ctx - The context object from your mutation or action.
* @param reference - The function reference to run, e.g., `internal.module.myAction`.
* @param args - Arguments for the action, e.g., `{ arg: 123 }`.
* @param options.initialBackoffMs - Optional override for the default initial backoff on failure.
* @param options.base - Optional override for the default base for the exponential backoff.
* @param options.maxFailures - Optional override for the default maximum number of retries.
* @param options.onComplete - Optional mutation to run after the function succeeds, fails,
* or is canceled. This function must take in a single `result` argument of type `RunResult`: use
* `runResultValidator` to validate this argument.
* @returns - A `RunId` for the run that can be used to query its status below.
*/
async run(ctx, reference, args, options) {
const handle = await createFunctionHandle(reference);
let onComplete;
if (options?.onComplete) {
onComplete = await createFunctionHandle(options.onComplete);
}
const runId = await ctx.runMutation(this.component.public.start, {
functionHandle: handle,
functionArgs: args ?? {},
options: {
...this.options,
...stripUndefined(options),
onComplete,
},
});
return runId;
}
/**
* Run an action like {@link run} but no earlier than a specific timestamp.
*
* @param ctx - The context object from your mutation or action.
* @param runAtTimestampMs - The timestamp in milliseconds to run the action at.
* @param reference - The function reference to run, e.g., `internal.module.myAction`.
* @param args - Arguments for the action, e.g., `{ arg: 123 }`.
* @param options - See {@link RunOptions}.
*/
async runAt(ctx, runAtTimestampMs, reference, args, options) {
const opts = {
...options,
runAt: runAtTimestampMs,
};
return this.run(ctx, reference, args, opts);
}
/**
* Run an action like {@link run} but no earlier than after specific delay.
*
* Note: the delay is from the time of calling this, not from when it's made
* it to the front of the queue.
*
* @param ctx - The context object from your mutation or action.
* @param runAfterMs - The delay in milliseconds before running the action.
* @param reference - The function reference to run, e.g., `internal.module.myAction`.
* @param args - Arguments for the action, e.g., `{ arg: 123 }`.
* @param options - See {@link RunOptions}.
*/
async runAfter(ctx, runAfterMs, reference, args, options) {
const opts = {
...options,
runAfter: runAfterMs,
};
return this.run(ctx, reference, args, opts);
}
/**
* Query the status of a run.
*
* @param ctx - The context object from your query, mutation, or action.
* @param runId - The `RunId` returned from `run`.
* @returns - An object indicating whether the run is in progress or has completed. If
* the run has completed, the `result.type` field indicates whether it succeeded,
* failed, or was canceled.
*/
async status(ctx, runId) {
return ctx.runQuery(this.component.public.status, { runId });
}
/**
* Attempt to cancel a run. This method throws if the run isn't currently executing.
* If the run is currently executing (and not waiting for retry), action execution may
* continue after this method successfully returns.
*
* @param ctx - The context object from your mutation or action.
* @param runId - The `RunId` returned from `run`.
*/
async cancel(ctx, runId) {
await ctx.runMutation(this.component.public.cancel, { runId });
}
/**
* Cleanup a completed run's storage from the system. This method throws if the run
* doesn't exist or isn't in the completed state.
*
* The system will also automatically clean up runs that are more than 7 days old.
*
* @param ctx - The context object from your mutation or action.
* @param runId - The `RunId` returned from `run`.
*/
async cleanup(ctx, runId) {
await ctx.runMutation(this.component.public.cleanup, { runId });
}
}
function stripUndefined(obj) {
if (obj === undefined) {
return obj;
}
return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined));
}
/**
* Validator for the `result` argument of the `onComplete` callback.
*/
export const runResultValidator = runResult;
//# sourceMappingURL=index.js.map