Initial Commit
This commit is contained in:
339
lib/connection/retry_strategy.dart
Normal file
339
lib/connection/retry_strategy.dart
Normal file
@@ -0,0 +1,339 @@
|
||||
import 'dart:math';
|
||||
|
||||
|
||||
enum ErrorType {
|
||||
|
||||
network,
|
||||
|
||||
|
||||
server,
|
||||
|
||||
|
||||
authentication,
|
||||
|
||||
|
||||
protocol,
|
||||
|
||||
|
||||
unknown,
|
||||
}
|
||||
|
||||
|
||||
class ErrorInfo {
|
||||
final ErrorType type;
|
||||
final String message;
|
||||
final int? httpStatusCode;
|
||||
final DateTime timestamp;
|
||||
final Map<String, dynamic>? metadata;
|
||||
|
||||
ErrorInfo({
|
||||
required this.type,
|
||||
required this.message,
|
||||
this.httpStatusCode,
|
||||
required this.timestamp,
|
||||
this.metadata,
|
||||
});
|
||||
|
||||
|
||||
static ErrorType getErrorTypeFromHttpStatus(int statusCode) {
|
||||
if (statusCode >= 500) return ErrorType.server;
|
||||
if (statusCode == 401 || statusCode == 403) return ErrorType.authentication;
|
||||
if (statusCode >= 400) return ErrorType.protocol;
|
||||
return ErrorType.network;
|
||||
}
|
||||
|
||||
|
||||
static ErrorType getErrorTypeFromMessage(String message) {
|
||||
final lowerMessage = message.toLowerCase();
|
||||
|
||||
if (lowerMessage.contains('timeout') ||
|
||||
lowerMessage.contains('connection') ||
|
||||
lowerMessage.contains('network')) {
|
||||
return ErrorType.network;
|
||||
}
|
||||
|
||||
if (lowerMessage.contains('unauthorized') ||
|
||||
lowerMessage.contains('forbidden') ||
|
||||
lowerMessage.contains('token')) {
|
||||
return ErrorType.authentication;
|
||||
}
|
||||
|
||||
if (lowerMessage.contains('server') || lowerMessage.contains('internal')) {
|
||||
return ErrorType.server;
|
||||
}
|
||||
|
||||
return ErrorType.unknown;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RetryStrategy {
|
||||
final int maxAttempts;
|
||||
final Duration baseDelay;
|
||||
final Duration maxDelay;
|
||||
final double backoffMultiplier;
|
||||
final double jitterFactor;
|
||||
final Map<ErrorType, RetryConfig> errorConfigs;
|
||||
|
||||
RetryStrategy({
|
||||
this.maxAttempts = 10,
|
||||
this.baseDelay = const Duration(seconds: 1),
|
||||
this.maxDelay = const Duration(minutes: 5),
|
||||
this.backoffMultiplier = 2.0,
|
||||
this.jitterFactor = 0.1,
|
||||
Map<ErrorType, RetryConfig>? errorConfigs,
|
||||
}) : errorConfigs = errorConfigs ?? _defaultErrorConfigs;
|
||||
|
||||
static final Map<ErrorType, RetryConfig> _defaultErrorConfigs = {
|
||||
ErrorType.network: RetryConfig(
|
||||
maxAttempts: 15,
|
||||
baseDelay: Duration(seconds: 2),
|
||||
maxDelay: Duration(minutes: 10),
|
||||
backoffMultiplier: 1.5,
|
||||
),
|
||||
ErrorType.server: RetryConfig(
|
||||
maxAttempts: 8,
|
||||
baseDelay: Duration(seconds: 5),
|
||||
maxDelay: Duration(minutes: 3),
|
||||
backoffMultiplier: 2.0,
|
||||
),
|
||||
ErrorType.authentication: RetryConfig(
|
||||
maxAttempts: 3,
|
||||
baseDelay: Duration(seconds: 1),
|
||||
maxDelay: Duration(seconds: 10),
|
||||
backoffMultiplier: 1.0,
|
||||
),
|
||||
ErrorType.protocol: RetryConfig(
|
||||
maxAttempts: 5,
|
||||
baseDelay: Duration(seconds: 2),
|
||||
maxDelay: Duration(minutes: 2),
|
||||
backoffMultiplier: 1.5,
|
||||
),
|
||||
ErrorType.unknown: RetryConfig(
|
||||
maxAttempts: 5,
|
||||
baseDelay: Duration(seconds: 3),
|
||||
maxDelay: Duration(minutes: 5),
|
||||
backoffMultiplier: 2.0,
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
Duration calculateDelay(int attempt, ErrorType errorType) {
|
||||
final config = errorConfigs[errorType] ?? errorConfigs[ErrorType.unknown]!;
|
||||
|
||||
|
||||
final exponentialDelay =
|
||||
config.baseDelay * pow(config.backoffMultiplier, attempt - 1);
|
||||
final cappedDelay = exponentialDelay > config.maxDelay
|
||||
? config.maxDelay
|
||||
: exponentialDelay;
|
||||
|
||||
|
||||
final jitter =
|
||||
cappedDelay.inMilliseconds *
|
||||
jitterFactor *
|
||||
(Random().nextDouble() * 2 - 1);
|
||||
final finalDelay = Duration(
|
||||
milliseconds: (cappedDelay.inMilliseconds + jitter).round(),
|
||||
);
|
||||
|
||||
return finalDelay;
|
||||
}
|
||||
|
||||
|
||||
bool shouldRetry(int attempt, ErrorType errorType) {
|
||||
final config = errorConfigs[errorType] ?? errorConfigs[ErrorType.unknown]!;
|
||||
return attempt <= config.maxAttempts;
|
||||
}
|
||||
|
||||
|
||||
RetryConfig getConfigForError(ErrorType errorType) {
|
||||
return errorConfigs[errorType] ?? errorConfigs[ErrorType.unknown]!;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RetryConfig {
|
||||
final int maxAttempts;
|
||||
final Duration baseDelay;
|
||||
final Duration maxDelay;
|
||||
final double backoffMultiplier;
|
||||
|
||||
RetryConfig({
|
||||
required this.maxAttempts,
|
||||
required this.baseDelay,
|
||||
required this.maxDelay,
|
||||
required this.backoffMultiplier,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
class RetryManager {
|
||||
final RetryStrategy _strategy;
|
||||
final Map<String, RetrySession> _sessions = {};
|
||||
|
||||
RetryManager({RetryStrategy? strategy})
|
||||
: _strategy = strategy ?? RetryStrategy();
|
||||
|
||||
|
||||
RetrySession startSession(String sessionId, ErrorType initialErrorType) {
|
||||
final session = RetrySession(
|
||||
id: sessionId,
|
||||
strategy: _strategy,
|
||||
initialErrorType: initialErrorType,
|
||||
);
|
||||
_sessions[sessionId] = session;
|
||||
return session;
|
||||
}
|
||||
|
||||
|
||||
RetrySession? getSession(String sessionId) {
|
||||
return _sessions[sessionId];
|
||||
}
|
||||
|
||||
|
||||
void endSession(String sessionId) {
|
||||
_sessions.remove(sessionId);
|
||||
}
|
||||
|
||||
|
||||
void clearSessions() {
|
||||
_sessions.clear();
|
||||
}
|
||||
|
||||
|
||||
Map<String, dynamic> getStatistics() {
|
||||
final totalSessions = _sessions.length;
|
||||
final activeSessions = _sessions.values.where((s) => s.isActive).length;
|
||||
final successfulSessions = _sessions.values
|
||||
.where((s) => s.isSuccessful)
|
||||
.length;
|
||||
final failedSessions = _sessions.values.where((s) => s.isFailed).length;
|
||||
|
||||
return {
|
||||
'total_sessions': totalSessions,
|
||||
'active_sessions': activeSessions,
|
||||
'successful_sessions': successfulSessions,
|
||||
'failed_sessions': failedSessions,
|
||||
'success_rate': totalSessions > 0
|
||||
? successfulSessions / totalSessions
|
||||
: 0.0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RetrySession {
|
||||
final String id;
|
||||
final RetryStrategy strategy;
|
||||
final ErrorType initialErrorType;
|
||||
final DateTime startTime;
|
||||
final List<RetryAttempt> attempts = [];
|
||||
|
||||
RetrySession({
|
||||
required this.id,
|
||||
required this.strategy,
|
||||
required this.initialErrorType,
|
||||
}) : startTime = DateTime.now();
|
||||
|
||||
|
||||
void addAttempt(
|
||||
ErrorType errorType, {
|
||||
String? message,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) {
|
||||
final attempt = RetryAttempt(
|
||||
number: attempts.length + 1,
|
||||
errorType: errorType,
|
||||
timestamp: DateTime.now(),
|
||||
message: message,
|
||||
metadata: metadata,
|
||||
);
|
||||
attempts.add(attempt);
|
||||
}
|
||||
|
||||
|
||||
Duration getNextDelay() {
|
||||
return strategy.calculateDelay(attempts.length + 1, currentErrorType);
|
||||
}
|
||||
|
||||
|
||||
bool canRetry() {
|
||||
return strategy.shouldRetry(attempts.length + 1, currentErrorType);
|
||||
}
|
||||
|
||||
|
||||
ErrorType get currentErrorType {
|
||||
if (attempts.isEmpty) return initialErrorType;
|
||||
return attempts.last.errorType;
|
||||
}
|
||||
|
||||
|
||||
int get attemptCount => attempts.length;
|
||||
|
||||
|
||||
bool get isActive => !isSuccessful && !isFailed && canRetry();
|
||||
|
||||
|
||||
bool get isSuccessful => attempts.isNotEmpty && attempts.last.isSuccessful;
|
||||
|
||||
|
||||
bool get isFailed => !canRetry() && !isSuccessful;
|
||||
|
||||
|
||||
Duration get duration => DateTime.now().difference(startTime);
|
||||
|
||||
|
||||
RetryAttempt? get lastAttempt => attempts.isNotEmpty ? attempts.last : null;
|
||||
|
||||
|
||||
Map<String, dynamic> getStatistics() {
|
||||
final errorTypes = attempts.map((a) => a.errorType.name).toList();
|
||||
final errorTypeCounts = <String, int>{};
|
||||
for (final type in errorTypes) {
|
||||
errorTypeCounts[type] = (errorTypeCounts[type] ?? 0) + 1;
|
||||
}
|
||||
|
||||
return {
|
||||
'session_id': id,
|
||||
'start_time': startTime.toIso8601String(),
|
||||
'duration_seconds': duration.inSeconds,
|
||||
'attempt_count': attemptCount,
|
||||
'is_active': isActive,
|
||||
'is_successful': isSuccessful,
|
||||
'is_failed': isFailed,
|
||||
'error_types': errorTypeCounts,
|
||||
'last_attempt': lastAttempt?.toJson(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class RetryAttempt {
|
||||
final int number;
|
||||
final ErrorType errorType;
|
||||
final DateTime timestamp;
|
||||
final String? message;
|
||||
final Map<String, dynamic>? metadata;
|
||||
final bool isSuccessful;
|
||||
|
||||
RetryAttempt({
|
||||
required this.number,
|
||||
required this.errorType,
|
||||
required this.timestamp,
|
||||
this.message,
|
||||
this.metadata,
|
||||
this.isSuccessful = false,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'number': number,
|
||||
'error_type': errorType.name,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'message': message,
|
||||
'metadata': metadata,
|
||||
'is_successful': isSuccessful,
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user