/** * 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)}" } } }