Skip to content

Commit ef91497

Browse files
committed
feat: maintain automatic node pool
1 parent 279a415 commit ef91497

8 files changed

Lines changed: 791 additions & 140 deletions

File tree

‎src-tauri/src/core.rs‎

Lines changed: 685 additions & 91 deletions
Large diffs are not rendered by default.

‎src-tauri/src/git.rs‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -190,21 +190,25 @@ pub fn choose_node<'a>(nodes: &'a [(NodeDefinition, HealthSummary)]) -> Option<&
190190
let mut candidates: Vec<_> = nodes
191191
.iter()
192192
.filter(|(node, health)| {
193-
node.enabled && matches!(health.status, NodeStatus::Available | NodeStatus::Slow)
193+
node.enabled
194+
&& health.in_auto_pool
195+
&& matches!(health.status, NodeStatus::Available | NodeStatus::Slow)
194196
})
195197
.collect();
196-
candidates.sort_by_key(|(_, health)| {
197-
let failure_penalty = u64::from(health.consecutive_failures) * 100_000;
198-
let success_rate_penalty = if health.attempt_count == 0 {
199-
100_000
200-
} else {
201-
100_000 - (u64::from(health.success_count) * 100_000 / u64::from(health.attempt_count))
202-
};
203-
failure_penalty + success_rate_penalty + health.median_latency_ms.unwrap_or(u64::MAX / 4)
204-
});
198+
candidates.sort_by_key(|(_, health)| health_score(health));
205199
candidates.first().map(|(node, _)| node)
206200
}
207201

202+
pub fn health_score(health: &HealthSummary) -> u64 {
203+
let failure_penalty = u64::from(health.consecutive_failures) * 100_000;
204+
let success_rate_penalty = if health.attempt_count == 0 {
205+
100_000
206+
} else {
207+
100_000 - (u64::from(health.success_count) * 100_000 / u64::from(health.attempt_count))
208+
};
209+
failure_penalty + success_rate_penalty + health.median_latency_ms.unwrap_or(u64::MAX / 4)
210+
}
211+
208212
pub fn build_config(
209213
settings: &Settings,
210214
node: Option<&NodeDefinition>,

‎src-tauri/src/lib.rs‎

Lines changed: 73 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ mod usage;
99
use crate::{core::AppCore, models::*};
1010
use std::{
1111
path::{Path, PathBuf},
12-
sync::atomic::{AtomicU64, Ordering},
12+
sync::atomic::{AtomicBool, AtomicU64, Ordering},
1313
time::{Duration, Instant},
1414
};
1515
use tauri::{
@@ -80,7 +80,25 @@ where
8080
}
8181

8282
fn emit_node_test_progress(app: &tauri::AppHandle, completed: usize, total: usize) {
83-
let _ = app.emit("node-test-progress", NodeTestProgress { completed, total });
83+
let _ = app.emit(
84+
"node-test-progress",
85+
NodeTestProgress {
86+
completed,
87+
total,
88+
finished: false,
89+
},
90+
);
91+
}
92+
93+
fn emit_node_test_finished(app: &tauri::AppHandle) {
94+
let _ = app.emit(
95+
"node-test-progress",
96+
NodeTestProgress {
97+
completed: 0,
98+
total: 0,
99+
finished: true,
100+
},
101+
);
84102
}
85103

86104
#[tauri::command]
@@ -122,23 +140,18 @@ async fn test_all_nodes(app: tauri::AppHandle) -> CommandResult<Vec<NodeEntry>>
122140
emit_node_test_progress(&progress_app, completed, total);
123141
})
124142
})
125-
.await?;
143+
.await;
144+
emit_node_test_finished(&app);
145+
let result = result?;
126146
refresh_tray_from_core(&app);
127147
Ok(result)
128148
}
129149

