UNPKG

datapilot-cli

Version:

Enterprise-grade streaming multi-format data analysis with comprehensive statistical insights and intelligent relationship detection - supports CSV, JSON, Excel, TSV, Parquet - memory-efficient, cross-platform

377 lines 13.7 kB
"use strict"; /** * Retry logic utilities for DataPilot * Handles transient failures with configurable retry strategies */ Object.defineProperty(exports, "__esModule", { value: true }); exports.RetryUtils = exports.RetryStrategies = exports.RetryManager = void 0; const types_1 = require("../core/types"); const logger_1 = require("./logger"); class RetryManager { static defaultOptions = { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 30000, backoffMultiplier: 2, jitter: true, retryIf: (error) => RetryManager.isRetryableError(error), }; /** * Execute operation with retry logic */ static async retry(operation, options = {}, context) { const opts = { ...this.defaultOptions, ...options }; const startTime = Date.now(); let lastError = new Error('No attempts made'); for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) { try { const result = await operation(); if (attempt > 1) { const totalTime = Date.now() - startTime; logger_1.logger.info(`Operation succeeded on attempt ${attempt} after ${totalTime}ms`, { ...context, operation: 'retry', }); } return result; } catch (error) { lastError = error instanceof Error ? error : new Error(String(error)); // Check if we should retry this error if (!opts.retryIf || !opts.retryIf(error)) { logger_1.logger.debug(`Error not retryable: ${lastError.message}`, context); throw lastError; } // Don't retry on last attempt if (attempt === opts.maxAttempts) { break; } // Calculate delay const delay = this.calculateDelay(attempt, opts); logger_1.logger.warn(`Operation failed on attempt ${attempt}, retrying in ${delay}ms: ${lastError.message}`, { ...context, operation: 'retry', }); // Call onRetry callback if provided if (opts.onRetry) { try { opts.onRetry(error, attempt); } catch (callbackError) { logger_1.logger.debug('Error in retry callback', context, callbackError); } } // Wait before retry await this.delay(delay); } } const totalTime = Date.now() - startTime; const finalError = this.createRetryError(lastError, opts.maxAttempts, totalTime, context); logger_1.logger.error('Retry attempts exhausted', { ...context, operation: 'retry', }); throw finalError; } /** * Execute operation with retry and return detailed result */ static async retryWithResult(operation, options = {}, context) { const startTime = Date.now(); let attempts = 0; try { const result = await this.retry(operation, { ...options, onRetry: (error, attempt) => { attempts = attempt; if (options.onRetry) { options.onRetry(error, attempt); } }, }, context); return { success: true, result, attempts: Math.max(attempts, 1), totalTime: Date.now() - startTime, }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(String(error)), attempts: attempts || (options.maxAttempts ?? this.defaultOptions.maxAttempts), totalTime: Date.now() - startTime, }; } } /** * Batch retry multiple operations */ static async retryBatch(operations, options = {}, context) { const results = await Promise.allSettled(operations.map((op, index) => this.retryWithResult(op, options, { ...context, operation: `batch_${index}`, }))); return results.map((result) => result.status === 'fulfilled' ? result.value : { success: false, error: result.reason instanceof Error ? result.reason : new Error(String(result.reason)), attempts: options.maxAttempts ?? this.defaultOptions.maxAttempts, totalTime: 0, }); } /** * Check if an error is retryable */ static isRetryableError(error) { if (error instanceof types_1.DataPilotError) { // Don't retry validation errors or critical analysis errors if (error.category === types_1.ErrorCategory.VALIDATION) { return false; } // Don't retry if explicitly marked as non-recoverable if (!error.recoverable) { return false; } // Retry network, IO, and memory errors (with caution) const retryableCategories = [types_1.ErrorCategory.NETWORK, types_1.ErrorCategory.IO]; return retryableCategories.includes(error.category); } if (error instanceof Error) { const message = error.message.toLowerCase(); // Network-related errors if (message.includes('timeout') || message.includes('connection') || message.includes('network') || message.includes('enotfound') || message.includes('econnreset')) { return true; } // File system errors that might be transient if (message.includes('ebusy') || message.includes('eagain') || message.includes('emfile')) { return true; } // Memory errors are generally not retryable if (message.includes('out of memory') || message.includes('heap') || message.includes('memory')) { return false; } } return false; } /** * Create retry-specific error with context */ static createRetryError(originalError, attempts, totalTime, context) { return types_1.DataPilotError.analysis(`Operation failed after ${attempts} retry attempts over ${totalTime}ms: ${originalError.message}`, 'RETRY_EXHAUSTED', { ...context, retryCount: attempts, timeElapsed: totalTime, }, [ { action: 'Check underlying issue', description: 'Investigate the root cause of the recurring failure', severity: types_1.ErrorSeverity.HIGH, }, { action: 'Increase retry limits', description: 'Consider increasing maxAttempts or delay if issue is transient', severity: types_1.ErrorSeverity.MEDIUM, }, { action: 'Check system resources', description: 'Verify sufficient memory, disk space, and network connectivity', severity: types_1.ErrorSeverity.MEDIUM, }, ]); } /** * Calculate exponential backoff delay with jitter */ static calculateDelay(attempt, options) { const exponentialDelay = options.baseDelayMs * Math.pow(options.backoffMultiplier, attempt - 1); const cappedDelay = Math.min(exponentialDelay, options.maxDelayMs); if (!options.jitter) { return cappedDelay; } // Add jitter (±25% of delay) const jitterRange = cappedDelay * 0.25; const jitter = (Math.random() - 0.5) * 2 * jitterRange; return Math.max(0, cappedDelay + jitter); } /** * Promise-based delay */ static delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } } exports.RetryManager = RetryManager; /** * Specialized retry strategies for common operations */ class RetryStrategies { /** * File operations retry strategy */ static fileOperations() { return { maxAttempts: 3, baseDelayMs: 500, maxDelayMs: 5000, backoffMultiplier: 1.5, jitter: true, retryIf: (error) => { if (error instanceof Error) { const message = error.message.toLowerCase(); return (message.includes('ebusy') || message.includes('eagain') || message.includes('emfile') || message.includes('enoent')); // Sometimes files become temporarily unavailable } return false; }, }; } /** * Network operations retry strategy */ static networkOperations() { return { maxAttempts: 5, baseDelayMs: 1000, maxDelayMs: 30000, backoffMultiplier: 2, jitter: true, retryIf: (error) => { if (error instanceof Error) { const message = error.message.toLowerCase(); return (message.includes('timeout') || message.includes('connection') || message.includes('network') || message.includes('enotfound') || message.includes('econnreset') || message.includes('502') || message.includes('503') || message.includes('504')); } return false; }, }; } /** * Analysis operations retry strategy */ static analysisOperations() { return { maxAttempts: 2, baseDelayMs: 2000, maxDelayMs: 10000, backoffMultiplier: 1.5, jitter: false, retryIf: (error) => { if (error instanceof types_1.DataPilotError) { // Only retry analysis errors that are marked as recoverable return (error.recoverable && error.category !== types_1.ErrorCategory.VALIDATION && error.severity !== types_1.ErrorSeverity.CRITICAL); } return false; }, }; } /** * Memory-sensitive operations retry strategy */ static memorySensitiveOperations() { return { maxAttempts: 2, baseDelayMs: 5000, // Longer delay to allow memory to free up maxDelayMs: 15000, backoffMultiplier: 1.5, jitter: false, retryIf: (error) => { // Don't retry memory errors - they usually require intervention if (error instanceof types_1.DataPilotError && error.category === types_1.ErrorCategory.MEMORY) { return false; } if (error instanceof Error) { const message = error.message.toLowerCase(); if (message.includes('memory') || message.includes('heap')) { return false; } } return RetryManager.isRetryableError(error); }, onRetry: () => { // Force garbage collection if available before retry if (global.gc) { global.gc(); } }, }; } /** * Quick operations retry strategy (for fast, simple operations) */ static quickOperations() { return { maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 1000, backoffMultiplier: 2, jitter: true, }; } } exports.RetryStrategies = RetryStrategies; /** * Utility class for common retry patterns */ class RetryUtils { /** * Retry file read operation */ static async retryFileRead(operation, context) { return RetryManager.retry(operation, RetryStrategies.fileOperations(), { ...context, operation: 'fileRead', }); } /** * Retry network operation */ static async retryNetworkOperation(operation, context) { return RetryManager.retry(operation, RetryStrategies.networkOperations(), { ...context, operation: 'network', }); } /** * Retry analysis operation with error handling */ static async retryAnalysis(operation, context) { return RetryManager.retry(operation, RetryStrategies.analysisOperations(), { ...context, operation: 'analysis', }); } /** * Wrap operation with automatic retry based on error type */ static async autoRetry(operation, context) { return RetryManager.retry(operation, { maxAttempts: 3, baseDelayMs: 1000, maxDelayMs: 10000, backoffMultiplier: 1.5, jitter: true, retryIf: (error) => RetryManager.isRetryableError(error), }, { ...context, operation: 'autoRetry' }); } } exports.RetryUtils = RetryUtils; //# sourceMappingURL=retry.js.map