Perf: Erreichbarkeitspruefung parallelisiert, lokale IP nur einmal aufgeloest
watch_once() rief reconcile_pair() sequenziell fuer jedes Paar auf - bei mehreren/langsamen Paaren (Ping-/curl-Timeouts von mehreren Sekunden pro Seite) wuchs die Laufzeit eines watch-Durchlaufs dadurch linear mit der Anzahl konfigurierter Paare, obwohl die pro-Paar-Sperre (siehe lock.rs) genau fuer parallele Reconciles gedacht ist. Die Erreichbarkeitspruefungen laufen jetzt ueber spawn_blocking parallel; das eigentliche Mounten/ Aushaengen bleibt sequenziell (teilt sich CredentialStore/Dateizustand). Ausserdem loeste ein Reconcile-Durchlauf fuer ein MAC-adressiertes Paar dieselbe Adresse bislang zweimal ueber mac2ip auf: einmal fuer die Erreichbarkeitspruefung (check_local_reachable), ein zweites Mal beim tatsaechlichen Mounten (build_target -> local_source). check_local_reachable gibt die aufgeloeste IP jetzt zurueck und reicht sie bis zu mount_side() durch (target::build_target_with_cached_local_ip); status.rs loest die lokale Adresse ebenfalls nur noch einmal pro Paar auf und nutzt sie fuer Quelle UND Erreichbarkeitsanzeige. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+17
-17
@@ -43,14 +43,28 @@ pub async fn run(name: Option<String>, json: bool) -> anyhow::Result<()> {
|
||||
Some(Side::Cloud) => Some("cloud"),
|
||||
None => None,
|
||||
};
|
||||
// Die lokale Adresse (bei MAC-Adressierung ein `mac2ip`-Subprozessaufruf) wird nur
|
||||
// einmal pro Paar aufgelöst und für Quelle UND Erreichbarkeitsprüfung
|
||||
// wiederverwendet, statt sie für beide Zwecke unabhängig voneinander ein zweites
|
||||
// Mal aufzulösen.
|
||||
let resolved_local_ip =
|
||||
smart_mount::network::address::resolve_ip(&pair.local.address, &cfg.settings);
|
||||
let local_source = resolved_local_ip
|
||||
.as_ref()
|
||||
.map(|ip| target::format_local_source(&pair.local, *ip))
|
||||
.unwrap_or_else(|_| "unresolved".to_string());
|
||||
let local_reachable = resolved_local_ip
|
||||
.as_ref()
|
||||
.map(|ip| network::is_reachable(&ip.to_string()))
|
||||
.unwrap_or(false);
|
||||
|
||||
PairStatus {
|
||||
id: pair.id.clone(),
|
||||
name: pair.name.clone(),
|
||||
mount_point: pair.mount_point.display().to_string(),
|
||||
active,
|
||||
local_source: target::local_source(&pair.local, &cfg.settings)
|
||||
.unwrap_or_else(|_| "unresolved".to_string()),
|
||||
local_reachable: local_reachable(&pair.local, &cfg.settings),
|
||||
local_source,
|
||||
local_reachable,
|
||||
cloud_source: target::cloud_source(&pair.cloud),
|
||||
cloud_reachable: network::is_reachable(&pair.cloud.host_or_url),
|
||||
}
|
||||
@@ -88,17 +102,3 @@ fn reachable_str(reachable: bool) -> &'static str {
|
||||
"unreachable"
|
||||
}
|
||||
}
|
||||
|
||||
/// `false` wenn die Adresse (MAC->IP) gar nicht erst aufgelöst werden kann, statt den
|
||||
/// Platzhalter-String `"unresolved"` an `network::is_reachable` weiterzureichen - das würde
|
||||
/// dort einen sinnlosen `ping unresolved`-Aufruf gegen einen nicht existierenden Hostnamen
|
||||
/// auslösen statt korrekt "nicht erreichbar" zu melden.
|
||||
fn local_reachable(
|
||||
local: &smart_mount::config::LocalSide,
|
||||
settings: &smart_mount::config::GlobalSettings,
|
||||
) -> bool {
|
||||
match smart_mount::network::address::resolve_ip(&local.address, settings) {
|
||||
Ok(ip) => network::is_reachable(&ip.to_string()),
|
||||
Err(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
+27
-7
@@ -45,6 +45,20 @@ pub fn build_target(
|
||||
pair: &DrivePair,
|
||||
settings: &GlobalSettings,
|
||||
side: Side,
|
||||
) -> Result<MountTarget> {
|
||||
build_target_with_cached_local_ip(pair, settings, side, None)
|
||||
}
|
||||
|
||||
/// Wie [`build_target`], nimmt aber optional eine bereits aufgelöste lokale IP entgegen
|
||||
/// (`Side::Local` mit `LocalAddress::Mac`), um eine zweite `mac2ip`-Subprozess-Auflösung
|
||||
/// derselben Adresse innerhalb desselben Reconcile-Durchlaufs zu vermeiden (siehe
|
||||
/// [`crate::reconcile`], das die Erreichbarkeit ohnehin schon per `address::resolve_ip` prüft,
|
||||
/// bevor es ggf. auf diese Seite umschaltet). Wird für `Side::Cloud` ignoriert.
|
||||
pub fn build_target_with_cached_local_ip(
|
||||
pair: &DrivePair,
|
||||
settings: &GlobalSettings,
|
||||
side: Side,
|
||||
cached_local_ip: Option<std::net::Ipv4Addr>,
|
||||
) -> Result<MountTarget> {
|
||||
let invocation = match pair.context {
|
||||
MountContext::System => MountInvocation::Direct,
|
||||
@@ -54,7 +68,10 @@ pub fn build_target(
|
||||
|
||||
let (source, mut options) = match side {
|
||||
Side::Local => (
|
||||
local_source(&pair.local, settings)?,
|
||||
match cached_local_ip {
|
||||
Some(ip) => format_local_source(&pair.local, ip),
|
||||
None => local_source(&pair.local, settings)?,
|
||||
},
|
||||
parse_options(&pair.local.extra_options),
|
||||
),
|
||||
Side::Cloud => (
|
||||
@@ -169,14 +186,17 @@ fn run_id(username: &str, flag: &str) -> Result<u32> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Löst die konfigurierte lokale Adresse auf und formatiert die Mount-Quelle daraus. Gibt
|
||||
/// einen Fehler zurück, statt eine fehlgeschlagene Auflösung (z. B. `mac2ip` findet die
|
||||
/// MAC-Adresse nicht) hinter dem Platzhalter-String `"unresolved"` zu verstecken - Aufrufer
|
||||
/// sollen einen echten Auflösungsfehler von einer tatsächlich formatierten, aber unerreichbaren
|
||||
/// Quelle unterscheiden können.
|
||||
pub fn local_source(local: &LocalSide, settings: &GlobalSettings) -> Result<String> {
|
||||
let ip = address::resolve_ip(&local.address, settings)?;
|
||||
Ok(format_source(local.kind, &ip.to_string(), &local.share))
|
||||
Ok(format_local_source(local, ip))
|
||||
}
|
||||
|
||||
/// Formatiert die Mount-Quelle für eine bereits aufgelöste lokale IP, ohne selbst erneut
|
||||
/// aufzulösen - für Aufrufer, die die IP ohnehin schon für eine andere Prüfung (z. B.
|
||||
/// Erreichbarkeit) aufgelöst haben und damit eine zweite `mac2ip`-Auflösung derselben
|
||||
/// MAC-Adresse vermeiden wollen.
|
||||
pub fn format_local_source(local: &LocalSide, ip: std::net::Ipv4Addr) -> String {
|
||||
format_source(local.kind, &ip.to_string(), &local.share)
|
||||
}
|
||||
|
||||
pub fn cloud_source(cloud: &CloudSide) -> String {
|
||||
|
||||
+92
-14
@@ -12,6 +12,8 @@
|
||||
//! neue Seite zuerst, flippt dann den Symlink, und hängt erst danach die alte Seite aus - so
|
||||
//! gibt es nie ein Zeitfenster, in dem der sichtbare Pfad auf nichts Gemountetes zeigt.
|
||||
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use crate::config::{AppConfig, DrivePair, GlobalSettings, LocalSide};
|
||||
use crate::db::credentials::{CredentialStore, Side};
|
||||
use crate::error::{Error, Result};
|
||||
@@ -37,10 +39,45 @@ pub struct ReconcileOutcome {
|
||||
}
|
||||
|
||||
/// Führt `reconcile_pair` für jedes aktivierte Paar in `cfg` aus.
|
||||
///
|
||||
/// Die Erreichbarkeitsprüfungen (Ping/`curl`-Subprozesse, bis zu mehrere Sekunden pro Seite)
|
||||
/// sind rein lesend und unabhängig voneinander - sie laufen daher parallel über
|
||||
/// `spawn_blocking` statt sequenziell, damit die Gesamtlaufzeit eines `watch`-Durchlaufs bei
|
||||
/// vielen/langsamen Paaren nicht linear mit deren Anzahl wächst. Das eigentliche Mounten/
|
||||
/// Aushängen bleibt dagegen sequenziell: es teilt sich den `CredentialStore` und schreibt
|
||||
/// Zustand (Symlinks, Credential-/Secrets-Dateien) - die Pro-Paar-Sperre (siehe [`lock`])
|
||||
/// verhindert zwar Kollisionen, unbegrenzt paralleler DB-/Dateizugriff wäre aber unnötiges
|
||||
/// Risiko für einen Effizienzgewinn, den die Reachability-Parallelisierung bereits liefert.
|
||||
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
|
||||
let mut outcomes = Vec::with_capacity(cfg.pairs.len());
|
||||
for pair in cfg.pairs.iter().filter(|p| p.enabled) {
|
||||
outcomes.push(reconcile_pair(pair, &cfg.settings, creds).await);
|
||||
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
|
||||
|
||||
let mut checks = Vec::with_capacity(enabled.len());
|
||||
for pair in &enabled {
|
||||
let local = pair.local.clone();
|
||||
let cloud_host = pair.cloud.host_or_url.clone();
|
||||
let settings = cfg.settings.clone();
|
||||
checks.push(tokio::task::spawn_blocking(move || {
|
||||
let (local_reachable, local_ip) = check_local_reachable(&local, &settings);
|
||||
let cloud_reachable = network::is_reachable(&cloud_host);
|
||||
(local_reachable, local_ip, cloud_reachable)
|
||||
}));
|
||||
}
|
||||
|
||||
let mut outcomes = Vec::with_capacity(enabled.len());
|
||||
for (pair, check) in enabled.iter().zip(checks) {
|
||||
let (local_reachable, local_ip, cloud_reachable) =
|
||||
check.await.unwrap_or((false, None, false));
|
||||
outcomes.push(
|
||||
reconcile_pair_checked(
|
||||
pair,
|
||||
&cfg.settings,
|
||||
creds,
|
||||
local_reachable,
|
||||
local_ip,
|
||||
cloud_reachable,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
}
|
||||
outcomes
|
||||
}
|
||||
@@ -51,11 +88,41 @@ pub async fn reconcile_pair(
|
||||
pair: &DrivePair,
|
||||
settings: &GlobalSettings,
|
||||
creds: &CredentialStore,
|
||||
) -> ReconcileOutcome {
|
||||
let (local_reachable, local_ip) = check_local_reachable(&pair.local, settings);
|
||||
let cloud_reachable = network::is_reachable(&pair.cloud.host_or_url);
|
||||
reconcile_pair_checked(
|
||||
pair,
|
||||
settings,
|
||||
creds,
|
||||
local_reachable,
|
||||
local_ip,
|
||||
cloud_reachable,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn reconcile_pair_checked(
|
||||
pair: &DrivePair,
|
||||
settings: &GlobalSettings,
|
||||
creds: &CredentialStore,
|
||||
local_reachable: bool,
|
||||
local_ip: Option<Ipv4Addr>,
|
||||
cloud_reachable: bool,
|
||||
) -> ReconcileOutcome {
|
||||
let pair_id = pair.id.clone();
|
||||
let pair_name = pair.name.clone();
|
||||
|
||||
match reconcile_pair_inner(pair, settings, creds).await {
|
||||
match reconcile_pair_inner(
|
||||
pair,
|
||||
settings,
|
||||
creds,
|
||||
local_reachable,
|
||||
local_ip,
|
||||
cloud_reachable,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(action) => ReconcileOutcome {
|
||||
pair_id,
|
||||
pair_name,
|
||||
@@ -73,10 +140,12 @@ async fn reconcile_pair_inner(
|
||||
pair: &DrivePair,
|
||||
settings: &GlobalSettings,
|
||||
creds: &CredentialStore,
|
||||
local_reachable: bool,
|
||||
local_ip: Option<Ipv4Addr>,
|
||||
cloud_reachable: bool,
|
||||
) -> Result<Action> {
|
||||
let _guard = lock::acquire(&pair.id).await?;
|
||||
|
||||
let local_reachable = check_local_reachable(&pair.local, settings);
|
||||
let active = target::active_side(pair);
|
||||
cleanup_orphaned_mounts(pair, settings, active).await;
|
||||
|
||||
@@ -84,7 +153,7 @@ async fn reconcile_pair_inner(
|
||||
if active == Some(Side::Local) {
|
||||
return Ok(Action::NoOp);
|
||||
}
|
||||
switch_to(pair, settings, Side::Local, creds, active).await?;
|
||||
switch_to(pair, settings, Side::Local, creds, active, local_ip).await?;
|
||||
return Ok(if active.is_some() {
|
||||
Action::SwitchedToLocal
|
||||
} else {
|
||||
@@ -92,12 +161,11 @@ async fn reconcile_pair_inner(
|
||||
});
|
||||
}
|
||||
|
||||
let cloud_reachable = network::is_reachable(&pair.cloud.host_or_url);
|
||||
if cloud_reachable {
|
||||
if active == Some(Side::Cloud) {
|
||||
return Ok(Action::NoOp);
|
||||
}
|
||||
switch_to(pair, settings, Side::Cloud, creds, active).await?;
|
||||
switch_to(pair, settings, Side::Cloud, creds, active, local_ip).await?;
|
||||
return Ok(if active.is_some() {
|
||||
Action::SwitchedToCloud
|
||||
} else {
|
||||
@@ -108,10 +176,13 @@ async fn reconcile_pair_inner(
|
||||
Ok(Action::NoOp)
|
||||
}
|
||||
|
||||
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> bool {
|
||||
/// Prüft die Erreichbarkeit der lokalen Seite und gibt dabei - falls erfolgreich aufgelöst -
|
||||
/// die konkrete IP zurück, damit ein nachfolgender `switch_to(..., Side::Local, ...)` dieselbe
|
||||
/// MAC-Adresse nicht ein zweites Mal über `mac2ip` auflösen muss.
|
||||
fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> (bool, Option<Ipv4Addr>) {
|
||||
match address::resolve_ip(&local.address, settings) {
|
||||
Ok(ip) => network::is_reachable(&ip.to_string()),
|
||||
Err(_) => false,
|
||||
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
|
||||
Err(_) => (false, None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +194,11 @@ fn check_local_reachable(local: &LocalSide, settings: &GlobalSettings) -> bool {
|
||||
/// manuell gemountet wurde) bliebe dadurch unbemerkt: `status`/`watch` sähen ihn nie, und er
|
||||
/// würde nie automatisch wieder ausgehängt. Best-effort (nur geloggt, nicht propagiert) - ein
|
||||
/// hier fehlschlagendes Aufräumen darf den eigentlichen Reconcile-Schritt nicht blockieren.
|
||||
async fn cleanup_orphaned_mounts(pair: &DrivePair, settings: &GlobalSettings, active: Option<Side>) {
|
||||
async fn cleanup_orphaned_mounts(
|
||||
pair: &DrivePair,
|
||||
settings: &GlobalSettings,
|
||||
active: Option<Side>,
|
||||
) {
|
||||
for side in [Side::Local, Side::Cloud] {
|
||||
if Some(side) == active {
|
||||
continue;
|
||||
@@ -174,8 +249,9 @@ async fn switch_to(
|
||||
new_side: Side,
|
||||
creds: &CredentialStore,
|
||||
old_active: Option<Side>,
|
||||
cached_local_ip: Option<Ipv4Addr>,
|
||||
) -> Result<()> {
|
||||
mount_side(pair, settings, new_side, creds).await?;
|
||||
mount_side(pair, settings, new_side, creds, cached_local_ip).await?;
|
||||
|
||||
// Falls das Aktivieren des Symlinks fehlschlägt, muss die gerade gemountete `new_side`
|
||||
// wieder ausgehängt werden, statt sie unbemerkt gemountet zu lassen: der nächste
|
||||
@@ -200,8 +276,10 @@ async fn mount_side(
|
||||
settings: &GlobalSettings,
|
||||
side: Side,
|
||||
creds: &CredentialStore,
|
||||
cached_local_ip: Option<Ipv4Addr>,
|
||||
) -> Result<()> {
|
||||
let mount_target = target::build_target(pair, settings, side)?;
|
||||
let mount_target =
|
||||
target::build_target_with_cached_local_ip(pair, settings, side, cached_local_ip)?;
|
||||
std::fs::create_dir_all(&mount_target.mount_point)
|
||||
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user