130150
#[tauri::command]
131151
async fn refresh_system_nodes(app: tauri::AppHandle) -> CommandResult<bool> {
132-
let progress_app = app.clone();
133152
let worker_app = app.clone();
134-
let result = run_node_test(move || {
135-
worker_app
136-
.state::<AppCore>()
137-
.refresh_system_nodes_with_progress(|completed, total| {
138-
emit_node_test_progress(&progress_app, completed, total);
139-
})
140-
})
141-
.await?;
153+
let result =
154+
run_node_test(move || worker_app.state::<AppCore>().refresh_system_nodes()).await?;
142155
refresh_tray_from_core(&app);
143156
Ok(result)
144157
}
@@ -347,6 +360,7 @@ fn install_tray(app: &tauri::App) -> tauri::Result<()> {
347360
.test_all_nodes_or_join_with_progress(|completed, total| {
348361
emit_node_test_progress(&handle, completed, total)
349362
});
363+
emit_node_test_finished(&handle);
350364
match result {
351365
Ok(_) => {
352366
if let Ok(state) = handle.state::<AppCore>().snapshot() {
@@ -372,33 +386,45 @@ fn install_tray(app: &tauri::App) -> tauri::Result<()> {
372386
fn start_health_monitor(app: tauri::AppHandle) {
373387
tauri::async_runtime::spawn(async move {
374388
let mut last_check = Instant::now();
389+
let mut initial_discovery_pending = true;
375390
loop {
376391
tokio::time::sleep(Duration::from_secs(60)).await;
377-
let minutes = app
378-
.state::<AppCore>()
392+
let core = app.state::<AppCore>();
393+
let minutes = core
379394
.snapshot()
380395
.map(|state| state.settings.health_check_minutes)
381396
.unwrap_or(0);
382-
if minutes == 0 || last_check.elapsed() < Duration::from_secs(u64::from(minutes) * 60) {
397+
let needs_initial_discovery = initial_discovery_pending
398+
&& core.needs_background_node_discovery().unwrap_or(false);
399+
if !health_check_due(minutes, last_check.elapsed(), needs_initial_discovery) {
383400
continue;
384401
}
385-
last_check = Instant::now();
386402
let handle = app.clone();
387-
let _ = tauri::async_runtime::spawn_blocking(move || {
388-
if handle
403+
let succeeded = tauri::async_runtime::spawn_blocking(move || {
404+
let started = AtomicBool::new(false);
405+
let result = handle
389406
.state::<AppCore>()
390-
.test_all_nodes_with_progress(|completed, total| {
407+
.test_background_nodes_with_progress(|completed, total| {
408+
started.store(true, Ordering::Relaxed);
391409
emit_node_test_progress(&handle, completed, total);
392-
})
393-
.is_ok()
394-
{
410+
});
411+
if started.load(Ordering::Relaxed) {
412+
emit_node_test_finished(&handle);
413+
}
414+
if result.is_ok() {
395415
if let Ok(state) = handle.state::<AppCore>().snapshot() {
396416
refresh_tray(&handle, &state);
397417
let _ = handle.emit("snapshot-updated", state);
398418
}
399419
}
420+
result.is_ok()
400421
})
401-
.await;
422+
.await
423+
.unwrap_or(false);
424+
if succeeded {
425+
last_check = Instant::now();
426+
initial_discovery_pending = false;
427+
}
402428
}
403429
});
404430
}
@@ -409,9 +435,7 @@ fn start_system_node_monitor(app: tauri::AppHandle) {
409435
let handle = app.clone();
410436
let _ = tauri::async_runtime::spawn_blocking(move || {
411437
let core = handle.state::<AppCore>();
412-
if let Err(error) = core.refresh_system_nodes_with_progress(|completed, total| {
413-
emit_node_test_progress(&handle, completed, total);
414-
}) {
438+
if let Err(error) = core.refresh_system_nodes() {
415439
core.system_node_refresh_failed(&error);
416440
}
417441
if let Ok(state) = core.snapshot() {
@@ -425,6 +449,11 @@ fn start_system_node_monitor(app: tauri::AppHandle) {
425449
});
426450
}
427451

452+
fn health_check_due(minutes: u32, elapsed: Duration, needs_initial_discovery: bool) -> bool {
453+
minutes != 0
454+
&& (needs_initial_discovery || elapsed >= Duration::from_secs(u64::from(minutes) * 60))
455+
}
456+
428457
pub fn run() {
429458
let autostart = tauri_plugin_autostart::Builder::new();
430459
#[cfg(target_os = "macos")]
@@ -488,8 +517,9 @@ pub fn run() {
488517

489518
#[cfg(test)]
490519
mod tests {
491-
use super::{run_node_test, tray_labels};
520+
use super::{health_check_due, run_node_test, tray_labels};
492521
use crate::models::{HealthSummary, NodeDefinition, NodeEntry, Settings};
522+
use std::time::Duration;
493523

494524
#[test]
495525
fn node_tests_run_off_the_calling_thread() {
@@ -529,4 +559,20 @@ mod tests {
529559
("当前线路:GitHub 直连".into(), "开启加速")
530560
);
531561
}
562+
563+
#[test]
564+
fn health_check_schedule_honors_interval_and_disabled_setting() {
565+
assert!(!health_check_due(0, Duration::from_secs(86_400), true));
566+
assert!(!health_check_due(
567+
24 * 60,
568+
Duration::from_secs(6 * 60 * 60),
569+
false
570+
));
571+
assert!(health_check_due(
572+
8 * 60,
573+
Duration::from_secs(8 * 60 * 60),
574+
false
575+
));
576+
assert!(health_check_due(24 * 60, Duration::from_secs(60), true));
577+
}
532578
}

‎src-tauri/src/models.rs‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
44
pub const SCHEMA_VERSION: u32 = 1;
55
pub const FASTGIT_REWRITE_BASE: &str = "https://fastgit.cc/https://github.com/";
66
pub const TEST_REPOSITORY: &str = "https://github.com/octocat/Hello-World.git";
7+
pub const DEFAULT_HEALTH_CHECK_MINUTES: u32 = 8 * 60;
78

89
#[derive(Debug, Clone, Serialize, Deserialize)]
910
#[serde(rename_all = "camelCase")]
@@ -35,7 +36,7 @@ impl Default for Settings {
3536
line_mode: LineMode::Automatic,
3637
fixed_node_id: None,
3738
current_node_id: None,
38-
health_check_minutes: 30,
39+
health_check_minutes: DEFAULT_HEALTH_CHECK_MINUTES,
3940
launch_at_login: false,
4041
log_level: "info".into(),
4142
usage_logging_enabled: true,
@@ -96,6 +97,7 @@ impl NodeDefinition {
9697
#[serde(rename_all = "camelCase")]
9798
pub struct HealthSummary {
9899
pub status: NodeStatus,
100+
pub in_auto_pool: bool,
99101
pub success_count: u32,
100102
pub attempt_count: u32,
101103
pub median_latency_ms: Option<u64>,
@@ -110,6 +112,7 @@ impl Default for HealthSummary {
110112
fn default() -> Self {
111113
Self {
112114
status: NodeStatus::Untested,
115+
in_auto_pool: false,
113116
success_count: 0,
114117
attempt_count: 0,
115118
median_latency_ms: None,
@@ -134,6 +137,7 @@ pub struct NodeEntry {
134137
pub struct NodeTestProgress {
135138
pub completed: usize,
136139
pub total: usize,
140+
pub finished: bool,
137141
}
138142

139143
#[derive(Debug, Clone, Serialize, Deserialize)]

0 commit comments

Comments
 (0)