UNPKG

durabull

Version:

A durable workflow engine built on top of BullMQ and Redis

192 lines (191 loc) 9.03 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.startWorkflowWorker = void 0; const bullmq_1 = require("bullmq"); const global_1 = require("../config/global"); const storage_1 = require("../runtime/storage"); const ioredis_1 = require("ioredis"); const logger_1 = require("../runtime/logger"); const queues_1 = require("../queues"); const ReplayEngine_1 = require("../runtime/ReplayEngine"); /** * Start the workflow worker */ function startWorkflowWorker(instance) { const durabullInstance = instance || global_1.Durabull.getActive(); if (!durabullInstance) { throw new Error('Durabull instance not initialized. Call new Durabull(config) first or pass instance to startWorkflowWorker.'); } const initialConfig = durabullInstance.getConfig(); const storage = (0, storage_1.getStorage)(); // Initialize queues if not already initialized (0, queues_1.initQueues)(initialConfig.redisUrl, initialConfig.queues.workflow, initialConfig.queues.activity); const logger = (0, logger_1.createLoggerFromConfig)(initialConfig.logger); const connection = new ioredis_1.Redis(initialConfig.redisUrl, { maxRetriesPerRequest: null, }); const worker = new bullmq_1.Worker(initialConfig.queues.workflow, async (job) => { const config = durabullInstance.getConfig(); const { workflowId, workflowName, isResume = false, timerId } = job.data; logger.info(`[WorkflowWorker] Processing workflow ${workflowId} (${workflowName || 'unknown'}) (resume: ${isResume})`); const lockAcquired = await storage.acquireLock(workflowId, 'workflow', 300); if (!lockAcquired) { logger.debug(`[WorkflowWorker] Workflow ${workflowId} is already running, skipping`); return; } if (isResume && timerId) { await storage.appendEvent(workflowId, { type: 'timer-fired', id: timerId, ts: Date.now(), }); } try { let record = await storage.readRecord(workflowId); if (!record) { throw new Error(`Workflow ${workflowId} not found`); } const readHistory = async () => (await storage.readHistory(workflowId)) || { events: [], cursor: 0 }; const history = await readHistory(); if (record.status === 'completed' || record.status === 'failed') { logger.debug(`[WorkflowWorker] Workflow ${workflowId} already ${record.status}`); return; } const resolvedWorkflowName = workflowName || record.class; const WorkflowClass = durabullInstance.resolveWorkflow(resolvedWorkflowName); if (!WorkflowClass) { throw new Error(`Workflow "${resolvedWorkflowName}" not registered`); } const previousWaiting = record.waiting ? { ...record.waiting } : undefined; if (record.status === 'pending' && config.lifecycleHooks?.workflow?.onStart) { try { await config.lifecycleHooks.workflow.onStart(workflowId, resolvedWorkflowName, record.args || []); } catch (hookError) { logger.error('Workflow onStart hook failed', hookError); } } record.status = 'running'; record.waiting = undefined; record.updatedAt = Date.now(); await storage.writeRecord(record); if (previousWaiting) { record.waiting = previousWaiting; } const workflow = new WorkflowClass(); const signals = await storage.listSignals(workflowId); const replayResult = await ReplayEngine_1.ReplayEngine.run({ workflowId, workflow, record, history, signals, isResume, getHistory: readHistory, onStep: async (cursor, updatedHistory) => { await storage.writeHistory(workflowId, updatedHistory); }, }); if (replayResult.signalCursor) { const latestRecord = await storage.readRecord(workflowId); if (latestRecord) { latestRecord.signalCursor = replayResult.signalCursor; latestRecord.updatedAt = Date.now(); await storage.writeRecord(latestRecord); record = latestRecord; } } if (replayResult.status === 'completed') { record = (await storage.readRecord(workflowId)) ?? record; if (!record) { throw new Error(`Workflow ${workflowId} record missing during completion`); } record.status = 'completed'; record.output = replayResult.result; record.waiting = undefined; record.updatedAt = Date.now(); await storage.writeRecord(record); if (config.lifecycleHooks?.workflow?.onComplete) { try { await config.lifecycleHooks.workflow.onComplete(workflowId, resolvedWorkflowName, replayResult.result); } catch (hookError) { logger.error('Workflow onComplete hook failed', hookError); } } logger.info(`[WorkflowWorker] Workflow ${workflowId} completed`); } else if (replayResult.status === 'failed') { const failedRecord = await storage.readRecord(workflowId); if (failedRecord) { failedRecord.status = 'failed'; failedRecord.error = { message: replayResult.error.message, stack: replayResult.error.stack, }; failedRecord.waiting = undefined; failedRecord.updatedAt = Date.now(); await storage.writeRecord(failedRecord); if (config.lifecycleHooks?.workflow?.onFailed) { try { await config.lifecycleHooks.workflow.onFailed(workflowId, resolvedWorkflowName, replayResult.error); } catch (hookError) { logger.error('Workflow onFailed hook failed', hookError); } } } logger.error(`[WorkflowWorker] Workflow ${workflowId} failed`, replayResult.error); throw replayResult.error; } else if (replayResult.status === 'waiting') { const waitingRecord = await storage.readRecord(workflowId); if (waitingRecord) { waitingRecord.status = 'waiting'; waitingRecord.updatedAt = Date.now(); await storage.writeRecord(waitingRecord); } if (config.lifecycleHooks?.workflow?.onWaiting) { try { await config.lifecycleHooks.workflow.onWaiting(workflowId, resolvedWorkflowName); } catch (hookError) { logger.error('Workflow onWaiting hook failed', hookError); } } logger.debug(`[WorkflowWorker] Workflow ${workflowId} waiting`); } else if (replayResult.status === 'continued') { if (config.lifecycleHooks?.workflow?.onContinued) { try { await config.lifecycleHooks.workflow.onContinued(workflowId, resolvedWorkflowName, replayResult.newWorkflowId); } catch (hookError) { logger.error('Workflow onContinued hook failed', hookError); } } logger.info(`[WorkflowWorker] Workflow ${workflowId} continued as new -> ${replayResult.newWorkflowId}`); } } catch (error) { logger.error(`[WorkflowWorker] Unexpected error in workflow ${workflowId}`, error); throw error; } finally { await storage.releaseLock(workflowId, 'workflow'); } }, { connection }); worker.on('completed', (job) => { logger.info(`[WorkflowWorker] Job ${job.id} completed`); }); worker.on('failed', (job, err) => { logger.error(`[WorkflowWorker] Job ${job?.id} failed`, err); }); worker.on('closed', async () => { await connection.quit(); logger.info('[WorkflowWorker] Connection closed'); }); logger.info('[WorkflowWorker] Started'); return worker; } exports.startWorkflowWorker = startWorkflowWorker;