UNPKG

durable-execution

Version:

A durable task engine for running tasks durably and resiliently

346 lines 16.8 kB
import type { StandardSchemaV1 } from '@standard-schema/spec'; import { type Logger } from './logger'; import { type Serializer } from './serializer'; import { type DurableStorage } from './storage'; import { type DurableChildTaskExecutionOutput, type DurableParentTaskOptions, type DurableTask, type DurableTaskEnqueueOptions, type DurableTaskHandle, type DurableTaskOptions } from './task'; export { type CancelSignal, createCancelSignal, createTimeoutCancelSignal, createCancellablePromise, } from './cancel'; export { DurableExecutionError, DurableExecutionTimedOutError, DurableExecutionCancelledError, type DurableExecutionErrorStorageObject, } from './errors'; export { type Logger, createConsoleLogger } from './logger'; export type { DurableTask, DurableTaskCommonOptions, DurableTaskRetryOptions, DurableTaskOptions, DurableParentTaskOptions, DurableFinalizeTaskOptions, DurableTaskOnChildrenCompleteInput, DurableTaskRunContext, DurableTaskExecution, DurableTaskFinishedExecution, DurableTaskReadyExecution, DurableTaskRunningExecution, DurableTaskFailedExecution, DurableTaskTimedOutExecution, DurableTaskCancelledExecution, DurableTaskWaitingForChildrenTasksExecution, DurableTaskChildrenTasksFailedExecution, DurableTaskWaitingForFinalizeTaskExecution, DurableTaskFinalizeTaskFailedExecution, DurableTaskCompletedExecution, DurableChildTask, DurableChildTaskExecution, DurableChildTaskExecutionOutput, DurableChildTaskExecutionError, DurableChildTaskExecutionErrorStorageObject, DurableTaskExecutionStatusStorageObject, DurableTaskEnqueueOptions, DurableTaskHandle, } from './task'; export { type Serializer, createSuperjsonSerializer, WrappedSerializer } from './serializer'; export { createInMemoryStorage, createTransactionMutex, type TransactionMutex, type DurableStorage, type DurableStorageTx, type DurableTaskExecutionStorageObject, type DurableTaskExecutionStorageWhere, type DurableTaskExecutionStorageObjectUpdate, } from './storage'; /** * A durable executor. It is used to execute durable tasks. * * Multiple durable executors can share the same storage. In such a case, all the tasks should be * present for all the durable executors. The work is distributed among the durable executors. * See the [usage](https://gpahal.github.io/durable-execution/index.html#usage) and * [task examples](https://gpahal.github.io/durable-execution/index.html#task-examples) * sections for more details on creating and enqueuing tasks. * * @example * ```ts * const executor = new DurableExecutor(storage) * * // Create tasks * const extractFileTitle = executor * .inputSchema(v.object({ filePath: v.string() })) * .task({ * id: 'extractFileTitle', * timeoutMs: 30_000, // 30 seconds * run: async (ctx, input) => { * // ... extract the file title * return { * title: 'File Title', * } * }, * }) * * const summarizeFile = executor * .validateInput(async (input: { filePath: string }) => { * if (!isValidFilePath(input.filePath)) { * throw new Error('Invalid file path') * } * return { * filePath: input.filePath, * } * }) * .task({ * id: 'summarizeFile', * timeoutMs: 30_000, // 30 seconds * run: async (ctx, input) => { * // ... summarize the file * return { * summary: 'File summary', * } * }, * }) * * const uploadFile = executor * .inputSchema(v.object({ filePath: v.string(), uploadUrl: v.string() })) * .parentTask({ * id: 'uploadFile', * timeoutMs: 60_000, // 1 minute * runParent: async (ctx, input) => { * // ... upload file to the given uploadUrl * // Extract the file title and summarize the file in parallel * return { * output: { * filePath: input.filePath, * uploadUrl: input.uploadUrl, * fileSize: 100, * }, * childrenTasks: [ * { * task: extractFileTitle, * input: { filePath: input.filePath }, * }, * { * task: summarizeFile, * input: { filePath: input.filePath }, * }, * ], * } * }, * finalizeTask: { * id: 'onUploadFileAndChildrenComplete', * timeoutMs: 60_000, // 1 minute * run: async (ctx, { input, output, childrenTasksOutputs }) => { * // ... combine the output of the run function and children tasks * return { * filePath: input.filePath, * uploadUrl: input.uploadUrl, * fileSize: 100, * title: 'File Title', * summary: 'File summary', * } * } * }, * }) * * async function app() { * // Enqueue task and manage its execution lifecycle * const uploadFileHandle = await executor.enqueueTask(uploadFile, {filePath: 'file.txt'}) * const uploadFileExecution = await uploadFileHandle.getExecution() * const uploadFileFinishedExecution = await uploadFileHandle.waitAndGetExecution() * await uploadFileHandle.cancel() * * console.log(uploadFileExecution) * } * * // Start the durable executor and run the app * await Promise.all([ * executor.start(), // Start the durable executor in the background * app(), // Run the app * ]) * * // Shutdown the durable executor when the app is done * await executor.shutdown() * ``` * * @category Executor */ export declare class DurableExecutor { private readonly storage; private readonly serializer; private readonly logger; private readonly expireMs; private readonly backgroundProcessIntraBatchSleepMs; private readonly taskInternalsMap; private readonly runningTaskExecutionsMap; private readonly shutdownSignal; private readonly internalCancel; private startPromise; /** * Create a durable executor. * * @param storage - The storage to use for the durable executor. * @param options - The options for the durable executor. * @param options.serializer - The serializer to use for the durable executor. If not provided, a * default serializer using superjson will be used. * @param options.logger - The logger to use for the durable executor. If not provided, a console * logger will be used. * @param options.enableDebug - Whether to enable debug logging. If `true`, debug logging will * be enabled. */ constructor(storage: DurableStorage, { serializer, logger, enableDebug, expireMs, backgroundProcessIntraBatchSleepMs, }?: { serializer?: Serializer; logger?: Logger; enableDebug?: boolean; expireMs?: number; backgroundProcessIntraBatchSleepMs?: number; }); /** * Execute a function with a transaction. Supports retries. * * @param fn - The function to execute. * @param maxRetryAttempts - The maximum number of times to retry the transaction. * @returns The result of the function. */ private withTransaction; private throwIfShutdown; /** * Start the durable executor. Starts a background process. Use {@link DurableExecutor.shutdown} * to stop the durable executor. */ start(): Promise<void>; private startBackgroundProcesses; /** * Shutdown the durable executor. Cancels all active executions and stops the background * process. * * On shutdown, these happen in this order: * - Stop enqueuing new tasks * - Stop background processes after the current iteration * - Wait for active task executions to finish. Task execution context contains a shutdown signal * that can be used to gracefully shutdown the task when executor is shutting down. */ shutdown(): Promise<void>; /** * Add a task to the durable executor. See the * [task examples](https://gpahal.github.io/durable-execution/index.html#task-examples) section * for more details on creating tasks. * * @param taskOptions - The task options. See {@link DurableTaskOptions} for more details on the * task options. * @returns The durable task. */ task<TInput = unknown, TOutput = unknown>(taskOptions: DurableTaskOptions<TInput, TOutput>): DurableTask<TInput, TOutput>; /** * Add a parent task to the durable executor. See the * [task examples](https://gpahal.github.io/durable-execution/index.html#task-examples) section * for more details on creating parent tasks. * * @param parentTaskOptions - The parent task options. See {@link DurableParentTaskOptions} for * more details on the parent task options. * @returns The durable parent task. */ parentTask<TInput = unknown, TRunOutput = unknown, TOutput = { output: TRunOutput; childrenTasksOutputs: Array<DurableChildTaskExecutionOutput>; }, TFinalizeTaskRunOutput = unknown>(parentTaskOptions: DurableParentTaskOptions<TInput, TRunOutput, TOutput, TFinalizeTaskRunOutput>): DurableTask<TInput, TOutput>; validateInput<TRunInput, TInput>(validateInputFn: (input: TInput) => TRunInput | Promise<TRunInput>): { task: <TOutput = unknown>(taskOptions: DurableTaskOptions<TRunInput, TOutput>) => DurableTask<TInput, TOutput>; parentTask: <TRunOutput = unknown, TOutput = { output: TRunOutput; childrenTasksOutputs: Array<DurableChildTaskExecutionOutput>; }, TFinalizeTaskRunOutput = unknown>(parentTaskOptions: DurableParentTaskOptions<TRunInput, TRunOutput, TOutput, TFinalizeTaskRunOutput>) => DurableTask<TInput, TOutput>; }; inputSchema<TInputSchema extends StandardSchemaV1>(inputSchema: TInputSchema): { task: <TOutput = unknown>(taskOptions: DurableTaskOptions<StandardSchemaV1.InferOutput<TInputSchema>, TOutput>) => DurableTask<StandardSchemaV1.InferInput<TInputSchema>, TOutput>; parentTask: <TRunOutput = unknown, TOutput = { output: TRunOutput; childrenTasksOutputs: Array<DurableChildTaskExecutionOutput>; }>(parentTaskOptions: DurableParentTaskOptions<StandardSchemaV1.InferOutput<TInputSchema>, TRunOutput, TOutput>) => DurableTask<StandardSchemaV1.InferInput<TInputSchema>, TOutput>; }; private withValidateInputInternal; /** * Create a new task that runs a sequence of tasks sequentially. * * The tasks list must be a list of tasks that are compatible with each other. The input of any * task must be the same as the output of the previous task. The output of the last task will be * the output of the sequential task. * * The tasks list cannot be empty. * * @param tasks - The tasks to run sequentially. * @returns The sequential task. */ sequentialTasks<T extends ReadonlyArray<DurableTask<unknown, unknown>>>(...tasks: SequentialDurableTasks<T>): DurableTask<ExtractDurableTaskInput<T[0]>, ExtractDurableTaskOutput<LastElement<T>>>; /** * Enqueue a task for execution. * * @param task - The task to enqueue. * @param input - The input to the task. * @returns A handle to the task execution. */ enqueueTask<TTask extends DurableTask<unknown, unknown>>(task: TTask, input: ExtractDurableTaskInput<TTask>, options?: DurableTaskEnqueueOptions): Promise<DurableTaskHandle<ExtractDurableTaskOutput<TTask>>>; /** * Get a handle to a task execution. * * @param task - The task to get the handle for. * @param executionId - The id of the execution to get the handle for. * @returns The handle to the task execution. */ getTaskHandle<TInput = unknown, TOutput = unknown>(task: DurableTask<TInput, TOutput>, executionId: string): Promise<DurableTaskHandle<TOutput>>; private getTaskHandleInternal; private runBackgroundProcess; private closeFinishedTaskExecutions; /** * Close finished task executions. */ private closeFinishedTaskExecutionsSingleBatch; /** * Close finished task execution parent. If the execution status is completed, it will update the * status of the parent to completed if it is waiting for children and all the children are * completed. If the parent execution has already finished, it will just update the children * state. */ private closeFinishedTaskExecutionParent; /** * Close finished task execution children and cancel running children task executions. */ private closeFinishedTaskExecutionChildren; private retryExpiredRunningTaskExecutions; /** * Retry expired running task executions. This will only happen when the process running the * execution previously crashed. */ private retryExpiredRunningTaskExecutionsSingleBatch; private cancelNeedPromiseCancellationTaskExecutions; /** * Cancel task executions that need promise cancellation. */ private cancelNeedPromiseCancellationTaskExecutionsSingleBatch; private processReadyTaskExecutions; /** * Process task executions that are ready to run based on status being ready and startAt * being in the past. */ private processReadyTaskExecutionsSingleBatch; /** * Run a task execution with a cancel signal. It is expected to be in running state. * * It will add the execution to the running executions map and make sure it is removed from the * running executions map if the task execution completes. If the process crashes, the execution * will be retried later on expiration. * * @param taskInternal - The task internal to run. * @param execution - The task execution to run. */ private runTaskExecutionWithCancelSignal; /** * Run a task execution with a context. It is expected to be in running state and present in the * running executions map. * * It will update the execution status to failed, timed_out, cancelled, * waiting_for_children_tasks, waiting_for_finalize_task, completed, or ready depending on the * result of the task. If the task completes successfully, it will update the execution status to * waiting_for_children_tasks, waiting_for_finalize_task, or completed. If the task fails, it will * update the execution status to failed, timed_out, cancelled, or ready depending on the error. * If the error is retryable and the retry attempts are less than the maximum retry attempts, it * will update the execution status to ready. All the errors are saved in storage even if the task * is retried. They only get cleared if the execution is completed later. * * If `runTaskExecutionWithContext` runs to completion, the running executions map is updated to * remove the execution. All this is atomic so the execution is guaranteed to be removed from the * running executions map if `runTaskExecutionWithContext` completes. If it fails but process * does not crash, the running executions map is updated in `runTaskExecutionWithCancelSignal`. * The task execution remains in the running state and retried later on expiration. * * @param taskInternal - The task internal to run. * @param execution - The task execution to run. * @param cancelSignal - The cancel signal. */ private runTaskExecutionWithContext; /** * Get the running task execution ids. * * @returns The running task execution ids. */ getRunningTaskExecutionIds(): ReadonlySet<string>; } type LastElement<T extends ReadonlyArray<unknown>> = T extends readonly [...Array<unknown>, infer L] ? L : never; type ExtractDurableTaskInput<T> = T extends DurableTask<infer I, unknown> ? I : never; type ExtractDurableTaskOutput<T> = T extends DurableTask<unknown, infer O> ? O : never; /** * The type of a sequence of tasks. Disallows empty sequences and sequences with tasks that have * different input and output types. * * @category Task */ export type SequentialDurableTasks<T extends ReadonlyArray<DurableTask<unknown, unknown>>> = T extends readonly [] ? never : SequentialDurableTasksHelper<T>; /** * A helper type to create a sequence of tasks. See {@link SequentialDurableTasks} for more details. * * @category Task */ export type SequentialDurableTasksHelper<T extends ReadonlyArray<DurableTask<unknown, unknown>>> = T extends readonly [] ? T : T extends readonly [DurableTask<infer _I1, infer _O1>] ? T : T extends readonly [ DurableTask<infer I1, infer O1>, DurableTask<infer I2, infer O2>, ...infer Rest ] ? O1 extends I2 ? Rest extends ReadonlyArray<DurableTask<unknown, unknown>> ? readonly [ DurableTask<I1, O1>, ...SequentialDurableTasksHelper<readonly [DurableTask<I2, O2>, ...Rest]> ] : never : never : T; //# sourceMappingURL=index.d.ts.map