161 lines
4.7 KiB
Dart
161 lines
4.7 KiB
Dart
/// Retry policy with exponential backoff for network requests
|
|
library retry_policy;
|
|
|
|
import 'dart:async';
|
|
import 'dart:io';
|
|
import 'dart:math';
|
|
|
|
/// Configuration for retry behavior
|
|
class RetryConfig {
|
|
/// Maximum number of retry attempts
|
|
final int maxAttempts;
|
|
|
|
/// Initial delay before first retry (milliseconds)
|
|
final int initialDelayMs;
|
|
|
|
/// Maximum delay between retries (milliseconds)
|
|
final int maxDelayMs;
|
|
|
|
/// Multiplier for exponential backoff
|
|
final double backoffMultiplier;
|
|
|
|
/// Whether to add jitter to retry delays
|
|
final bool useJitter;
|
|
|
|
const RetryConfig({
|
|
this.maxAttempts = 3,
|
|
this.initialDelayMs = 1000,
|
|
this.maxDelayMs = 30000,
|
|
this.backoffMultiplier = 2.0,
|
|
this.useJitter = true,
|
|
});
|
|
|
|
/// Default configuration for standard API calls
|
|
static const standard = RetryConfig();
|
|
|
|
/// Configuration for critical operations
|
|
static const critical = RetryConfig(
|
|
maxAttempts: 5,
|
|
initialDelayMs: 500,
|
|
maxDelayMs: 60000,
|
|
);
|
|
|
|
/// Configuration for background sync
|
|
static const backgroundSync = RetryConfig(
|
|
maxAttempts: 10,
|
|
initialDelayMs: 2000,
|
|
maxDelayMs: 120000,
|
|
);
|
|
}
|
|
|
|
/// Retry policy implementation with exponential backoff
|
|
class RetryPolicy {
|
|
final RetryConfig config;
|
|
final Random _random = Random();
|
|
|
|
RetryPolicy({RetryConfig? config}) : config = config ?? RetryConfig.standard;
|
|
|
|
/// Executes an operation with retry logic
|
|
///
|
|
/// [operation]: The async operation to execute
|
|
/// [shouldRetry]: Optional function to determine if error is retryable
|
|
/// [onRetry]: Optional callback when retry attempt is made
|
|
///
|
|
/// Returns the result of the operation
|
|
/// Throws the last error if all retries fail
|
|
Future<T> execute<T>({
|
|
required Future<T> Function() operation,
|
|
bool Function(dynamic error)? shouldRetry,
|
|
void Function(int attempt, dynamic error, Duration delay)? onRetry,
|
|
}) async {
|
|
int attempt = 0;
|
|
dynamic lastError;
|
|
|
|
while (attempt < config.maxAttempts) {
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
lastError = error;
|
|
attempt++;
|
|
|
|
// Check if we should retry this error
|
|
final retryable = shouldRetry?.call(error) ?? _isRetryableError(error);
|
|
|
|
if (!retryable || attempt >= config.maxAttempts) {
|
|
throw error;
|
|
}
|
|
|
|
// Calculate delay with exponential backoff
|
|
final delay = _calculateDelay(attempt);
|
|
|
|
// Notify about retry
|
|
onRetry?.call(attempt, error, delay);
|
|
|
|
// Wait before next attempt
|
|
await Future.delayed(delay);
|
|
}
|
|
}
|
|
|
|
// Should never reach here, but throw last error just in case
|
|
throw lastError!;
|
|
}
|
|
|
|
/// Calculates delay for given attempt number
|
|
Duration _calculateDelay(int attempt) {
|
|
// Exponential backoff: initialDelay * (multiplier ^ (attempt - 1))
|
|
final exponentialDelay = config.initialDelayMs *
|
|
pow(config.backoffMultiplier, attempt - 1).toInt();
|
|
|
|
// Cap at max delay
|
|
var delayMs = min(exponentialDelay, config.maxDelayMs);
|
|
|
|
// Add jitter to prevent thundering herd
|
|
if (config.useJitter) {
|
|
final jitter = _random.nextDouble() * 0.3; // ±30% jitter
|
|
delayMs = (delayMs * (1 + jitter - 0.15)).toInt();
|
|
}
|
|
|
|
return Duration(milliseconds: delayMs);
|
|
}
|
|
|
|
/// Determines if an error is retryable
|
|
bool _isRetryableError(dynamic error) {
|
|
// Network errors are retryable
|
|
if (error is TimeoutException) return true;
|
|
if (error is SocketException) return true;
|
|
|
|
// HTTP status codes that are retryable
|
|
if (error.toString().contains('500')) return true; // Internal Server Error
|
|
if (error.toString().contains('502')) return true; // Bad Gateway
|
|
if (error.toString().contains('503')) return true; // Service Unavailable
|
|
if (error.toString().contains('504')) return true; // Gateway Timeout
|
|
if (error.toString().contains('429')) return true; // Too Many Requests
|
|
|
|
// Client errors (4xx) are generally not retryable
|
|
if (error.toString().contains('400')) return false; // Bad Request
|
|
if (error.toString().contains('401')) return false; // Unauthorized
|
|
if (error.toString().contains('403')) return false; // Forbidden
|
|
if (error.toString().contains('404')) return false; // Not Found
|
|
|
|
// Default: don't retry unknown errors
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Extension to add retry capability to Future operations
|
|
extension RetryExtension<T> on Future<T> Function() {
|
|
/// Executes this operation with retry logic
|
|
Future<T> withRetry({
|
|
RetryConfig? config,
|
|
bool Function(dynamic error)? shouldRetry,
|
|
void Function(int attempt, dynamic error, Duration delay)? onRetry,
|
|
}) {
|
|
final policy = RetryPolicy(config: config);
|
|
return policy.execute(
|
|
operation: this,
|
|
shouldRetry: shouldRetry,
|
|
onRetry: onRetry,
|
|
);
|
|
}
|
|
}
|