diff --git a/dashcaddy-api/src/recipes/bundled-workflows.js b/dashcaddy-api/src/recipes/bundled-workflows.js index ec8706a..0fbe369 100644 --- a/dashcaddy-api/src/recipes/bundled-workflows.js +++ b/dashcaddy-api/src/recipes/bundled-workflows.js @@ -252,32 +252,49 @@ class WorkflowEngine extends EventEmitter { */ async _runActions(actions, triggerData = {}) { const results = []; + const MAX_RETRIES = 3; + const RETRY_DELAY_MS = 2000; for (let i = 0; i < actions.length; i++) { const action = actions[i]; const previousResult = i > 0 ? results[i - 1] : null; - // notify-on-failure needs to see the previous action's outcome to decide - // whether to fire. Passing the full results array in the trigger data lets - // executeAction do that lookup without changing the action shape. - // Also surface failingServices (set by healthCheckService on throw) so - // template variables like {{failingServices}} can interpolate. const actionContext = { ...triggerData, previousResult, failingServices: previousResult && previousResult.failingServices ? previousResult.failingServices : undefined, }; - try { - const result = await this.executeAction(action, actionContext); + + // DC-093: Retry with exponential backoff for transient failures + let lastError = null; + let result = null; + let succeeded = false; + + for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) { + try { + result = await this.executeAction(action, actionContext); + succeeded = true; + break; + } catch (error) { + lastError = error; + if (attempt < MAX_RETRIES) { + const delay = RETRY_DELAY_MS * Math.pow(2, attempt); + log.warn('workflow', `Action "${action.type}" failed (attempt ${attempt + 1}/${MAX_RETRIES + 1}), retrying in ${delay}ms`, { error: error.message }); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + } + + if (succeeded) { results.push({ action: action.type, success: true, result }); - } catch (error) { - log.error('workflow', error, { actionType: action.type }); + } else { + log.error('workflow', `Action "${action.type}" failed after ${MAX_RETRIES + 1} attempts`, { error: lastError.message }); results.push({ action: action.type, success: false, - error: error.message, - failingServices: error.failingServices, + error: lastError.message, + failingServices: lastError.failingServices, + exhaustedRetries: MAX_RETRIES + 1, }); - // Continue with other actions but log failure } }