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
JavaScript
;
/**
* 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