diff --git a/examples/phase3-2-advanced-error-handling.ts b/examples/phase3-2-advanced-error-handling.ts new file mode 100644 index 0000000..e723ed3 --- /dev/null +++ b/examples/phase3-2-advanced-error-handling.ts @@ -0,0 +1,423 @@ +/** + * Phase 3.2 Advanced Error Handling Usage Example + * + * Demonstrates comprehensive error handling with categorization, retry logic, and telemetry + * Shows error classification, exponential backoff, and debugging information + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import { DailyNotification } from '@timesafari/daily-notification-plugin'; + +/** + * Example: Configure advanced error handling + */ +async function configureAdvancedErrorHandling() { + try { + console.log('Configuring advanced error handling...'); + + // Configure with error handling + await DailyNotification.configure({ + storage: 'shared', + ttlSeconds: 1800, // 30 minutes TTL + prefetchLeadMinutes: 15, + enableErrorHandling: true, + maxRetries: 3, + baseRetryDelay: 1000, // 1 second + maxRetryDelay: 30000, // 30 seconds + backoffMultiplier: 2.0 + }); + + console.log('✅ Advanced error handling configured'); + + // The plugin will now: + // - Categorize errors by type, code, and severity + // - Implement exponential backoff retry logic + // - Track error metrics and telemetry + // - Provide comprehensive debugging information + // - Manage retry state and limits + + } catch (error) { + console.error('❌ Advanced error handling configuration failed:', error); + } +} + +/** + * Example: Demonstrate error categorization + */ +async function demonstrateErrorCategorization() { + try { + console.log('Demonstrating error categorization...'); + + // Configure error handling + await configureAdvancedErrorHandling(); + + // Simulate different types of errors + const errorScenarios = [ + { + name: 'Network Error', + url: 'https://unreachable-api.example.com/content', + expectedCategory: 'NETWORK', + expectedSeverity: 'MEDIUM' + }, + { + name: 'Permission Error', + url: 'https://api.example.com/content', + expectedCategory: 'PERMISSION', + expectedSeverity: 'MEDIUM' + }, + { + name: 'Configuration Error', + url: 'invalid-url', + expectedCategory: 'CONFIGURATION', + expectedSeverity: 'LOW' + } + ]; + + for (const scenario of errorScenarios) { + try { + console.log(`📡 Testing ${scenario.name}...`); + + await DailyNotification.scheduleDailyNotification({ + url: scenario.url, + time: '09:00', + title: 'Daily Update', + body: 'Your daily notification is ready' + }); + + } catch (error) { + console.log(`✅ ${scenario.name} handled:`, error.message); + // The error handler will: + // - Categorize the error by type + // - Assign appropriate severity level + // - Generate unique error codes + // - Track metrics for analysis + } + } + + } catch (error) { + console.error('❌ Error categorization demonstration failed:', error); + } +} + +/** + * Example: Demonstrate retry logic with exponential backoff + */ +async function demonstrateRetryLogic() { + try { + console.log('Demonstrating retry logic with exponential backoff...'); + + // Configure error handling + await configureAdvancedErrorHandling(); + + // Schedule notification with unreliable endpoint + console.log('📡 Scheduling notification with unreliable endpoint...'); + + await DailyNotification.scheduleDailyNotification({ + url: 'https://unreliable-api.example.com/content', + time: '09:00', + title: 'Daily Update', + body: 'Your daily notification is ready' + }); + + console.log('✅ Notification scheduled with retry logic'); + + // The plugin will: + // - Attempt the request + // - If it fails, categorize the error + // - If retryable, wait with exponential backoff + // - Retry up to maxRetries times + // - Track retry attempts and delays + + // Check retry statistics + const retryStats = await DailyNotification.getRetryStatistics(); + console.log('📊 Retry Statistics:', retryStats); + + } catch (error) { + console.error('❌ Retry logic demonstration failed:', error); + } +} + +/** + * Example: Check error metrics and telemetry + */ +async function checkErrorMetricsAndTelemetry() { + try { + console.log('Checking error metrics and telemetry...'); + + // Configure error handling + await configureAdvancedErrorHandling(); + + // Generate some errors to create metrics + await demonstrateErrorCategorization(); + + // Get error metrics + const errorMetrics = await DailyNotification.getErrorMetrics(); + + console.log('📊 Error Metrics:'); + console.log(` Total Errors: ${errorMetrics.totalErrors}`); + console.log(` Network Errors: ${errorMetrics.networkErrors}`); + console.log(` Storage Errors: ${errorMetrics.storageErrors}`); + console.log(` Scheduling Errors: ${errorMetrics.schedulingErrors}`); + console.log(` Permission Errors: ${errorMetrics.permissionErrors}`); + console.log(` Configuration Errors: ${errorMetrics.configurationErrors}`); + console.log(` System Errors: ${errorMetrics.systemErrors}`); + console.log(` Unknown Errors: ${errorMetrics.unknownErrors}`); + + // Get retry statistics + const retryStats = await DailyNotification.getRetryStatistics(); + console.log('🔄 Retry Statistics:'); + console.log(` Total Operations: ${retryStats.totalOperations}`); + console.log(` Active Retries: ${retryStats.activeRetries}`); + console.log(` Total Retries: ${retryStats.totalRetries}`); + + // Analyze error patterns + if (errorMetrics.networkErrors > 0) { + console.log('⚠️ Network errors detected - check connectivity'); + } + + if (errorMetrics.permissionErrors > 0) { + console.log('⚠️ Permission errors detected - check app permissions'); + } + + if (retryStats.totalRetries > retryStats.totalOperations * 2) { + console.log('⚠️ High retry rate - system may be unstable'); + } + + } catch (error) { + console.error('❌ Error metrics check failed:', error); + } +} + +/** + * Example: Handle custom retry configurations + */ +async function handleCustomRetryConfigurations() { + try { + console.log('Handling custom retry configurations...'); + + // Configure error handling + await configureAdvancedErrorHandling(); + + // Schedule notification with custom retry config + console.log('📡 Scheduling with custom retry configuration...'); + + await DailyNotification.scheduleDailyNotification({ + url: 'https://api.example.com/content', + time: '09:00', + title: 'Daily Update', + body: 'Your daily notification is ready', + retryConfig: { + maxRetries: 5, + baseRetryDelay: 2000, // 2 seconds + maxRetryDelay: 60000, // 60 seconds + backoffMultiplier: 1.5 + } + }); + + console.log('✅ Notification scheduled with custom retry config'); + + // The plugin will: + // - Use custom retry limits (5 instead of 3) + // - Use custom base delay (2s instead of 1s) + // - Use custom max delay (60s instead of 30s) + // - Use custom backoff multiplier (1.5 instead of 2.0) + + } catch (error) { + console.error('❌ Custom retry configuration failed:', error); + } +} + +/** + * Example: Monitor error patterns over time + */ +async function monitorErrorPatternsOverTime() { + try { + console.log('Monitoring error patterns over time...'); + + // Configure error handling + await configureAdvancedErrorHandling(); + + // Monitor errors over multiple operations + const monitoringInterval = setInterval(async () => { + try { + const errorMetrics = await DailyNotification.getErrorMetrics(); + const retryStats = await DailyNotification.getRetryStatistics(); + + console.log('📊 Error Pattern Snapshot:'); + console.log(` Total Errors: ${errorMetrics.totalErrors}`); + console.log(` Network Errors: ${errorMetrics.networkErrors}`); + console.log(` Active Retries: ${retryStats.activeRetries}`); + console.log(` Total Retries: ${retryStats.totalRetries}`); + + // Stop monitoring if we have enough data + if (errorMetrics.totalErrors >= 10) { + clearInterval(monitoringInterval); + console.log('✅ Error pattern monitoring completed'); + } + + } catch (error) { + console.error('❌ Error pattern monitoring error:', error); + } + }, 5000); // Check every 5 seconds + + // Make some requests to generate data + for (let i = 0; i < 5; i++) { + try { + await DailyNotification.scheduleDailyNotification({ + url: 'https://api.example.com/content', + time: `09:${i.toString().padStart(2, '0')}`, + title: 'Daily Update', + body: 'Your daily notification is ready' + }); + } catch (error) { + // Errors will be handled by the error handler + console.log(`Request ${i + 1} failed:`, error.message); + } + + // Wait between requests + await new Promise(resolve => setTimeout(resolve, 2000)); + } + + } catch (error) { + console.error('❌ Error pattern monitoring failed:', error); + } +} + +/** + * Example: Reset error metrics and retry states + */ +async function resetErrorMetricsAndRetryStates() { + try { + console.log('Resetting error metrics and retry states...'); + + // Configure error handling + await configureAdvancedErrorHandling(); + + // Get current metrics + const beforeMetrics = await DailyNotification.getErrorMetrics(); + const beforeRetryStats = await DailyNotification.getRetryStatistics(); + + console.log('📊 Before Reset:'); + console.log(` Total Errors: ${beforeMetrics.totalErrors}`); + console.log(` Active Retries: ${beforeRetryStats.activeRetries}`); + + // Reset metrics + await DailyNotification.resetErrorMetrics(); + console.log('✅ Error metrics reset'); + + // Clear retry states + await DailyNotification.clearRetryStates(); + console.log('✅ Retry states cleared'); + + // Get metrics after reset + const afterMetrics = await DailyNotification.getErrorMetrics(); + const afterRetryStats = await DailyNotification.getRetryStatistics(); + + console.log('📊 After Reset:'); + console.log(` Total Errors: ${afterMetrics.totalErrors}`); + console.log(` Active Retries: ${afterRetryStats.activeRetries}`); + + } catch (error) { + console.error('❌ Error metrics reset failed:', error); + } +} + +/** + * Example: Debug error handling information + */ +async function debugErrorHandlingInformation() { + try { + console.log('Debugging error handling information...'); + + // Configure error handling + await configureAdvancedErrorHandling(); + + // Get debugging information + const debugInfo = await DailyNotification.getErrorDebugInfo(); + + console.log('🐛 Error Debug Information:'); + console.log(` Error Handler Status: ${debugInfo.handlerStatus}`); + console.log(` Configuration: ${JSON.stringify(debugInfo.configuration)}`); + console.log(` Recent Errors: ${debugInfo.recentErrors.length}`); + console.log(` Retry States: ${debugInfo.retryStates.length}`); + + // Display recent errors + if (debugInfo.recentErrors.length > 0) { + console.log('📋 Recent Errors:'); + debugInfo.recentErrors.forEach((error, index) => { + console.log(` ${index + 1}. ${error.category} - ${error.severity} - ${error.errorCode}`); + }); + } + + // Display retry states + if (debugInfo.retryStates.length > 0) { + console.log('🔄 Retry States:'); + debugInfo.retryStates.forEach((state, index) => { + console.log(` ${index + 1}. Operation: ${state.operationId} - Attempts: ${state.attemptCount}`); + }); + } + + } catch (error) { + console.error('❌ Error debugging failed:', error); + } +} + +/** + * Example: Optimize error handling for production + */ +async function optimizeErrorHandlingForProduction() { + try { + console.log('Optimizing error handling for production...'); + + // Configure production-optimized error handling + await DailyNotification.configure({ + storage: 'shared', + ttlSeconds: 1800, + prefetchLeadMinutes: 15, + enableErrorHandling: true, + maxRetries: 3, + baseRetryDelay: 1000, + maxRetryDelay: 30000, + backoffMultiplier: 2.0, + enableErrorTelemetry: true, + errorReportingEndpoint: 'https://api.example.com/errors' + }); + + console.log('✅ Production error handling configured'); + + // The plugin will now: + // - Use production-optimized retry settings + // - Enable error telemetry and reporting + // - Send error data to monitoring endpoint + // - Provide comprehensive debugging information + // - Handle errors gracefully without user impact + + // Schedule notification with production error handling + await DailyNotification.scheduleDailyNotification({ + url: 'https://api.example.com/daily-content', + time: '09:00', + title: 'Daily Update', + body: 'Your daily notification is ready' + }); + + console.log('✅ Notification scheduled with production error handling'); + + } catch (error) { + console.error('❌ Production error handling optimization failed:', error); + } +} + +// Export examples for use +export { + configureAdvancedErrorHandling, + demonstrateErrorCategorization, + demonstrateRetryLogic, + checkErrorMetricsAndTelemetry, + handleCustomRetryConfigurations, + monitorErrorPatternsOverTime, + resetErrorMetricsAndRetryStates, + debugErrorHandlingInformation, + optimizeErrorHandlingForProduction +}; diff --git a/ios/Plugin/DailyNotificationErrorHandler.swift b/ios/Plugin/DailyNotificationErrorHandler.swift new file mode 100644 index 0000000..018d8b2 --- /dev/null +++ b/ios/Plugin/DailyNotificationErrorHandler.swift @@ -0,0 +1,650 @@ +/** + * DailyNotificationErrorHandler.swift + * + * iOS Error Handler for comprehensive error management + * Implements error categorization, retry logic, and telemetry + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +import Foundation + +/** + * Manages comprehensive error handling with categorization, retry logic, and telemetry + * + * This class implements the critical error handling functionality: + * - Categorizes errors by type, code, and severity + * - Implements exponential backoff retry logic + * - Tracks error metrics and telemetry + * - Provides debugging information + * - Manages retry state and limits + */ +class DailyNotificationErrorHandler { + + // MARK: - Constants + + private static let TAG = "DailyNotificationErrorHandler" + + // Retry configuration + private static let DEFAULT_MAX_RETRIES = 3 + private static let DEFAULT_BASE_DELAY_SECONDS: TimeInterval = 1.0 + private static let DEFAULT_MAX_DELAY_SECONDS: TimeInterval = 30.0 + private static let DEFAULT_BACKOFF_MULTIPLIER: Double = 2.0 + + // Error severity levels + enum ErrorSeverity { + case low // Minor issues, non-critical + case medium // Moderate issues, may affect functionality + case high // Serious issues, significant impact + case critical // Critical issues, system failure + } + + // Error categories + enum ErrorCategory { + case network // Network-related errors + case storage // Storage/database errors + case scheduling // Notification scheduling errors + case permission // Permission-related errors + case configuration // Configuration errors + case system // System-level errors + case unknown // Unknown/unclassified errors + } + + // MARK: - Properties + + private let logger: DailyNotificationLogger + private var retryStates: [String: RetryState] = [:] + private let retryQueue = DispatchQueue(label: "error.retry", attributes: .concurrent) + private let metrics = ErrorMetrics() + private let config: ErrorConfiguration + + // MARK: - Initialization + + /** + * Constructor with default configuration + */ + init(logger: DailyNotificationLogger) { + self.logger = logger + self.config = ErrorConfiguration() + + logger.debug(DailyNotificationErrorHandler.TAG, "ErrorHandler initialized with max retries: \(config.maxRetries)") + } + + /** + * Constructor with custom configuration + * + * @param logger Logger instance for debugging + * @param config Error handling configuration + */ + init(logger: DailyNotificationLogger, config: ErrorConfiguration) { + self.logger = logger + self.config = config + + logger.debug(DailyNotificationErrorHandler.TAG, "ErrorHandler initialized with max retries: \(config.maxRetries)") + } + + // MARK: - Error Handling + + /** + * Handle error with automatic retry logic + * + * @param operationId Unique identifier for the operation + * @param error Error to handle + * @param retryable Whether this error is retryable + * @return ErrorResult with handling information + */ + func handleError(operationId: String, error: Error, retryable: Bool) -> ErrorResult { + do { + logger.debug(DailyNotificationErrorHandler.TAG, "Handling error for operation: \(operationId)") + + // Categorize error + let errorInfo = categorizeError(error) + + // Update metrics + metrics.recordError(errorInfo) + + // Check if retryable and within limits + if retryable && shouldRetry(operationId: operationId, errorInfo: errorInfo) { + return handleRetryableError(operationId: operationId, errorInfo: errorInfo) + } else { + return handleNonRetryableError(operationId: operationId, errorInfo: errorInfo) + } + + } catch { + logger.error(DailyNotificationErrorHandler.TAG, "Error in error handler: \(error)") + return ErrorResult.fatal(message: "Error handler failure: \(error.localizedDescription)") + } + } + + /** + * Handle error with custom retry configuration + * + * @param operationId Unique identifier for the operation + * @param error Error to handle + * @param retryConfig Custom retry configuration + * @return ErrorResult with handling information + */ + func handleError(operationId: String, error: Error, retryConfig: RetryConfiguration) -> ErrorResult { + do { + logger.debug(DailyNotificationErrorHandler.TAG, "Handling error with custom retry config for operation: \(operationId)") + + // Categorize error + let errorInfo = categorizeError(error) + + // Update metrics + metrics.recordError(errorInfo) + + // Check if retryable with custom config + if shouldRetry(operationId: operationId, errorInfo: errorInfo, retryConfig: retryConfig) { + return handleRetryableError(operationId: operationId, errorInfo: errorInfo, retryConfig: retryConfig) + } else { + return handleNonRetryableError(operationId: operationId, errorInfo: errorInfo) + } + + } catch { + logger.error(DailyNotificationErrorHandler.TAG, "Error in error handler with custom config: \(error)") + return ErrorResult.fatal(message: "Error handler failure: \(error.localizedDescription)") + } + } + + // MARK: - Error Categorization + + /** + * Categorize error by type, code, and severity + * + * @param error Error to categorize + * @return ErrorInfo with categorization + */ + private func categorizeError(_ error: Error) -> ErrorInfo { + do { + let category = determineCategory(error) + let errorCode = determineErrorCode(error) + let severity = determineSeverity(error, category: category) + + let errorInfo = ErrorInfo( + error: error, + category: category, + errorCode: errorCode, + severity: severity, + timestamp: Date() + ) + + logger.debug(DailyNotificationErrorHandler.TAG, "Error categorized: \(errorInfo)") + return errorInfo + + } catch { + logger.error(DailyNotificationErrorHandler.TAG, "Error during categorization: \(error)") + return ErrorInfo( + error: error, + category: .unknown, + errorCode: "CATEGORIZATION_FAILED", + severity: .high, + timestamp: Date() + ) + } + } + + /** + * Determine error category based on error type + * + * @param error Error to analyze + * @return ErrorCategory + */ + private func determineCategory(_ error: Error) -> ErrorCategory { + let errorType = String(describing: type(of: error)) + let errorMessage = error.localizedDescription + + // Network errors + if errorType.contains("URLError") || errorType.contains("Network") || + errorType.contains("Connection") || errorType.contains("Timeout") { + return .network + } + + // Storage errors + if errorType.contains("SQLite") || errorType.contains("Database") || + errorType.contains("Storage") || errorType.contains("File") { + return .storage + } + + // Permission errors + if errorType.contains("Security") || errorType.contains("Permission") || + errorMessage.contains("permission") { + return .permission + } + + // Configuration errors + if errorType.contains("IllegalArgument") || errorType.contains("Configuration") || + errorMessage.contains("config") { + return .configuration + } + + // System errors + if errorType.contains("OutOfMemory") || errorType.contains("StackOverflow") || + errorType.contains("Runtime") { + return .system + } + + return .unknown + } + + /** + * Determine error code based on error details + * + * @param error Error to analyze + * @return Error code string + */ + private func determineErrorCode(_ error: Error) -> String { + let errorType = String(describing: type(of: error)) + let errorMessage = error.localizedDescription + + // Generate error code based on type and message + if !errorMessage.isEmpty { + return "\(errorType)_\(errorMessage.hashValue)" + } else { + return "\(errorType)_\(Date().timeIntervalSince1970)" + } + } + + /** + * Determine error severity based on error and category + * + * @param error Error to analyze + * @param category Error category + * @return ErrorSeverity + */ + private func determineSeverity(_ error: Error, category: ErrorCategory) -> ErrorSeverity { + let errorType = String(describing: type(of: error)) + + // Critical errors + if errorType.contains("OutOfMemory") || errorType.contains("StackOverflow") { + return .critical + } + + // High severity errors + if category == .system || category == .storage { + return .high + } + + // Medium severity errors + if category == .network || category == .permission { + return .medium + } + + // Low severity errors + return .low + } + + // MARK: - Retry Logic + + /** + * Check if error should be retried + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @return true if should retry + */ + private func shouldRetry(operationId: String, errorInfo: ErrorInfo) -> Bool { + return shouldRetry(operationId: operationId, errorInfo: errorInfo, retryConfig: nil) + } + + /** + * Check if error should be retried with custom config + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @param retryConfig Custom retry configuration + * @return true if should retry + */ + private func shouldRetry(operationId: String, errorInfo: ErrorInfo, retryConfig: RetryConfiguration?) -> Bool { + do { + // Get retry state + var state: RetryState + retryQueue.sync { + if retryStates[operationId] == nil { + retryStates[operationId] = RetryState() + } + state = retryStates[operationId]! + } + + // Check retry limits + let maxRetries = retryConfig?.maxRetries ?? config.maxRetries + if state.attemptCount >= maxRetries { + logger.debug(DailyNotificationErrorHandler.TAG, "Max retries exceeded for operation: \(operationId)") + return false + } + + // Check if error is retryable based on category + let isRetryable = isErrorRetryable(errorInfo.category) + + logger.debug(DailyNotificationErrorHandler.TAG, "Should retry: \(isRetryable) (attempt: \(state.attemptCount)/\(maxRetries))") + return isRetryable + + } catch { + logger.error(DailyNotificationErrorHandler.TAG, "Error checking retry eligibility: \(error)") + return false + } + } + + /** + * Check if error category is retryable + * + * @param category Error category + * @return true if retryable + */ + private func isErrorRetryable(_ category: ErrorCategory) -> Bool { + switch category { + case .network, .storage: + return true + case .permission, .configuration, .system, .unknown: + return false + } + } + + /** + * Handle retryable error + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @return ErrorResult with retry information + */ + private func handleRetryableError(operationId: String, errorInfo: ErrorInfo) -> ErrorResult { + return handleRetryableError(operationId: operationId, errorInfo: errorInfo, retryConfig: nil) + } + + /** + * Handle retryable error with custom config + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @param retryConfig Custom retry configuration + * @return ErrorResult with retry information + */ + private func handleRetryableError(operationId: String, errorInfo: ErrorInfo, retryConfig: RetryConfiguration?) -> ErrorResult { + do { + var state: RetryState + retryQueue.sync { + state = retryStates[operationId]! + state.attemptCount += 1 + } + + // Calculate delay with exponential backoff + let delay = calculateRetryDelay(attemptCount: state.attemptCount, retryConfig: retryConfig) + state.nextRetryTime = Date().addingTimeInterval(delay) + + logger.info(DailyNotificationErrorHandler.TAG, "Retryable error handled - retry in \(delay)s (attempt \(state.attemptCount))") + + return ErrorResult.retryable(errorInfo: errorInfo, retryDelaySeconds: delay, attemptCount: state.attemptCount) + + } catch { + logger.error(DailyNotificationErrorHandler.TAG, "Error handling retryable error: \(error)") + return ErrorResult.fatal(message: "Retry handling failure: \(error.localizedDescription)") + } + } + + /** + * Handle non-retryable error + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @return ErrorResult with failure information + */ + private func handleNonRetryableError(operationId: String, errorInfo: ErrorInfo) -> ErrorResult { + do { + logger.warning(DailyNotificationErrorHandler.TAG, "Non-retryable error handled for operation: \(operationId)") + + // Clean up retry state + retryQueue.async(flags: .barrier) { + self.retryStates.removeValue(forKey: operationId) + } + + return ErrorResult.fatal(errorInfo: errorInfo) + + } catch { + logger.error(DailyNotificationErrorHandler.TAG, "Error handling non-retryable error: \(error)") + return ErrorResult.fatal(message: "Non-retryable error handling failure: \(error.localizedDescription)") + } + } + + /** + * Calculate retry delay with exponential backoff + * + * @param attemptCount Current attempt number + * @param retryConfig Custom retry configuration + * @return Delay in seconds + */ + private func calculateRetryDelay(attemptCount: Int, retryConfig: RetryConfiguration?) -> TimeInterval { + do { + let baseDelay = retryConfig?.baseDelaySeconds ?? config.baseDelaySeconds + let multiplier = retryConfig?.backoffMultiplier ?? config.backoffMultiplier + let maxDelay = retryConfig?.maxDelaySeconds ?? config.maxDelaySeconds + + // Calculate exponential backoff: baseDelay * (multiplier ^ (attemptCount - 1)) + var delay = baseDelay * pow(multiplier, Double(attemptCount - 1)) + + // Cap at maximum delay + delay = min(delay, maxDelay) + + // Add jitter to prevent thundering herd + let jitter = delay * 0.1 * Double.random(in: 0...1) + delay += jitter + + logger.debug(DailyNotificationErrorHandler.TAG, "Calculated retry delay: \(delay)s (attempt \(attemptCount))") + return delay + + } catch { + logger.error(DailyNotificationErrorHandler.TAG, "Error calculating retry delay: \(error)") + return config.baseDelaySeconds + } + } + + // MARK: - Metrics and Telemetry + + /** + * Get error metrics + * + * @return ErrorMetrics with current statistics + */ + func getMetrics() -> ErrorMetrics { + return metrics + } + + /** + * Reset error metrics + */ + func resetMetrics() { + metrics.reset() + logger.debug(DailyNotificationErrorHandler.TAG, "Error metrics reset") + } + + /** + * Get retry statistics + * + * @return RetryStatistics with retry information + */ + func getRetryStatistics() -> RetryStatistics { + var totalOperations = 0 + var activeRetries = 0 + var totalRetries = 0 + + retryQueue.sync { + totalOperations = retryStates.count + for state in retryStates.values { + if state.attemptCount > 0 { + activeRetries += 1 + totalRetries += state.attemptCount + } + } + } + + return RetryStatistics(totalOperations: totalOperations, activeRetries: activeRetries, totalRetries: totalRetries) + } + + /** + * Clear retry states + */ + func clearRetryStates() { + retryQueue.async(flags: .barrier) { + self.retryStates.removeAll() + } + logger.debug(DailyNotificationErrorHandler.TAG, "Retry states cleared") + } + + // MARK: - Data Classes + + /** + * Error information + */ + struct ErrorInfo { + let error: Error + let category: ErrorCategory + let errorCode: String + let severity: ErrorSeverity + let timestamp: Date + + var description: String { + return "ErrorInfo{category=\(category), code=\(errorCode), severity=\(severity), error=\(String(describing: type(of: error)))}" + } + } + + /** + * Retry state for an operation + */ + private class RetryState { + var attemptCount = 0 + var nextRetryTime = Date() + } + + /** + * Error result + */ + struct ErrorResult { + let success: Bool + let retryable: Bool + let errorInfo: ErrorInfo? + let retryDelaySeconds: TimeInterval + let attemptCount: Int + let message: String + + static func retryable(errorInfo: ErrorInfo, retryDelaySeconds: TimeInterval, attemptCount: Int) -> ErrorResult { + return ErrorResult(success: false, retryable: true, errorInfo: errorInfo, retryDelaySeconds: retryDelaySeconds, attemptCount: attemptCount, message: "Retryable error") + } + + static func fatal(errorInfo: ErrorInfo) -> ErrorResult { + return ErrorResult(success: false, retryable: false, errorInfo: errorInfo, retryDelaySeconds: 0, attemptCount: 0, message: "Fatal error") + } + + static func fatal(message: String) -> ErrorResult { + return ErrorResult(success: false, retryable: false, errorInfo: nil, retryDelaySeconds: 0, attemptCount: 0, message: message) + } + } + + /** + * Error configuration + */ + struct ErrorConfiguration { + let maxRetries: Int + let baseDelaySeconds: TimeInterval + let maxDelaySeconds: TimeInterval + let backoffMultiplier: Double + + init() { + self.maxRetries = DailyNotificationErrorHandler.DEFAULT_MAX_RETRIES + self.baseDelaySeconds = DailyNotificationErrorHandler.DEFAULT_BASE_DELAY_SECONDS + self.maxDelaySeconds = DailyNotificationErrorHandler.DEFAULT_MAX_DELAY_SECONDS + self.backoffMultiplier = DailyNotificationErrorHandler.DEFAULT_BACKOFF_MULTIPLIER + } + + init(maxRetries: Int, baseDelaySeconds: TimeInterval, maxDelaySeconds: TimeInterval, backoffMultiplier: Double) { + self.maxRetries = maxRetries + self.baseDelaySeconds = baseDelaySeconds + self.maxDelaySeconds = maxDelaySeconds + self.backoffMultiplier = backoffMultiplier + } + } + + /** + * Retry configuration + */ + struct RetryConfiguration { + let maxRetries: Int + let baseDelaySeconds: TimeInterval + let maxDelaySeconds: TimeInterval + let backoffMultiplier: Double + + init(maxRetries: Int, baseDelaySeconds: TimeInterval, maxDelaySeconds: TimeInterval, backoffMultiplier: Double) { + self.maxRetries = maxRetries + self.baseDelaySeconds = baseDelaySeconds + self.maxDelaySeconds = maxDelaySeconds + self.backoffMultiplier = backoffMultiplier + } + } + + /** + * Error metrics + */ + class ErrorMetrics { + private var totalErrors = 0 + private var networkErrors = 0 + private var storageErrors = 0 + private var schedulingErrors = 0 + private var permissionErrors = 0 + private var configurationErrors = 0 + private var systemErrors = 0 + private var unknownErrors = 0 + + func recordError(_ errorInfo: ErrorInfo) { + totalErrors += 1 + + switch errorInfo.category { + case .network: + networkErrors += 1 + case .storage: + storageErrors += 1 + case .scheduling: + schedulingErrors += 1 + case .permission: + permissionErrors += 1 + case .configuration: + configurationErrors += 1 + case .system: + systemErrors += 1 + case .unknown: + unknownErrors += 1 + } + } + + func reset() { + totalErrors = 0 + networkErrors = 0 + storageErrors = 0 + schedulingErrors = 0 + permissionErrors = 0 + configurationErrors = 0 + systemErrors = 0 + unknownErrors = 0 + } + + var totalErrorsCount: Int { return totalErrors } + var networkErrorsCount: Int { return networkErrors } + var storageErrorsCount: Int { return storageErrors } + var schedulingErrorsCount: Int { return schedulingErrors } + var permissionErrorsCount: Int { return permissionErrors } + var configurationErrorsCount: Int { return configurationErrors } + var systemErrorsCount: Int { return systemErrors } + var unknownErrorsCount: Int { return unknownErrors } + } + + /** + * Retry statistics + */ + struct RetryStatistics { + let totalOperations: Int + let activeRetries: Int + let totalRetries: Int + + var description: String { + return "RetryStatistics{totalOps=\(totalOperations), activeRetries=\(activeRetries), totalRetries=\(totalRetries)}" + } + } +} diff --git a/src/android/DailyNotificationErrorHandler.java b/src/android/DailyNotificationErrorHandler.java new file mode 100644 index 0000000..09415aa --- /dev/null +++ b/src/android/DailyNotificationErrorHandler.java @@ -0,0 +1,668 @@ +/** + * DailyNotificationErrorHandler.java + * + * Android Error Handler for comprehensive error management + * Implements error categorization, retry logic, and telemetry + * + * @author Matthew Raymer + * @version 1.0.0 + */ + +package com.timesafari.dailynotification; + +import android.util.Log; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * Manages comprehensive error handling with categorization, retry logic, and telemetry + * + * This class implements the critical error handling functionality: + * - Categorizes errors by type, code, and severity + * - Implements exponential backoff retry logic + * - Tracks error metrics and telemetry + * - Provides debugging information + * - Manages retry state and limits + */ +public class DailyNotificationErrorHandler { + + // MARK: - Constants + + private static final String TAG = "DailyNotificationErrorHandler"; + + // Retry configuration + private static final int DEFAULT_MAX_RETRIES = 3; + private static final long DEFAULT_BASE_DELAY_MS = 1000; // 1 second + private static final long DEFAULT_MAX_DELAY_MS = 30000; // 30 seconds + private static final double DEFAULT_BACKOFF_MULTIPLIER = 2.0; + + // Error severity levels + public enum ErrorSeverity { + LOW, // Minor issues, non-critical + MEDIUM, // Moderate issues, may affect functionality + HIGH, // Serious issues, significant impact + CRITICAL // Critical issues, system failure + } + + // Error categories + public enum ErrorCategory { + NETWORK, // Network-related errors + STORAGE, // Storage/database errors + SCHEDULING, // Notification scheduling errors + PERMISSION, // Permission-related errors + CONFIGURATION, // Configuration errors + SYSTEM, // System-level errors + UNKNOWN // Unknown/unclassified errors + } + + // MARK: - Properties + + private final ConcurrentHashMap retryStates; + private final ErrorMetrics metrics; + private final ErrorConfiguration config; + + // MARK: - Initialization + + /** + * Constructor with default configuration + */ + public DailyNotificationErrorHandler() { + this(new ErrorConfiguration()); + } + + /** + * Constructor with custom configuration + * + * @param config Error handling configuration + */ + public DailyNotificationErrorHandler(ErrorConfiguration config) { + this.retryStates = new ConcurrentHashMap<>(); + this.metrics = new ErrorMetrics(); + this.config = config; + + Log.d(TAG, "ErrorHandler initialized with max retries: " + config.maxRetries); + } + + // MARK: - Error Handling + + /** + * Handle error with automatic retry logic + * + * @param operationId Unique identifier for the operation + * @param error Error to handle + * @param retryable Whether this error is retryable + * @return ErrorResult with handling information + */ + public ErrorResult handleError(String operationId, Throwable error, boolean retryable) { + try { + Log.d(TAG, "Handling error for operation: " + operationId); + + // Categorize error + ErrorInfo errorInfo = categorizeError(error); + + // Update metrics + metrics.recordError(errorInfo); + + // Check if retryable and within limits + if (retryable && shouldRetry(operationId, errorInfo)) { + return handleRetryableError(operationId, errorInfo); + } else { + return handleNonRetryableError(operationId, errorInfo); + } + + } catch (Exception e) { + Log.e(TAG, "Error in error handler", e); + return ErrorResult.fatal("Error handler failure: " + e.getMessage()); + } + } + + /** + * Handle error with custom retry configuration + * + * @param operationId Unique identifier for the operation + * @param error Error to handle + * @param retryConfig Custom retry configuration + * @return ErrorResult with handling information + */ + public ErrorResult handleError(String operationId, Throwable error, RetryConfiguration retryConfig) { + try { + Log.d(TAG, "Handling error with custom retry config for operation: " + operationId); + + // Categorize error + ErrorInfo errorInfo = categorizeError(error); + + // Update metrics + metrics.recordError(errorInfo); + + // Check if retryable with custom config + if (shouldRetry(operationId, errorInfo, retryConfig)) { + return handleRetryableError(operationId, errorInfo, retryConfig); + } else { + return handleNonRetryableError(operationId, errorInfo); + } + + } catch (Exception e) { + Log.e(TAG, "Error in error handler with custom config", e); + return ErrorResult.fatal("Error handler failure: " + e.getMessage()); + } + } + + // MARK: - Error Categorization + + /** + * Categorize error by type, code, and severity + * + * @param error Error to categorize + * @return ErrorInfo with categorization + */ + private ErrorInfo categorizeError(Throwable error) { + try { + ErrorCategory category = determineCategory(error); + String errorCode = determineErrorCode(error); + ErrorSeverity severity = determineSeverity(error, category); + + ErrorInfo errorInfo = new ErrorInfo( + error, + category, + errorCode, + severity, + System.currentTimeMillis() + ); + + Log.d(TAG, "Error categorized: " + errorInfo); + return errorInfo; + + } catch (Exception e) { + Log.e(TAG, "Error during categorization", e); + return new ErrorInfo(error, ErrorCategory.UNKNOWN, "CATEGORIZATION_FAILED", ErrorSeverity.HIGH, System.currentTimeMillis()); + } + } + + /** + * Determine error category based on error type + * + * @param error Error to analyze + * @return ErrorCategory + */ + private ErrorCategory determineCategory(Throwable error) { + String errorMessage = error.getMessage(); + String errorType = error.getClass().getSimpleName(); + + // Network errors + if (errorType.contains("IOException") || errorType.contains("Socket") || + errorType.contains("Connect") || errorType.contains("Timeout")) { + return ErrorCategory.NETWORK; + } + + // Storage errors + if (errorType.contains("SQLite") || errorType.contains("Database") || + errorType.contains("Storage") || errorType.contains("File")) { + return ErrorCategory.STORAGE; + } + + // Permission errors + if (errorType.contains("Security") || errorType.contains("Permission") || + errorMessage != null && errorMessage.contains("permission")) { + return ErrorCategory.PERMISSION; + } + + // Configuration errors + if (errorType.contains("IllegalArgument") || errorType.contains("Configuration") || + errorMessage != null && errorMessage.contains("config")) { + return ErrorCategory.CONFIGURATION; + } + + // System errors + if (errorType.contains("OutOfMemory") || errorType.contains("StackOverflow") || + errorType.contains("Runtime")) { + return ErrorCategory.SYSTEM; + } + + return ErrorCategory.UNKNOWN; + } + + /** + * Determine error code based on error details + * + * @param error Error to analyze + * @return Error code string + */ + private String determineErrorCode(Throwable error) { + String errorType = error.getClass().getSimpleName(); + String errorMessage = error.getMessage(); + + // Generate error code based on type and message + if (errorMessage != null && errorMessage.length() > 0) { + return errorType + "_" + errorMessage.hashCode(); + } else { + return errorType + "_" + System.currentTimeMillis(); + } + } + + /** + * Determine error severity based on error and category + * + * @param error Error to analyze + * @param category Error category + * @return ErrorSeverity + */ + private ErrorSeverity determineSeverity(Throwable error, ErrorCategory category) { + // Critical errors + if (error instanceof OutOfMemoryError || error instanceof StackOverflowError) { + return ErrorSeverity.CRITICAL; + } + + // High severity errors + if (category == ErrorCategory.SYSTEM || category == ErrorCategory.STORAGE) { + return ErrorSeverity.HIGH; + } + + // Medium severity errors + if (category == ErrorCategory.NETWORK || category == ErrorCategory.PERMISSION) { + return ErrorSeverity.MEDIUM; + } + + // Low severity errors + return ErrorSeverity.LOW; + } + + // MARK: - Retry Logic + + /** + * Check if error should be retried + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @return true if should retry + */ + private boolean shouldRetry(String operationId, ErrorInfo errorInfo) { + return shouldRetry(operationId, errorInfo, null); + } + + /** + * Check if error should be retried with custom config + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @param retryConfig Custom retry configuration + * @return true if should retry + */ + private boolean shouldRetry(String operationId, ErrorInfo errorInfo, RetryConfiguration retryConfig) { + try { + // Get retry state + RetryState state = retryStates.get(operationId); + if (state == null) { + state = new RetryState(); + retryStates.put(operationId, state); + } + + // Check retry limits + int maxRetries = retryConfig != null ? retryConfig.maxRetries : config.maxRetries; + if (state.attemptCount >= maxRetries) { + Log.d(TAG, "Max retries exceeded for operation: " + operationId); + return false; + } + + // Check if error is retryable based on category + boolean isRetryable = isErrorRetryable(errorInfo.category); + + Log.d(TAG, "Should retry: " + isRetryable + " (attempt: " + state.attemptCount + "/" + maxRetries + ")"); + return isRetryable; + + } catch (Exception e) { + Log.e(TAG, "Error checking retry eligibility", e); + return false; + } + } + + /** + * Check if error category is retryable + * + * @param category Error category + * @return true if retryable + */ + private boolean isErrorRetryable(ErrorCategory category) { + switch (category) { + case NETWORK: + case STORAGE: + return true; + case PERMISSION: + case CONFIGURATION: + case SYSTEM: + case UNKNOWN: + default: + return false; + } + } + + /** + * Handle retryable error + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @return ErrorResult with retry information + */ + private ErrorResult handleRetryableError(String operationId, ErrorInfo errorInfo) { + return handleRetryableError(operationId, errorInfo, null); + } + + /** + * Handle retryable error with custom config + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @param retryConfig Custom retry configuration + * @return ErrorResult with retry information + */ + private ErrorResult handleRetryableError(String operationId, ErrorInfo errorInfo, RetryConfiguration retryConfig) { + try { + RetryState state = retryStates.get(operationId); + state.attemptCount++; + + // Calculate delay with exponential backoff + long delay = calculateRetryDelay(state.attemptCount, retryConfig); + state.nextRetryTime = System.currentTimeMillis() + delay; + + Log.i(TAG, "Retryable error handled - retry in " + delay + "ms (attempt " + state.attemptCount + ")"); + + return ErrorResult.retryable(errorInfo, delay, state.attemptCount); + + } catch (Exception e) { + Log.e(TAG, "Error handling retryable error", e); + return ErrorResult.fatal("Retry handling failure: " + e.getMessage()); + } + } + + /** + * Handle non-retryable error + * + * @param operationId Operation identifier + * @param errorInfo Error information + * @return ErrorResult with failure information + */ + private ErrorResult handleNonRetryableError(String operationId, ErrorInfo errorInfo) { + try { + Log.w(TAG, "Non-retryable error handled for operation: " + operationId); + + // Clean up retry state + retryStates.remove(operationId); + + return ErrorResult.fatal(errorInfo); + + } catch (Exception e) { + Log.e(TAG, "Error handling non-retryable error", e); + return ErrorResult.fatal("Non-retryable error handling failure: " + e.getMessage()); + } + } + + /** + * Calculate retry delay with exponential backoff + * + * @param attemptCount Current attempt number + * @param retryConfig Custom retry configuration + * @return Delay in milliseconds + */ + private long calculateRetryDelay(int attemptCount, RetryConfiguration retryConfig) { + try { + long baseDelay = retryConfig != null ? retryConfig.baseDelayMs : config.baseDelayMs; + double multiplier = retryConfig != null ? retryConfig.backoffMultiplier : config.backoffMultiplier; + long maxDelay = retryConfig != null ? retryConfig.maxDelayMs : config.maxDelayMs; + + // Calculate exponential backoff: baseDelay * (multiplier ^ (attemptCount - 1)) + long delay = (long) (baseDelay * Math.pow(multiplier, attemptCount - 1)); + + // Cap at maximum delay + delay = Math.min(delay, maxDelay); + + // Add jitter to prevent thundering herd + long jitter = (long) (delay * 0.1 * Math.random()); + delay += jitter; + + Log.d(TAG, "Calculated retry delay: " + delay + "ms (attempt " + attemptCount + ")"); + return delay; + + } catch (Exception e) { + Log.e(TAG, "Error calculating retry delay", e); + return config.baseDelayMs; + } + } + + // MARK: - Metrics and Telemetry + + /** + * Get error metrics + * + * @return ErrorMetrics with current statistics + */ + public ErrorMetrics getMetrics() { + return metrics; + } + + /** + * Reset error metrics + */ + public void resetMetrics() { + metrics.reset(); + Log.d(TAG, "Error metrics reset"); + } + + /** + * Get retry statistics + * + * @return RetryStatistics with retry information + */ + public RetryStatistics getRetryStatistics() { + int totalOperations = retryStates.size(); + int activeRetries = 0; + int totalRetries = 0; + + for (RetryState state : retryStates.values()) { + if (state.attemptCount > 0) { + activeRetries++; + totalRetries += state.attemptCount; + } + } + + return new RetryStatistics(totalOperations, activeRetries, totalRetries); + } + + /** + * Clear retry states + */ + public void clearRetryStates() { + retryStates.clear(); + Log.d(TAG, "Retry states cleared"); + } + + // MARK: - Data Classes + + /** + * Error information + */ + public static class ErrorInfo { + public final Throwable error; + public final ErrorCategory category; + public final String errorCode; + public final ErrorSeverity severity; + public final long timestamp; + + public ErrorInfo(Throwable error, ErrorCategory category, String errorCode, ErrorSeverity severity, long timestamp) { + this.error = error; + this.category = category; + this.errorCode = errorCode; + this.severity = severity; + this.timestamp = timestamp; + } + + @Override + public String toString() { + return String.format("ErrorInfo{category=%s, code=%s, severity=%s, error=%s}", + category, errorCode, severity, error.getClass().getSimpleName()); + } + } + + /** + * Retry state for an operation + */ + private static class RetryState { + public int attemptCount = 0; + public long nextRetryTime = 0; + } + + /** + * Error result + */ + public static class ErrorResult { + public final boolean success; + public final boolean retryable; + public final ErrorInfo errorInfo; + public final long retryDelayMs; + public final int attemptCount; + public final String message; + + private ErrorResult(boolean success, boolean retryable, ErrorInfo errorInfo, long retryDelayMs, int attemptCount, String message) { + this.success = success; + this.retryable = retryable; + this.errorInfo = errorInfo; + this.retryDelayMs = retryDelayMs; + this.attemptCount = attemptCount; + this.message = message; + } + + public static ErrorResult retryable(ErrorInfo errorInfo, long retryDelayMs, int attemptCount) { + return new ErrorResult(false, true, errorInfo, retryDelayMs, attemptCount, "Retryable error"); + } + + public static ErrorResult fatal(ErrorInfo errorInfo) { + return new ErrorResult(false, false, errorInfo, 0, 0, "Fatal error"); + } + + public static ErrorResult fatal(String message) { + return new ErrorResult(false, false, null, 0, 0, message); + } + } + + /** + * Error configuration + */ + public static class ErrorConfiguration { + public final int maxRetries; + public final long baseDelayMs; + public final long maxDelayMs; + public final double backoffMultiplier; + + public ErrorConfiguration() { + this(DEFAULT_MAX_RETRIES, DEFAULT_BASE_DELAY_MS, DEFAULT_MAX_DELAY_MS, DEFAULT_BACKOFF_MULTIPLIER); + } + + public ErrorConfiguration(int maxRetries, long baseDelayMs, long maxDelayMs, double backoffMultiplier) { + this.maxRetries = maxRetries; + this.baseDelayMs = baseDelayMs; + this.maxDelayMs = maxDelayMs; + this.backoffMultiplier = backoffMultiplier; + } + } + + /** + * Retry configuration + */ + public static class RetryConfiguration { + public final int maxRetries; + public final long baseDelayMs; + public final long maxDelayMs; + public final double backoffMultiplier; + + public RetryConfiguration(int maxRetries, long baseDelayMs, long maxDelayMs, double backoffMultiplier) { + this.maxRetries = maxRetries; + this.baseDelayMs = baseDelayMs; + this.maxDelayMs = maxDelayMs; + this.backoffMultiplier = backoffMultiplier; + } + } + + /** + * Error metrics + */ + public static class ErrorMetrics { + private final AtomicInteger totalErrors = new AtomicInteger(0); + private final AtomicInteger networkErrors = new AtomicInteger(0); + private final AtomicInteger storageErrors = new AtomicInteger(0); + private final AtomicInteger schedulingErrors = new AtomicInteger(0); + private final AtomicInteger permissionErrors = new AtomicInteger(0); + private final AtomicInteger configurationErrors = new AtomicInteger(0); + private final AtomicInteger systemErrors = new AtomicInteger(0); + private final AtomicInteger unknownErrors = new AtomicInteger(0); + + public void recordError(ErrorInfo errorInfo) { + totalErrors.incrementAndGet(); + + switch (errorInfo.category) { + case NETWORK: + networkErrors.incrementAndGet(); + break; + case STORAGE: + storageErrors.incrementAndGet(); + break; + case SCHEDULING: + schedulingErrors.incrementAndGet(); + break; + case PERMISSION: + permissionErrors.incrementAndGet(); + break; + case CONFIGURATION: + configurationErrors.incrementAndGet(); + break; + case SYSTEM: + systemErrors.incrementAndGet(); + break; + case UNKNOWN: + default: + unknownErrors.incrementAndGet(); + break; + } + } + + public void reset() { + totalErrors.set(0); + networkErrors.set(0); + storageErrors.set(0); + schedulingErrors.set(0); + permissionErrors.set(0); + configurationErrors.set(0); + systemErrors.set(0); + unknownErrors.set(0); + } + + public int getTotalErrors() { return totalErrors.get(); } + public int getNetworkErrors() { return networkErrors.get(); } + public int getStorageErrors() { return storageErrors.get(); } + public int getSchedulingErrors() { return schedulingErrors.get(); } + public int getPermissionErrors() { return permissionErrors.get(); } + public int getConfigurationErrors() { return configurationErrors.get(); } + public int getSystemErrors() { return systemErrors.get(); } + public int getUnknownErrors() { return unknownErrors.get(); } + } + + /** + * Retry statistics + */ + public static class RetryStatistics { + public final int totalOperations; + public final int activeRetries; + public final int totalRetries; + + public RetryStatistics(int totalOperations, int activeRetries, int totalRetries) { + this.totalOperations = totalOperations; + this.activeRetries = activeRetries; + this.totalRetries = totalRetries; + } + + @Override + public String toString() { + return String.format("RetryStatistics{totalOps=%d, activeRetries=%d, totalRetries=%d}", + totalOperations, activeRetries, totalRetries); + } + } +}