@podx/core
Version:
🚀 Core utilities and shared functionality for PODx - Advanced Twitter/X scraping and crypto analysis toolkit
353 lines (304 loc) • 10.9 kB
text/typescript
/**
* Enhanced error recovery strategies and automated problem resolution
*/
import { logger } from '../logger/index.js';
import type { LogContext } from '../logger/index.js';
import type { CategorizedError, ErrorCategory } from '../types/common.js';
import { errorHandling } from '../error-handling/index.js';
// Recovery strategy interface
export interface RecoveryStrategy<T = unknown> {
readonly name: string;
readonly canRecover: (error: CategorizedError) => boolean;
readonly recover: (error: CategorizedError, context?: LogContext) => Promise<errorHandling.Result<T, CategorizedError>>;
readonly priority: number; // Higher number = higher priority
readonly timeout: number; // Maximum time to spend on recovery
}
// Recovery context for strategies
export interface RecoveryContext extends LogContext {
readonly attempt: number;
readonly maxAttempts: number;
readonly previousErrors: CategorizedError[];
readonly recoveryHistory: RecoveryAttempt[];
}
export interface RecoveryAttempt {
readonly strategy: string;
readonly success: boolean;
readonly duration: number;
readonly timestamp: number;
readonly error?: string;
}
// Enhanced recovery manager
export class EnhancedRecoveryManager {
private static readonly strategies = new Map<string, RecoveryStrategy>();
private static readonly recoveryHistory: RecoveryAttempt[] = [];
private static readonly maxHistorySize = 1000;
/**
* Register a recovery strategy
*/
static registerStrategy<T>(strategy: RecoveryStrategy<T>): void {
this.strategies.set(strategy.name, strategy);
logger.debug('Recovery strategy registered', {
strategyName: strategy.name,
priority: strategy.priority,
timeout: strategy.timeout
});
}
/**
* Attempt to recover from an error with multiple strategies
*/
static async attemptRecovery<T>(
error: CategorizedError,
context?: LogContext
): Promise<errorHandling.Result<T, CategorizedError>> {
const applicableStrategies = this.getApplicableStrategies(error);
if (applicableStrategies.length === 0) {
logger.debug('No recovery strategies available', {
errorCategory: error.category,
errorMessage: error.message,
...context
});
return errorHandling.result.err(error);
}
const recoveryContext: RecoveryContext = {
...context,
attempt: 1,
maxAttempts: applicableStrategies.length,
previousErrors: [error],
recoveryHistory: []
};
logger.info('Attempting error recovery', {
errorCategory: error.category,
applicableStrategies: applicableStrategies.map(s => s.name),
...context
});
// Try strategies in priority order
for (const strategy of applicableStrategies) {
const startTime = performance.now();
try {
const result = await Promise.race([
strategy.recover(error, recoveryContext),
new Promise<never>((_, reject) =>
setTimeout(() => reject(new Error('Recovery timeout')), strategy.timeout)
)
]);
const duration = performance.now() - startTime;
const attempt: RecoveryAttempt = {
strategy: strategy.name,
success: result.success,
duration,
timestamp: Date.now()
};
this.addRecoveryAttempt(attempt);
if (result.success) {
logger.info('Error recovery successful', {
strategyName: strategy.name,
duration,
errorCategory: error.category,
...context
});
return result;
} else {
logger.warn('Recovery strategy failed', {
strategyName: strategy.name,
duration,
error: result.error.message,
...context
});
}
} catch (recoveryError) {
const duration = performance.now() - startTime;
const attempt: RecoveryAttempt = {
strategy: strategy.name,
success: false,
duration,
timestamp: Date.now(),
error: (recoveryError as Error).message
};
this.addRecoveryAttempt(attempt);
logger.warn('Recovery strategy error', {
strategyName: strategy.name,
duration,
error: (recoveryError as Error).message,
...context
});
}
}
logger.error('All recovery strategies failed', {
errorCategory: error.category,
attemptedStrategies: applicableStrategies.map(s => s.name),
...context
});
return errorHandling.result.err(error);
}
/**
* Get applicable strategies for an error, sorted by priority
*/
private static getApplicableStrategies(error: CategorizedError): RecoveryStrategy[] {
return Array.from(this.strategies.values())
.filter(strategy => strategy.canRecover(error))
.sort((a, b) => b.priority - a.priority);
}
/**
* Add recovery attempt to history
*/
private static addRecoveryAttempt(attempt: RecoveryAttempt): void {
this.recoveryHistory.push(attempt);
// Keep only recent attempts
if (this.recoveryHistory.length > this.maxHistorySize) {
this.recoveryHistory.shift();
}
}
/**
* Get recovery statistics
*/
static getRecoveryStats(): {
totalAttempts: number;
successRate: number;
averageDuration: number;
strategyStats: Record<string, {
attempts: number;
successes: number;
averageDuration: number;
}>;
} {
const totalAttempts = this.recoveryHistory.length;
const successfulAttempts = this.recoveryHistory.filter(a => a.success).length;
const successRate = totalAttempts > 0 ? successfulAttempts / totalAttempts : 0;
const totalDuration = this.recoveryHistory.reduce((sum, a) => sum + a.duration, 0);
const averageDuration = totalAttempts > 0 ? totalDuration / totalAttempts : 0;
// Calculate strategy statistics
const strategyStats: Record<string, {
attempts: number;
successes: number;
averageDuration: number;
}> = {};
for (const attempt of this.recoveryHistory) {
if (!strategyStats[attempt.strategy]) {
strategyStats[attempt.strategy] = {
attempts: 0,
successes: 0,
averageDuration: 0
};
}
const stats = strategyStats[attempt.strategy];
stats.attempts++;
if (attempt.success) {
stats.successes++;
}
stats.averageDuration = (stats.averageDuration * (stats.attempts - 1) + attempt.duration) / stats.attempts;
}
return {
totalAttempts,
successRate,
averageDuration,
strategyStats
};
}
/**
* Clear recovery history
*/
static clearHistory(): void {
this.recoveryHistory.length = 0;
}
}
// Built-in recovery strategies
export const createNetworkRecoveryStrategy = (): RecoveryStrategy => ({
name: 'network-retry',
priority: 10,
timeout: 5000,
canRecover: (error) => error.category === 'NETWORK' && error.retryable,
recover: async (error, context) => {
logger.info('Attempting network retry recovery', { ...context });
// Wait for a short period before retrying
await new Promise(resolve => setTimeout(resolve, 1000));
// In a real implementation, you would retry the original operation
// For now, we'll simulate a successful recovery
return errorHandling.result.ok('network-recovered');
}
});
export const createRateLimitRecoveryStrategy = (): RecoveryStrategy => ({
name: 'rate-limit-wait',
priority: 20,
timeout: 30000,
canRecover: (error) => error.category === 'RATE_LIMIT' && error.retryAfter !== undefined,
recover: async (error, context) => {
if (!error.retryAfter) {
return errorHandling.result.err(error);
}
logger.info('Waiting for rate limit reset', {
retryAfter: error.retryAfter,
...context
});
await new Promise(resolve => setTimeout(resolve, error.retryAfter!));
return errorHandling.result.ok('rate-limit-reset');
}
});
export const createDatabaseRecoveryStrategy = (): RecoveryStrategy => ({
name: 'database-reconnect',
priority: 15,
timeout: 10000,
canRecover: (error) => error.category === 'DATABASE' && error.retryable,
recover: async (error, context) => {
logger.info('Attempting database reconnection', { ...context });
// Simulate database reconnection
await new Promise(resolve => setTimeout(resolve, 2000));
// In a real implementation, you would attempt to reconnect to the database
return errorHandling.result.ok('database-reconnected');
}
});
export const createConfigurationRecoveryStrategy = (): RecoveryStrategy => ({
name: 'configuration-reload',
priority: 5,
timeout: 5000,
canRecover: (error) => error.category === 'CONFIGURATION',
recover: async (error, context) => {
logger.info('Attempting configuration reload', { ...context });
// Simulate configuration reload
await new Promise(resolve => setTimeout(resolve, 1000));
// In a real implementation, you would reload configuration
return errorHandling.result.ok('configuration-reloaded');
}
});
export const createFallbackRecoveryStrategy = (): RecoveryStrategy => ({
name: 'fallback-operation',
priority: 1,
timeout: 15000,
canRecover: (error) => error.category === 'EXTERNAL_SERVICE' || error.category === 'BUSINESS_LOGIC',
recover: async (error, context) => {
logger.info('Attempting fallback operation', { ...context });
// Simulate fallback operation
await new Promise(resolve => setTimeout(resolve, 3000));
// In a real implementation, you would perform a fallback operation
return errorHandling.result.ok('fallback-completed');
}
});
// Initialize built-in recovery strategies
export const initializeRecoveryStrategies = (): void => {
EnhancedRecoveryManager.registerStrategy(createNetworkRecoveryStrategy());
EnhancedRecoveryManager.registerStrategy(createRateLimitRecoveryStrategy());
EnhancedRecoveryManager.registerStrategy(createDatabaseRecoveryStrategy());
EnhancedRecoveryManager.registerStrategy(createConfigurationRecoveryStrategy());
EnhancedRecoveryManager.registerStrategy(createFallbackRecoveryStrategy());
logger.info('Recovery strategies initialized', {
strategyCount: 5,
strategies: [
'network-retry',
'rate-limit-wait',
'database-reconnect',
'configuration-reload',
'fallback-operation'
]
});
};
// Export recovery utilities
export const recovery = {
manager: EnhancedRecoveryManager,
strategies: {
network: createNetworkRecoveryStrategy,
rateLimit: createRateLimitRecoveryStrategy,
database: createDatabaseRecoveryStrategy,
configuration: createConfigurationRecoveryStrategy,
fallback: createFallbackRecoveryStrategy
},
initialize: initializeRecoveryStrategies
};