fix: cooldown resume health probes, debounce connectivity, coalesce concurrent checks
Add per-platform cooldown on resume health probes (10s mobile, 2min desktop). Debounce connectivity listener by 2s. Coalesce concurrent checkServerHealth and reconnectOfflineServers into single in-flight futures.
This commit is contained in:
+15
-3
@@ -301,6 +301,9 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
late final DownloadManagerService _downloadManager;
|
||||
late final OfflineWatchSyncService _offlineWatchSyncService;
|
||||
|
||||
/// Last time server health probes ran from a resume event (cooldown for desktop)
|
||||
DateTime _lastResumeProbe = DateTime(0);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -335,9 +338,18 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
// App came back to foreground - trigger sync check and start new session
|
||||
_offlineWatchSyncService.onAppResumed();
|
||||
InAppReviewService.instance.startSession();
|
||||
// Re-probe servers — mobile OS may have dropped TCP connections during doze/sleep
|
||||
_serverManager.checkServerHealth();
|
||||
_serverManager.reconnectOfflineServers();
|
||||
// Re-probe servers — mobile OS may have dropped TCP connections during doze/sleep.
|
||||
// On desktop, resumed fires on every window focus (alt-tab), so apply a cooldown
|
||||
// to avoid piling up network probes from rapid alt-tabbing.
|
||||
final now = DateTime.now();
|
||||
final cooldown = (Platform.isIOS || Platform.isAndroid)
|
||||
? const Duration(seconds: 10)
|
||||
: const Duration(minutes: 2);
|
||||
if (now.difference(_lastResumeProbe) >= cooldown) {
|
||||
_lastResumeProbe = now;
|
||||
_serverManager.checkServerHealth();
|
||||
_serverManager.reconnectOfflineServers();
|
||||
}
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
// App went to background or is closing - end session
|
||||
|
||||
@@ -38,6 +38,15 @@ class MultiServerManager {
|
||||
/// Debounce timers for endpoint-exhaustion-triggered reconnection (per server)
|
||||
final Map<String, Timer> _reconnectDebounce = {};
|
||||
|
||||
/// Coalescing guard for checkServerHealth — prevents concurrent health checks
|
||||
Future<void>? _activeHealthCheck;
|
||||
|
||||
/// Coalescing guard for reconnectOfflineServers — prevents concurrent reconnect sweeps
|
||||
Future<void>? _activeReconnect;
|
||||
|
||||
/// Debounce timer for connectivity events — collapses rapid network flapping
|
||||
Timer? _connectivityDebounce;
|
||||
|
||||
/// Get all registered server IDs
|
||||
List<String> get serverIds => _servers.keys.toList();
|
||||
|
||||
@@ -292,6 +301,18 @@ class MultiServerManager {
|
||||
/// Uses [PlexClient.isHealthy] which checks for HTTP 200, so servers with
|
||||
/// invalid tokens (401) are correctly reported as offline.
|
||||
Future<void> checkServerHealth() async {
|
||||
// Coalesce concurrent calls — return the in-flight future if one exists
|
||||
if (_activeHealthCheck != null) return _activeHealthCheck!;
|
||||
|
||||
_activeHealthCheck = _doCheckServerHealth();
|
||||
try {
|
||||
await _activeHealthCheck;
|
||||
} finally {
|
||||
_activeHealthCheck = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doCheckServerHealth() async {
|
||||
appLogger.d('Checking health for ${_clients.length} servers');
|
||||
|
||||
final healthChecks = _clients.entries.map((entry) async {
|
||||
@@ -327,18 +348,24 @@ class MultiServerManager {
|
||||
return;
|
||||
}
|
||||
|
||||
appLogger.d(
|
||||
'Connectivity change detected, re-optimizing all servers',
|
||||
error: {
|
||||
'status': status.name,
|
||||
'interfaces': results.map((r) => r.name).toList(),
|
||||
'serverCount': _servers.length,
|
||||
},
|
||||
);
|
||||
// Debounce rapid connectivity events (e.g. WiFi flapping) into a single trigger
|
||||
_connectivityDebounce?.cancel();
|
||||
_connectivityDebounce = Timer(const Duration(seconds: 2), () {
|
||||
_connectivityDebounce = null;
|
||||
|
||||
// Re-optimize all servers and re-probe offline ones
|
||||
_reoptimizeAllServers(reason: 'connectivity:${status.name}');
|
||||
checkServerHealth();
|
||||
appLogger.d(
|
||||
'Connectivity change detected, re-optimizing all servers',
|
||||
error: {
|
||||
'status': status.name,
|
||||
'interfaces': results.map((r) => r.name).toList(),
|
||||
'serverCount': _servers.length,
|
||||
},
|
||||
);
|
||||
|
||||
// Re-optimize all servers and re-probe offline ones
|
||||
_reoptimizeAllServers(reason: 'connectivity:${status.name}');
|
||||
checkServerHealth();
|
||||
});
|
||||
},
|
||||
onError: (error, stackTrace) {
|
||||
appLogger.w('Connectivity listener error', error: error, stackTrace: stackTrace);
|
||||
@@ -353,6 +380,8 @@ class MultiServerManager {
|
||||
void stopNetworkMonitoring() {
|
||||
_connectivitySubscription?.cancel();
|
||||
_connectivitySubscription = null;
|
||||
_connectivityDebounce?.cancel();
|
||||
_connectivityDebounce = null;
|
||||
appLogger.i('Stopped network monitoring');
|
||||
}
|
||||
|
||||
@@ -441,6 +470,18 @@ class MultiServerManager {
|
||||
|
||||
/// Attempt reconnection for all offline servers
|
||||
Future<void> reconnectOfflineServers() async {
|
||||
// Coalesce concurrent calls — return the in-flight future if one exists
|
||||
if (_activeReconnect != null) return _activeReconnect!;
|
||||
|
||||
_activeReconnect = _doReconnectOfflineServers();
|
||||
try {
|
||||
await _activeReconnect;
|
||||
} finally {
|
||||
_activeReconnect = null;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doReconnectOfflineServers() async {
|
||||
final offline = offlineServerIds;
|
||||
if (offline.isEmpty) return;
|
||||
|
||||
@@ -501,6 +542,8 @@ class MultiServerManager {
|
||||
timer.cancel();
|
||||
}
|
||||
_reconnectDebounce.clear();
|
||||
_activeHealthCheck = null;
|
||||
_activeReconnect = null;
|
||||
_clients.clear();
|
||||
_servers.clear();
|
||||
_serverStatus.clear();
|
||||
|
||||
Reference in New Issue
Block a user