Improve Automatic Node Switching (#2596)

* feat: Enhance node switching logic with connectivity checks and attempt limits - Added connectivity checks before node health verification to ensure network availability. - Introduced maximum node switching attempts and cooldown periods to prevent excessive retries. * refactor: Improve node switch PR comments - Reduce cooldown time to 15 seconds - Combine increment code into one within the finally block --------- Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

David Adegoke committed Oct 25, 2025 at 12:34 UTC c457a7fea6938d5db949681a156f3e9ab35c47f8
1 file changed +83 -18
lib/core/node_switching_service.dart
+83 -18
@@ -5,6 +5,7 @@ import 'package:cake_wallet/store/app_store.dart';
5 import 'package:cake_wallet/store/settings_store.dart';
6 import 'package:cw_core/utils/print_verbose.dart';
7 import 'package:hive/hive.dart';
8 +import 'package:connectivity_plus/connectivity_plus.dart';
9
10 class NodeSwitchingService {
11 NodeSwitchingService({
@@ -15,12 +16,16 @@ class NodeSwitchingService {
16
17 static const int _healthCheckIntervalSeconds = 30;
18
18 - // The number of times we want to reset the overall used trusted nodes list.
19 - // We don't want an infinite loop if all trusted nodes are down.
20 - static const int _usedTrustedNodeResetCount = 1;
19 + // Maximum number of node switching attempts per session
20 + static const int _maxNodeSwitchingAttempts = 5;
21
22 - // State to manage the reset count
23 - int _resetCount = 0;
22 + // Cooldown period between node switching attempts (in seconds)
23 + static const int _nodeSwitchingCooldownSeconds = 15;
24 +
25 + // State to manage switching attempts and cooldown
26 + int _switchingAttempts = 0;
27 + DateTime? _lastSwitchingAttempt;
28 + bool _hasExhaustedAllNodes = false;
29
30 String walletName = '';
31
@@ -58,11 +63,45 @@ class NodeSwitchingService {
63
64 if (_isSwitching) return;
65
66 + // Reset counters when wallet changes
67 + if (walletName.isNotEmpty && walletName != appStore.wallet!.name) {
68 + _resetSwitchingState();
69 + }
70 + walletName = appStore.wallet!.name;
71 +
72 + // Check if we've exhausted all switching attempts
73 + if (_hasExhaustedAllNodes) {
74 + printV('Node switching exhausted for wallet: $walletName. Skipping health check.');
75 + return;
76 + }
77 +
78 + // Check cooldown period
79 + if (_lastSwitchingAttempt != null) {
80 + final timeSinceLastAttempt = DateTime.now().difference(_lastSwitchingAttempt!);
81 + if (timeSinceLastAttempt.inSeconds < _nodeSwitchingCooldownSeconds) {
82 + printV('Node switching in cooldown period. Skipping health check.');
83 + return;
84 + }
85 + }
86 +
87 try {
88 + // Check network connectivity first
89 + final connectivityResult = await Connectivity().checkConnectivity();
90 + if (connectivityResult == ConnectivityResult.none) {
91 + printV('No network connectivity detected. Skipping node health check.');
92 + return;
93 + }
94 +
95 final isHealthy = await appStore.wallet!.checkNodeHealth();
96
97 if (!isHealthy) {
98 + printV('Node health check failed. Attempting to switch to next trusted node.');
99 await _switchToNextTrustedNode();
100 + } else {
101 + // Reset switching attempts on successful health check
102 + _switchingAttempts = 0;
103 + _hasExhaustedAllNodes = false;
104 + printV('Node health check passed. Current node is healthy.');
105 }
106 } catch (e) {
107 printV('Error during health check: $e');
@@ -73,11 +112,15 @@ class NodeSwitchingService {
112 Future<void> _switchToNextTrustedNode() async {
113 _isSwitching = true;
114
76 - if (walletName.isNotEmpty && (walletName != appStore.wallet!.name)) _resetCount = 0;
77 -
78 - walletName = appStore.wallet!.name;
79 -
115 try {
116 + // Check if we've exceeded maximum switching attempts
117 + if (_switchingAttempts >= _maxNodeSwitchingAttempts) {
118 + printV('Maximum node switching attempts ($_maxNodeSwitchingAttempts) reached. '
119 + 'Disabling automatic switching.');
120 + _hasExhaustedAllNodes = true;
121 + return;
122 + }
123 +
124 final walletType = appStore.wallet!.type;
125 final currentNode = settingsStore.getCurrentNode(walletType);
126
@@ -88,6 +131,7 @@ class NodeSwitchingService {
131
132 if (trustedNodes.isEmpty) {
133 printV('No trusted nodes available for switching');
134 + _hasExhaustedAllNodes = true;
135 return;
136 }
137
@@ -108,21 +152,27 @@ class NodeSwitchingService {
152 }
153 }
154
111 - // If all trusted nodes have been used, reset the list and start over
155 + // If all trusted nodes have been used, check if we should reset
156 if (nextNode == null) {
113 - printV('All trusted nodes have been tried, resetting and starting over');
114 - _resetCount++;
115 -
116 - if (_resetCount > _usedTrustedNodeResetCount) return;
117 -
118 - _usedNodeKeys[walletType]!.clear();
119 - nextNode = trustedNodes.first;
157 + printV('All trusted nodes have been tried for wallet type: $walletType');
158 +
159 + // If we've tried all nodes and still haven't reached max attempts, reset and try again
160 + if (_switchingAttempts < _maxNodeSwitchingAttempts) {
161 + printV('Resetting used nodes list and trying again');
162 + _usedNodeKeys[walletType]!.clear();
163 + nextNode = trustedNodes.first;
164 + } else {
165 + printV('Maximum attempts reached. No more node switching.');
166 + _hasExhaustedAllNodes = true;
167 + return;
168 + }
169 }
170
171 // Add the next node to used list
172 _usedNodeKeys[walletType]!.add(nextNode.key);
173
125 - printV('Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw}');
174 + printV(
175 + 'Switching from ${currentNode.uriRaw} to ${nextNode.uriRaw} (attempt $_switchingAttempts/$_maxNodeSwitchingAttempts)');
176 printV('Used nodes for ${walletType}: ${_usedNodeKeys[walletType]}');
177
178 // Update the current node in settings
@@ -137,7 +187,22 @@ class NodeSwitchingService {
187 } catch (e) {
188 printV('Error switching to next trusted node: $e');
189 } finally {
190 + // Increment switching attempts counter on every attempt
191 + _switchingAttempts++;
192 + _lastSwitchingAttempt = DateTime.now();
193 _isSwitching = false;
194 }
195 }
196 +
197 + /// Reset switching state when wallet changes
198 + void _resetSwitchingState() {
199 + _switchingAttempts = 0;
200 + _lastSwitchingAttempt = null;
201 + _hasExhaustedAllNodes = false;
202 + _usedNodeKeys.clear();
203 + printV('Node switching state reset for new wallet');
204 + }
205 +
206 + /// Check if automatic node switching is currently disabled due to exhaustion
207 + bool get isAutomaticSwitchingDisabled => _hasExhaustedAllNodes;
208 }