UNPKG

durable-execution

Version:

A durable task engine for running tasks durably and resiliently

501 lines (498 loc) 15.8 kB
// src/errors.ts import { CustomError } from "ts-custom-error"; var DurableExecutionError = class extends CustomError { /** * Whether the error is retryable. */ isRetryable; /** * @param message - The error message. * @param isRetryable - Whether the error is retryable. */ constructor(message, isRetryable = true) { super(message); this.isRetryable = isRetryable; } getErrorType() { return "generic"; } }; function convertDurableExecutionErrorToStorageObject(error) { return { errorType: error.getErrorType(), message: error.message, isRetryable: error.isRetryable }; } // src/logger.ts function createConsoleLogger(name) { return { debug: (message) => console.debug(`DEBUG [${name}] ${message}`), info: (message) => console.info(`INFO [${name}] ${message}`), error: (message, error) => console.error(`ERROR [${name}] ${message}`, error) }; } function noop() { } function createLoggerDebugDisabled(logger) { return { debug: noop, info: logger.info, error: logger.error }; } // src/storage.ts function createTransactionMutex() { let locked = false; const waiting = []; const acquire = () => { return new Promise((resolve) => { if (!locked) { locked = true; resolve(); } else { waiting.push(resolve); } }); }; const release = () => { if (waiting.length > 0) { const next = waiting.shift(); next(); } else { locked = false; } }; return { acquire, release }; } async function updateTaskExecutionsWithLimit(tx, where, update, limit) { const ids = await tx.getTaskExecutionIds(where, limit); if (ids.length === 0) { return []; } await tx.updateTaskExecutions( { type: "by_execution_ids", executionIds: ids }, update ); return ids; } function createDurableTaskExecutionStorageObject({ now, rootTask, parentTask, taskId, executionId, retryOptions, sleepMsBeforeRun, timeoutMs, runInput }) { return { rootTask, parentTask, taskId, executionId, retryOptions, sleepMsBeforeRun, timeoutMs, runInput, childrenTasksCompletedCount: 0, status: "ready", isClosed: false, needsPromiseCancellation: false, retryAttempts: 0, startAt: new Date(now.getTime() + sleepMsBeforeRun), createdAt: now, updatedAt: now }; } function convertTaskExecutionStorageObjectToTaskExecution(execution, serializer) { const runInput = serializer.deserialize(execution.runInput); const runOutput = execution.runOutput ? serializer.deserialize(execution.runOutput) : void 0; const output = execution.output ? serializer.deserialize(execution.output) : void 0; const childrenTasks = execution.childrenTasks ? execution.childrenTasks.map((child) => ({ taskId: child.taskId, executionId: child.executionId })) : void 0; const childrenTasksErrors = execution.childrenTasksErrors ? execution.childrenTasksErrors.map((childError) => ({ index: childError.index, taskId: childError.taskId, executionId: childError.executionId, error: childError.error })) : void 0; const finalizeTask = execution.finalizeTask ? { taskId: execution.finalizeTask.taskId, executionId: execution.finalizeTask.executionId } : void 0; const finalizeTaskError = execution.finalizeTaskError; const error = execution.error; switch (execution.status) { case "ready": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, error, status: "ready", retryAttempts: execution.retryAttempts, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "running": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, error, status: "running", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "failed": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, error, status: "failed", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, finishedAt: execution.finishedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "timed_out": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, error, status: "timed_out", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, finishedAt: execution.finishedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "waiting_for_children_tasks": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, runOutput, childrenTasks: childrenTasks ?? [], status: "waiting_for_children_tasks", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "children_tasks_failed": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, runOutput, childrenTasks: childrenTasks ?? [], childrenTasksErrors: childrenTasksErrors ?? [], status: "children_tasks_failed", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, finishedAt: execution.finishedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "waiting_for_finalize_task": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, runOutput, childrenTasks: childrenTasks ?? [], finalizeTask, status: "waiting_for_finalize_task", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "finalize_task_failed": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, runOutput, childrenTasks: childrenTasks ?? [], finalizeTask, finalizeTaskError, status: "finalize_task_failed", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, finishedAt: execution.finishedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "completed": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, runOutput, output, childrenTasks: childrenTasks ?? [], finalizeTask, status: "completed", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, finishedAt: execution.finishedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } case "cancelled": { return { rootTask: execution.rootTask, parentTask: execution.parentTask, taskId: execution.taskId, executionId: execution.executionId, retryOptions: execution.retryOptions, sleepMsBeforeRun: execution.sleepMsBeforeRun, timeoutMs: execution.timeoutMs, runInput, runOutput, childrenTasks, finalizeTask, error, status: "cancelled", retryAttempts: execution.retryAttempts, startedAt: execution.startedAt, finishedAt: execution.finishedAt, expiresAt: execution.expiresAt, createdAt: execution.createdAt, updatedAt: execution.updatedAt }; } default: { throw new DurableExecutionError(`Unknown execution status: ${execution.status}`, false); } } } function getDurableTaskExecutionStorageObjectParentError(execution) { if (execution.error) { return execution.error; } if (execution.finalizeTaskError) { return convertDurableExecutionErrorToStorageObject( new DurableExecutionError( `Finalize task with id ${execution.finalizeTask?.taskId} failed: ${execution.finalizeTaskError.message}`, false ) ); } if (execution.childrenTasksErrors) { return convertDurableExecutionErrorToStorageObject( new DurableExecutionError( `Children task errors: ${execution.childrenTasksErrors.map((e) => ` Child task with id ${e.taskId} failed: ${e.error.message}`).join("\n")}`, false ) ); } return convertDurableExecutionErrorToStorageObject( new DurableExecutionError("Unknown error", false) ); } function createInMemoryStorage({ enableDebug = false } = {}) { return new InMemoryStorage({ enableDebug }); } var InMemoryStorage = class { logger; taskExecutions; transactionMutex; constructor({ enableDebug = false } = {}) { this.logger = createConsoleLogger("InMemoryStorage"); if (!enableDebug) { this.logger = createLoggerDebugDisabled(this.logger); } this.taskExecutions = /* @__PURE__ */ new Map(); this.transactionMutex = createTransactionMutex(); } async withTransaction(fn) { await this.transactionMutex.acquire(); try { const tx = new InMemoryStorageTx(this.logger, this.taskExecutions); const output = await fn(tx); this.taskExecutions = tx.taskExecutions; return output; } finally { this.transactionMutex.release(); } } async save(saveFn) { await saveFn(JSON.stringify(this.taskExecutions, null, 2)); } async load(loadFn) { try { const data = await loadFn(); if (!data.trim()) { this.taskExecutions = /* @__PURE__ */ new Map(); return; } this.taskExecutions = new Map( JSON.parse(data) ); } catch { this.taskExecutions = /* @__PURE__ */ new Map(); } } logAllTaskExecutions() { this.logger.info("------\n\nAll task executions:"); for (const execution of this.taskExecutions.values()) { this.logger.info( `Task execution: ${execution.executionId} JSON: ${JSON.stringify(execution, null, 2)} ` ); } this.logger.info("------"); } }; var InMemoryStorageTx = class { logger; taskExecutions; constructor(logger, executions) { this.logger = logger; this.taskExecutions = /* @__PURE__ */ new Map(); for (const [key, value] of executions) { this.taskExecutions.set(key, { ...value }); } } insertTaskExecutions(executions) { this.logger.debug( `Inserting ${executions.length} task executions: executions=${executions.map((e) => e.executionId).join(", ")}` ); for (const execution of executions) { if (this.taskExecutions.has(execution.executionId)) { throw new Error(`Task execution ${execution.executionId} already exists`); } this.taskExecutions.set(execution.executionId, execution); } } getTaskExecutionIds(where) { const executions = this.getTaskExecutions(where); return executions.map((e) => e.executionId); } getTaskExecutions(where, limit) { let executions = [...this.taskExecutions.values()].filter((execution) => { if (where.type === "by_execution_ids" && where.executionIds.includes(execution.executionId) && (!where.statuses || where.statuses.includes(execution.status)) && (!where.needsPromiseCancellation || execution.needsPromiseCancellation === where.needsPromiseCancellation)) { return true; } if (where.type === "by_statuses" && where.statuses.includes(execution.status) && (where.isClosed == null || execution.isClosed === where.isClosed) && (where.expiresAtLessThan == null || execution.expiresAt && execution.expiresAt < where.expiresAtLessThan)) { return true; } if (where.type === "by_start_at_less_than" && execution.startAt < where.startAtLessThan && (!where.statuses || where.statuses.includes(execution.status))) { return true; } return false; }); if (limit != null && limit >= 0) { executions = executions.slice(0, limit); } this.logger.debug( `Got ${executions.length} task executions: where=${JSON.stringify(where)} limit=${limit} executions=${executions.map((e) => e.executionId).join(", ")}` ); return executions; } updateTaskExecutions(where, update) { const executions = this.getTaskExecutions(where); for (const execution of executions) { for (const key in update) { if (key === "unsetError") { execution.error = void 0; } else if (key === "unsetExpiresAt") { execution.expiresAt = void 0; } else if (key != null) { execution[key] = update[key]; } } } return executions.map((execution) => execution.executionId); } }; export { convertTaskExecutionStorageObjectToTaskExecution, createDurableTaskExecutionStorageObject, createInMemoryStorage, createTransactionMutex, getDurableTaskExecutionStorageObjectParentError, updateTaskExecutionsWithLimit }; //# sourceMappingURL=storage.js.map