fix: connection selection failover and timeout budget

This commit is contained in:
edde746
2026-04-07 06:49:51 +02:00
parent d96848181d
commit 7528dcec71
4 changed files with 30 additions and 51 deletions
+27 -27
View File
@@ -103,7 +103,7 @@ class MultiServerManager {
final baseUrl = workingConnection.uri;
// Create PlexClient with failover support
final prioritizedEndpoints = server.prioritizedEndpointUrls(preferredFirst: cachedEndpoint ?? baseUrl);
final prioritizedEndpoints = server.prioritizedEndpointUrls(preferredFirst: baseUrl);
final config = await PlexConfig.create(
baseUrl: baseUrl,
token: server.accessToken,
@@ -131,6 +131,18 @@ class MultiServerManager {
return client;
}
/// Persists a new endpoint, rebuilds the failover list, and switches the client.
Future<void> _promoteEndpoint({
required PlexClient client,
required PlexServer server,
required StorageService storage,
required String newUrl,
}) async {
await storage.saveServerEndpoint(server.clientIdentifier, newUrl);
final newEndpoints = server.prioritizedEndpointUrls(preferredFirst: newUrl);
await client.updateEndpointPreferences(newEndpoints, switchToFirst: true);
}
/// Continues draining the connection optimization stream in the background,
/// switching the client to any better endpoint found.
void _drainOptimizationStream(
@@ -139,8 +151,6 @@ class MultiServerManager {
required PlexServer server,
required StorageService storage,
}) {
final serverId = server.clientIdentifier;
() async {
try {
while (await streamIterator.moveNext()) {
@@ -157,9 +167,7 @@ class MultiServerManager {
error: {'from': client.config.baseUrl, 'to': newUrl, 'type': connection.displayType},
);
await storage.saveServerEndpoint(serverId, newUrl);
final newEndpoints = server.prioritizedEndpointUrls(preferredFirst: newUrl);
await client.updateEndpointPreferences(newEndpoints, switchToFirst: true);
await _promoteEndpoint(client: client, server: server, storage: storage, newUrl: newUrl);
}
} catch (e, stackTrace) {
appLogger.w('Background connection optimization failed for ${server.name}', error: e, stackTrace: stackTrace);
@@ -174,7 +182,7 @@ class MultiServerManager {
Future<int> connectToAllServers(
List<PlexServer> servers, {
String? clientIdentifier,
Duration timeout = ConnectionTimeouts.connectAll,
Duration timeout = ConnectionTimeouts.perServerConnect,
Function(String serverId, PlexClient client)? onServerConnected,
Function(String serverId, Object error)? onServerFailed,
}) async {
@@ -190,14 +198,15 @@ class MultiServerManager {
final effectiveClientId = clientIdentifier ?? DateTime.now().millisecondsSinceEpoch.toString();
_clientIdentifier = effectiveClientId;
// Create connection tasks for all servers
// Create connection tasks for all servers (timeout is inside each task
// so a timed-out task cannot keep mutating manager state).
final connectionFutures = servers.map((server) async {
final serverId = server.clientIdentifier;
try {
appLogger.d('Attempting connection to server: ${server.name}');
final client = await _createClientForServer(server: server, clientIdentifier: effectiveClientId);
final client = await _createClientForServer(server: server, clientIdentifier: effectiveClientId).timeout(timeout);
// Store the client and server info
_clients[serverId] = client;
@@ -208,6 +217,12 @@ class MultiServerManager {
appLogger.i('Successfully connected to ${server.name}');
return serverId;
} on TimeoutException {
appLogger.w('Server connection timed out for ${server.name}');
_servers[serverId] = server;
_serverStatus[serverId] = false;
onServerFailed?.call(serverId, TimeoutException('Connection to ${server.name} timed out'));
return null;
} catch (e, stackTrace) {
appLogger.e('Failed to connect to ${server.name}', error: e, stackTrace: stackTrace);
@@ -220,18 +235,7 @@ class MultiServerManager {
}
});
// Wait for all connections with timeout
final results = await Future.wait(
connectionFutures.map(
(f) => f.timeout(
timeout,
onTimeout: () {
appLogger.w('Server connection timed out');
return null;
},
),
),
);
final results = await Future.wait(connectionFutures);
// Count successful connections
final successCount = results.where((id) => id != null).length;
@@ -432,15 +436,11 @@ class MultiServerManager {
continue;
}
// Save the new endpoint
await storage.saveServerEndpoint(serverId, newUrl);
// Actively switch the running client to the better endpoint
if (client != null) {
final newEndpoints = server.prioritizedEndpointUrls(preferredFirst: newUrl);
await client.updateEndpointPreferences(newEndpoints, switchToFirst: true);
await _promoteEndpoint(client: client, server: server, storage: storage, newUrl: newUrl);
appLogger.i('Switched ${server.name} to better endpoint: $newUrl', error: {'type': connection.displayType});
} else {
await storage.saveServerEndpoint(serverId, newUrl);
appLogger.i('Updated optimal endpoint for ${server.name}: $newUrl', error: {'type': connection.displayType});
}
}
-20
View File
@@ -365,26 +365,6 @@ class PlexServer {
/// Check if server is online using the presence field
bool get isOnline => presence;
PlexConnection? _selectBest(Iterable<PlexConnection> candidates) {
final local = candidates.where((c) => c.local && !c.relay).toList();
if (local.isNotEmpty) return local.first;
final remote = candidates.where((c) => !c.local && !c.relay).toList();
if (remote.isNotEmpty) return remote.first;
final relay = candidates.where((c) => c.relay).toList();
if (relay.isNotEmpty) return relay.first;
if (candidates.isNotEmpty) return candidates.first;
return null;
}
/// Get the best connection URL
/// Priority: local > remote > relay
PlexConnection? getBestConnection() {
return _selectBest(connections);
}
/// Find the best working connection by testing them
/// Returns a Stream that emits connections progressively:
/// 1. First emission: The first connection that responds successfully
@@ -36,7 +36,7 @@ class ServerConnectionOrchestrator {
required LibrariesProvider librariesProvider,
required OfflineWatchSyncService syncService,
String? clientIdentifier,
Duration timeout = ConnectionTimeouts.connectAll,
Duration timeout = ConnectionTimeouts.perServerConnect,
void Function(String serverId, bool success)? onServerStatus,
}) async {
appLogger.i('Connecting to ${servers.length} servers...');
+2 -3
View File
@@ -11,9 +11,8 @@ class ConnectionTimeouts {
/// Dio connect timeout for individual HTTP requests to a Plex server.
static const connect = Duration(seconds: 10);
/// Timeout for [MultiServerManager.connectToAllServers] — the maximum time
/// to wait for each server's connection future.
static const connectAll = Duration(seconds: 4);
/// Per-server connection budget: preferred probe + race + HTTPS upgrade attempt + 1s buffer.
static const perServerConnect = Duration(milliseconds: 1500 + 2000 + 2000 + 1000);
/// Dio receive timeout for streaming/large responses from a Plex server.
static const receive = Duration(seconds: 120);