You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

650 lines
22 KiB

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