- Created RetryHelper service with three retry strategies: - retry(): Generic retry with configurable backoff - retryNetwork(): Optimized for network operations (4 attempts, 2-16s backoff) - retryCritical(): Aggressive retry for critical operations (5 attempts, 1-10s backoff) - Applied retry logic to critical API methods: - getPlayers(): Essential for player list loading - getQueue(): Critical for now playing bar - getAlbumTracks(): Required for album playback - getPlaylistTracks(): Required for playlist playback - playTracks(): Critical for playback operations - search(): User-facing search functionality - Network-aware retry detection: - Automatically retries on socket/network/timeout/connection errors - Smart error filtering to avoid retrying non-transient errors - Exponential backoff to prevent server overload - Comprehensive logging: - Logs each retry attempt with delay duration - Logs final failure after all retries exhausted - Maintains visibility into retry behavior This improves app reliability under poor network conditions and handles transient network failures gracefully.
88 lines
2.7 KiB
Dart
88 lines
2.7 KiB
Dart
import 'dart:async';
|
|
import 'debug_logger.dart';
|
|
|
|
class RetryHelper {
|
|
static final _logger = DebugLogger();
|
|
|
|
/// Retry a function with exponential backoff
|
|
///
|
|
/// [operation] - The async function to retry
|
|
/// [maxAttempts] - Maximum number of attempts (default: 3)
|
|
/// [initialDelay] - Initial delay in seconds (default: 2)
|
|
/// [maxDelay] - Maximum delay in seconds (default: 16)
|
|
/// [shouldRetry] - Optional function to determine if we should retry based on error
|
|
static Future<T> retry<T>({
|
|
required Future<T> Function() operation,
|
|
int maxAttempts = 3,
|
|
int initialDelaySeconds = 2,
|
|
int maxDelaySeconds = 16,
|
|
bool Function(dynamic error)? shouldRetry,
|
|
}) async {
|
|
int attempt = 0;
|
|
int delaySeconds = initialDelaySeconds;
|
|
|
|
while (true) {
|
|
attempt++;
|
|
|
|
try {
|
|
return await operation();
|
|
} catch (error) {
|
|
// Check if we should retry this error
|
|
final canRetry = shouldRetry?.call(error) ?? true;
|
|
|
|
// If this was the last attempt or we shouldn't retry, rethrow
|
|
if (attempt >= maxAttempts || !canRetry) {
|
|
_logger.log('❌ Operation failed after $attempt attempts: $error');
|
|
rethrow;
|
|
}
|
|
|
|
// Log the retry attempt
|
|
_logger.log(
|
|
'⚠️ Attempt $attempt/$maxAttempts failed: $error. Retrying in ${delaySeconds}s...'
|
|
);
|
|
|
|
// Wait before retrying (exponential backoff)
|
|
await Future.delayed(Duration(seconds: delaySeconds));
|
|
|
|
// Double the delay for next attempt, but cap at maxDelay
|
|
delaySeconds = (delaySeconds * 2).clamp(initialDelaySeconds, maxDelaySeconds);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Retry specifically for network operations
|
|
/// Uses default settings optimized for network calls
|
|
static Future<T> retryNetwork<T>({
|
|
required Future<T> Function() operation,
|
|
int maxAttempts = 4, // More attempts for network
|
|
}) {
|
|
return retry(
|
|
operation: operation,
|
|
maxAttempts: maxAttempts,
|
|
initialDelaySeconds: 2,
|
|
maxDelaySeconds: 16,
|
|
shouldRetry: (error) {
|
|
// Retry on network-related errors
|
|
final errorString = error.toString().toLowerCase();
|
|
return errorString.contains('socket') ||
|
|
errorString.contains('network') ||
|
|
errorString.contains('timeout') ||
|
|
errorString.contains('connection');
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Retry for critical operations that should almost never fail
|
|
/// Uses more aggressive retry strategy
|
|
static Future<T> retryCritical<T>({
|
|
required Future<T> Function() operation,
|
|
}) {
|
|
return retry(
|
|
operation: operation,
|
|
maxAttempts: 5,
|
|
initialDelaySeconds: 1,
|
|
maxDelaySeconds: 10,
|
|
);
|
|
}
|
|
}
|