durable-execution
Version:
A durable task engine for running tasks durably and resiliently
501 lines (498 loc) • 15.8 kB
JavaScript
// 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