durable-execution
Version:
A durable task engine for running tasks durably and resiliently
346 lines • 16.8 kB
TypeScript
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