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
+97
View File
@@ -184,6 +184,9 @@ def test_mounten_geht_bei_totem_mount_den_reparatur_weg(monkeypatch):
ablauf = []
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
# pfad_lage gestubbt: Sie ruft selbst `timeout ls` auf, und hier geht es um
# die Reihenfolge von Loesen und Mounten.
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "da")
# Vorher tot (der Vor-Check), nach dem Mount dauerhaft erreichbar. Die
# Doppelprobe wird hier gestubbt, damit der Test nicht 3 s echt wartet.
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
@@ -217,6 +220,7 @@ def test_mounten_prueft_das_ergebnis_und_versucht_es_zweimal(monkeypatch):
ablauf = []
# nie erreichbar: vorher, nach Versuch 1, nach Versuch 2
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "da")
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: False)
monkeypatch.setattr(mounts, "_stale_mounts_loesen", lambda ziel: None)
@@ -322,3 +326,96 @@ def test_stale_loesen_wartet_nicht_wenn_nichts_zu_loesen_war(monkeypatch):
lambda cmd, **k: types.SimpleNamespace(returncode=1, stdout=b"", stderr=b""))
assert mounts._stale_mounts_loesen("/app/media/x") == 0
assert gewartet == []
def test_pfad_lage_unterscheidet_da_weg_unklar(monkeypatch):
"""DER Fund vom 26.07.2026: Eine Wiederanbindung brauchte 3 min 15 s, und die
Zeit ging in die ERSTE 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. Deshalb wird die
Lage jetzt mit einem abbrechbaren Kind-Prozess erfragt."""
import types
import mounts
def antwort(rc):
return lambda cmd, **k: types.SimpleNamespace(returncode=rc, stdout=b"", stderr=b"")
monkeypatch.setattr(mounts.subprocess, "run", antwort(0))
assert mounts.pfad_lage("/app/media/rippy") == "da"
monkeypatch.setattr(mounts.subprocess, "run", antwort(2))
assert mounts.pfad_lage("/app/media/neu") == "weg"
# 124 = `timeout` hat abgeschossen: existiert, antwortet aber nicht
monkeypatch.setattr(mounts.subprocess, "run", antwort(124))
assert mounts.pfad_lage("/app/media/totes-nas") == "unklar"
def test_pfad_lage_nutzt_eine_zeitgrenze(monkeypatch):
import types
import mounts
gesehen = {}
monkeypatch.setattr(
mounts.subprocess, "run",
lambda cmd, **k: (gesehen.update(cmd=cmd, kw=k),
types.SimpleNamespace(returncode=0))[1])
mounts.pfad_lage("/x")
assert gesehen["cmd"][0] == "timeout"
assert gesehen["kw"]["timeout"] is not None
def test_mounten_legt_den_ordner_nur_an_wenn_er_fehlt(monkeypatch):
"""Bei "unklar" (toter Mount) darf makedirs NICHT laufen - genau dort hing es
drei Minuten. Der Ordner ist dann ohnehin da."""
import types
import mounts
angelegt = []
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "unklar")
monkeypatch.setattr(mounts.os, "makedirs",
lambda *a, **k: angelegt.append(a[0] if a else ""))
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: True)
monkeypatch.setattr(mounts, "_stale_mounts_loesen", lambda ziel: 0)
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
monkeypatch.setattr(
mounts.subprocess, "run",
lambda cmd, **k: types.SimpleNamespace(returncode=0, stdout="", stderr=""))
assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True
assert angelegt == []
def test_mounten_legt_den_ordner_bei_erstinstallation_an(monkeypatch):
import types
import mounts
angelegt = []
monkeypatch.setattr(mounts, "pfad_lage", lambda ziel: "weg")
monkeypatch.setattr(mounts.os, "makedirs",
lambda *a, **k: angelegt.append(a[0] if a else ""))
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: True)
monkeypatch.setattr(mounts, "_stale_mounts_loesen", lambda ziel: 0)
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
monkeypatch.setattr(
mounts.subprocess, "run",
lambda cmd, **k: types.SimpleNamespace(returncode=0, stdout="", stderr=""))
mounts.mounten("rippy", "cifs", "//nas/rippy")
assert angelegt == ["/app/media/rippy"]
def test_reparieren_fragt_ismount_nicht_mehr(monkeypatch):
"""os.path.ismount ist ein `stat` und blockiert auf einem toten CIFS-Mount.
Gebraucht wird es nicht: `umount -l` auf einen leeren Pfad kostet nichts."""
import mounts
monkeypatch.setattr(mounts.os.path, "ismount",
lambda p: (_ for _ in ()).throw(AssertionError("nicht fragen!")))
monkeypatch.setattr(mounts, "_lazy_umount", lambda ziel: None)
monkeypatch.setattr(mounts, "mounten", lambda *a, **k: True)
assert mounts.reparieren("rippy", "cifs", "//nas/rippy") is True