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
}