fix(mounts): 3 min 15 s hingen in EINER Zeile - os.makedirs auf dem toten Mount
Ampel / ampel (push) Successful in 29s

Nachtrag, weil die letzte Runde die Wiederanbindung nicht schneller machte
(202 s statt 150 s). Die Zeitstempel im Log zeigten, wo die Zeit sitzt:

  12:53:56  API gestartet
  12:54:03  "antwortet nicht - wird neu verbunden"    <- Erkennung: 7 s, gut
  12:57:18  "eingehaengt"                             <- Reparatur: 3 min 15 s

Die Erkennung war also schon schnell; die REPARATUR fraess die Zeit. Und zwar
nicht in den Mount-Versuchen, sondern in der ersten Zeile von mounten():

  os.makedirs(ziel, exist_ok=True)

`exist_ok` prueft mit os.path.isdir, und ein `stat` auf einen toten CIFS-Mount
blockiert im Kernel bis zum SMB-Timeout. Ausgerechnet der Aufruf, der nur "lege
den Ordner an, falls er fehlt" bedeutet, hing drei Minuten - BEVOR irgendeine der
sorgfaeltig begrenzten Pruefungen dran war. Dritter Fund derselben Sorte an einem
Tag: os-Aufruf auf einen Netzpfad ohne Zeitgrenze.

Jetzt klaert `pfad_lage()` die Lage mit einem abbrechbaren Kind-Prozess
(`timeout 4 ls -d`, drei Antworten: da / weg / unklar), und makedirs laeuft nur
bei "weg". Dieselbe Falle in `reparieren()` (os.path.ismount als Vorbedingung -
gebraucht wird es nicht, `umount -l` auf einen leeren Pfad kostet nichts) und in
`aushaengen()` (ismount + rmdir).

Dazu steht die DAUER jetzt im Log ("neu verbunden (4.2s)"). Sie war die
entscheidende Spur; wer sie ablesen kann, muss sie nicht rekonstruieren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 15:01:09 +02:00
parent 2a90538473
commit 58f4991d92
3 changed files with 170 additions and 28 deletions
+63 -26
View File
@@ -67,6 +67,37 @@ def ist_erreichbar(name: str) -> bool:
return False
def pfad_lage(ziel: str) -> str:
"""Existiert dieses Verzeichnis? „da" | „weg" | „unklar" — mit Zeitgrenze.
⚠️ Der Grund, warum es das gibt (Befund 26.07.2026, an Zeitstempeln gemessen):
Eine Wiederanbindung brauchte **3 Minuten 15 Sekunden**, und die Zeit ging
nicht in die Mount-Versuche, sondern in die allererste Zeile von `mounten()`:
os.makedirs(ziel, exist_ok=True)
`exist_ok` prüft mit `os.path.isdir()`, und ein `stat` auf einen TOTEN
CIFS-Mount blockiert im Kernel bis zum SMB-Timeout. Ausgerechnet der Aufruf,
der nur „lege den Ordner an, falls er fehlt" bedeutet, hing also drei Minuten
— bevor irgendeine der sorgfältig begrenzten Prüfungen überhaupt dran war.
Dritter Fund derselben Sorte an einem Tag: os-Aufruf auf einen Netzpfad ohne
Zeitgrenze.
Deshalb wird die Lage jetzt mit einem Kind-Prozess erfragt (`timeout 4 ls`),
der sich abbrechen lässt. „unklar" heißt: existiert, antwortet aber nicht —
dann ist ein makedirs weder nötig noch möglich.
"""
try:
ergebnis = subprocess.run(
["timeout", "4", "ls", "-d", ziel], capture_output=True, timeout=6
)
except (OSError, subprocess.TimeoutExpired):
return "unklar"
if ergebnis.returncode == 0:
return "da"
return "unklar" if ergebnis.returncode == 124 else "weg"
def wirklich_erreichbar(name: str, warten=None) -> bool:
"""Antwortet die Freigabe auch noch DREI SEKUNDEN später? (zweimal geprüft)
@@ -273,12 +304,14 @@ def mounten(name: str, typ: str, quelle: str, optionen: str = "",
Mount-Fehlern.
"""
ziel = _mountpoint(name)
# exist_ok fängt „Ordner da" NICHT, wenn ein toter Mount os.path.isdir
# täuschen lässt (FileExistsError, Befund 24.07.) — deshalb tolerant.
try:
os.makedirs(ziel, exist_ok=True)
except FileExistsError:
pass
# Den Ordner NUR anlegen, wenn er wirklich fehlt — und das mit Zeitgrenze
# klären (siehe pfad_lage): `os.makedirs(..., exist_ok=True)` hing auf einem
# toten CIFS-Mount drei Minuten im Kernel, weil `exist_ok` ein `stat` macht.
if pfad_lage(ziel) == "weg":
try:
os.makedirs(ziel, exist_ok=True)
except OSError:
pass # Rennen mit einem anderen Aufruf oder Rechte — mount sagt es
# ⚠️ ERREICHBARKEIT VOR ismount — nicht umgekehrt (Befund 26.07.2026).
#
# Vorher stand hier `if os.path.ismount(ziel): return schreibtest(ziel)`.
@@ -387,22 +420,25 @@ def _lazy_umount(ziel: str) -> None:
def aushaengen(name: str) -> None:
"""Freigabe abhängen und den Mountpunkt aufräumen.
Ohne `os.path.ismount`/`os.rmdir` als erste Schritte: Beide `stat`en den
Pfad, und auf einem toten CIFS-Mount blockiert das im Kernel (siehe
pfad_lage). Ein `umount` auf einen Pfad ohne Mount kostet dagegen nichts.
"""
ziel = _mountpoint(name)
try:
noch_mount = os.path.ismount(ziel)
except OSError:
noch_mount = True # stale
if noch_mount:
ergebnis = subprocess.run(
["umount", ziel], capture_output=True, text=True, timeout=30
)
if ergebnis.returncode != 0:
# Toter/beschäftigter Mount → lazy detach (klappt immer)
_lazy_umount(ziel)
try:
os.rmdir(ziel)
except OSError:
pass # nicht leer oder weg — egal
ergebnis = subprocess.run(
["umount", ziel], capture_output=True, text=True, timeout=30
)
if ergebnis.returncode != 0:
# Toter/beschäftigter Mount → lazy detach (klappt immer)
_lazy_umount(ziel)
# Erst wenn der Pfad wieder antwortet, darf rmdir ihn anfassen.
if pfad_lage(ziel) == "da":
try:
os.rmdir(ziel)
except OSError:
pass # nicht leer oder weg — egal
def reparieren(name: str, typ: str, quelle: str, optionen: str = "",
@@ -411,13 +447,14 @@ def reparieren(name: str, typ: str, quelle: str, optionen: str = "",
Nötig, wenn ein NAS-Mount stale geworden ist (Rebuild, NAS-Schlaf) — das
normale mounten() würde am „ist schon Mountpoint" hängenbleiben.
`os.path.ismount` stand hier vorher als Vorbedingung — auf einem toten
CIFS-Mount blockiert dieses `stat` im Kernel (siehe pfad_lage). Gefragt wird
es gar nicht mehr: `umount -l` auf einen Pfad, an dem nichts hängt, kostet
nichts und meldet nur einen Rückgabewert, den hier niemand braucht.
"""
ziel = _mountpoint(name)
try:
if os.path.ismount(ziel):
_lazy_umount(ziel)
except OSError:
_lazy_umount(ziel)
_lazy_umount(ziel)
return mounten(name, typ, quelle, optionen, username, passwort)