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"),
|
Some(Side::Cloud) => Some("cloud"),
|
||||||
None => None,
|
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 {
|
PairStatus {
|
||||||
id: pair.id.clone(),
|
id: pair.id.clone(),
|
||||||
name: pair.name.clone(),
|
name: pair.name.clone(),
|
||||||
mount_point: pair.mount_point.display().to_string(),
|
mount_point: pair.mount_point.display().to_string(),
|
||||||
active,
|
active,
|
||||||
local_source: target::local_source(&pair.local, &cfg.settings)
|
local_source,
|
||||||
.unwrap_or_else(|_| "unresolved".to_string()),
|
local_reachable,
|
||||||
local_reachable: local_reachable(&pair.local, &cfg.settings),
|
|
||||||
cloud_source: target::cloud_source(&pair.cloud),
|
cloud_source: target::cloud_source(&pair.cloud),
|
||||||
cloud_reachable: network::is_reachable(&pair.cloud.host_or_url),
|
cloud_reachable: network::is_reachable(&pair.cloud.host_or_url),
|
||||||
}
|
}
|
||||||
@@ -88,17 +102,3 @@ fn reachable_str(reachable: bool) -> &'static str {
|
|||||||
"unreachable"
|
"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,
|
pair: &DrivePair,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
side: Side,
|
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> {
|
) -> Result<MountTarget> {
|
||||||
let invocation = match pair.context {
|
let invocation = match pair.context {
|
||||||
MountContext::System => MountInvocation::Direct,
|
MountContext::System => MountInvocation::Direct,
|
||||||
@@ -54,7 +68,10 @@ pub fn build_target(
|
|||||||
|
|
||||||
let (source, mut options) = match side {
|
let (source, mut options) = match side {
|
||||||
Side::Local => (
|
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),
|
parse_options(&pair.local.extra_options),
|
||||||
),
|
),
|
||||||
Side::Cloud => (
|
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> {
|
pub fn local_source(local: &LocalSide, settings: &GlobalSettings) -> Result<String> {
|
||||||
let ip = address::resolve_ip(&local.address, settings)?;
|
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 {
|
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
|
//! 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.
|
//! 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::config::{AppConfig, DrivePair, GlobalSettings, LocalSide};
|
||||||
use crate::db::credentials::{CredentialStore, Side};
|
use crate::db::credentials::{CredentialStore, Side};
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
@@ -37,10 +39,45 @@ pub struct ReconcileOutcome {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Führt `reconcile_pair` für jedes aktivierte Paar in `cfg` aus.
|
/// 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> {
|
pub async fn watch_once(cfg: &AppConfig, creds: &CredentialStore) -> Vec<ReconcileOutcome> {
|
||||||
let mut outcomes = Vec::with_capacity(cfg.pairs.len());
|
let enabled: Vec<&DrivePair> = cfg.pairs.iter().filter(|p| p.enabled).collect();
|
||||||
for pair in cfg.pairs.iter().filter(|p| p.enabled) {
|
|
||||||
outcomes.push(reconcile_pair(pair, &cfg.settings, creds).await);
|
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
|
outcomes
|
||||||
}
|
}
|
||||||
@@ -51,11 +88,41 @@ pub async fn reconcile_pair(
|
|||||||
pair: &DrivePair,
|
pair: &DrivePair,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
creds: &CredentialStore,
|
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 {
|
) -> ReconcileOutcome {
|
||||||
let pair_id = pair.id.clone();
|
let pair_id = pair.id.clone();
|
||||||
let pair_name = pair.name.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 {
|
Ok(action) => ReconcileOutcome {
|
||||||
pair_id,
|
pair_id,
|
||||||
pair_name,
|
pair_name,
|
||||||
@@ -73,10 +140,12 @@ async fn reconcile_pair_inner(
|
|||||||
pair: &DrivePair,
|
pair: &DrivePair,
|
||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
creds: &CredentialStore,
|
creds: &CredentialStore,
|
||||||
|
local_reachable: bool,
|
||||||
|
local_ip: Option<Ipv4Addr>,
|
||||||
|
cloud_reachable: bool,
|
||||||
) -> Result<Action> {
|
) -> Result<Action> {
|
||||||
let _guard = lock::acquire(&pair.id).await?;
|
let _guard = lock::acquire(&pair.id).await?;
|
||||||
|
|
||||||
let local_reachable = check_local_reachable(&pair.local, settings);
|
|
||||||
let active = target::active_side(pair);
|
let active = target::active_side(pair);
|
||||||
cleanup_orphaned_mounts(pair, settings, active).await;
|
cleanup_orphaned_mounts(pair, settings, active).await;
|
||||||
|
|
||||||
@@ -84,7 +153,7 @@ async fn reconcile_pair_inner(
|
|||||||
if active == Some(Side::Local) {
|
if active == Some(Side::Local) {
|
||||||
return Ok(Action::NoOp);
|
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() {
|
return Ok(if active.is_some() {
|
||||||
Action::SwitchedToLocal
|
Action::SwitchedToLocal
|
||||||
} else {
|
} 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 cloud_reachable {
|
||||||
if active == Some(Side::Cloud) {
|
if active == Some(Side::Cloud) {
|
||||||
return Ok(Action::NoOp);
|
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() {
|
return Ok(if active.is_some() {
|
||||||
Action::SwitchedToCloud
|
Action::SwitchedToCloud
|
||||||
} else {
|
} else {
|
||||||
@@ -108,10 +176,13 @@ async fn reconcile_pair_inner(
|
|||||||
Ok(Action::NoOp)
|
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) {
|
match address::resolve_ip(&local.address, settings) {
|
||||||
Ok(ip) => network::is_reachable(&ip.to_string()),
|
Ok(ip) => (network::is_reachable(&ip.to_string()), Some(ip)),
|
||||||
Err(_) => false,
|
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
|
/// 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
|
/// 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.
|
/// 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] {
|
for side in [Side::Local, Side::Cloud] {
|
||||||
if Some(side) == active {
|
if Some(side) == active {
|
||||||
continue;
|
continue;
|
||||||
@@ -174,8 +249,9 @@ async fn switch_to(
|
|||||||
new_side: Side,
|
new_side: Side,
|
||||||
creds: &CredentialStore,
|
creds: &CredentialStore,
|
||||||
old_active: Option<Side>,
|
old_active: Option<Side>,
|
||||||
|
cached_local_ip: Option<Ipv4Addr>,
|
||||||
) -> Result<()> {
|
) -> 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`
|
// 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
|
// wieder ausgehängt werden, statt sie unbemerkt gemountet zu lassen: der nächste
|
||||||
@@ -200,8 +276,10 @@ async fn mount_side(
|
|||||||
settings: &GlobalSettings,
|
settings: &GlobalSettings,
|
||||||
side: Side,
|
side: Side,
|
||||||
creds: &CredentialStore,
|
creds: &CredentialStore,
|
||||||
|
cached_local_ip: Option<Ipv4Addr>,
|
||||||
) -> Result<()> {
|
) -> 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)
|
std::fs::create_dir_all(&mount_target.mount_point)
|
||||||
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
.map_err(|e| crate::error::Error::io(&mount_target.mount_point, e))?;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user