fix(mounts): Freigabe kam nach einem Rebuild nicht zurueck - Erreichbarkeit zuerst
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
Bestandsfehler, beim Gegenpruefen dieser Sitzung aufgefallen und dreimal
reproduziert: Nach `docker compose up -d --build` war die CIFS-Freigabe TOT
(4 von 4 Zugriffen liefen in 10 s Timeout, /storage-mounts meldete
`reachable: false`) - und blieb es. Erst POST /storage-mounts/rippy/repair
stellte sie in einer Sekunde her.
Beim API-Start haette dasselbe passieren muessen. Warum nicht:
if os.path.ismount(ziel):
return schreibtest(ziel)
Der Mountpunkt existiert im NEUEN Container weiter (Bind-Mount vom Host), also
sagte ismount "ist schon da" und alle_remounten() brach genau hier ab. Dazu
oeffnet schreibtest() eine Datei OHNE Zeitgrenze - auf einem toten CIFS
blockiert das im Kernel, und der Start-Thread haengt dauerhaft (das waren die
zwei Threads im Zustand D, die diese Sitzung schon einmal gekostet haben).
Jetzt entscheidet ist_erreichbar() zuerst - das hat eine harte Grenze
(`timeout 3 ls`, seit 24.07. im Einsatz). Antwortet die Freigabe, sind ismount
und schreibtest danach gefahrlos. Antwortet sie nicht, faellt es durch auf
loesen + frisch mounten: derselbe Weg, den reparieren() geht, nur automatisch.
Fuer den Commander heisst das: Der NAS-Mount steht nach einem Deploy wieder von
selbst, statt still zu fehlen. Ohne das war jeder Rip auf die NAS nach einem
Update kaputt, ohne dass irgendwo etwas davon zu sehen war.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+22
-1
@@ -244,11 +244,32 @@ def mounten(name: str, typ: str, quelle: str, optionen: str = "",
|
|||||||
os.makedirs(ziel, exist_ok=True)
|
os.makedirs(ziel, exist_ok=True)
|
||||||
except FileExistsError:
|
except FileExistsError:
|
||||||
pass
|
pass
|
||||||
|
# ⚠️ ERREICHBARKEIT VOR ismount — nicht umgekehrt (Befund 26.07.2026).
|
||||||
|
#
|
||||||
|
# Vorher stand hier `if os.path.ismount(ziel): return schreibtest(ziel)`.
|
||||||
|
# Das hatte zwei Folgen, beide am selben Nachmittag gemessen:
|
||||||
|
#
|
||||||
|
# 1. Nach `docker compose up -d --build` war die CIFS-Freigabe TOT (4 von 4
|
||||||
|
# Zugriffen liefen in 10 s Timeout, /storage-mounts meldete
|
||||||
|
# `reachable: false`) — und blieb es. Der Mountpunkt existierte im neuen
|
||||||
|
# Container weiter (Bind-Mount vom Host), `ismount` sagte also „ist schon
|
||||||
|
# da" und das Wiederherstellen brach genau hier ab. Die Reparatur per
|
||||||
|
# POST /storage-mounts/rippy/repair stellte sie in einer Sekunde her —
|
||||||
|
# beim Start hätte dasselbe passieren müssen.
|
||||||
|
# 2. `schreibtest()` öffnet eine Datei OHNE Zeitgrenze. Auf einem toten CIFS
|
||||||
|
# blockiert das im Kernel (Thread-Zustand D), und `alle_remounten()`
|
||||||
|
# hängt beim API-Start dauerhaft in einem Hintergrund-Thread.
|
||||||
|
#
|
||||||
|
# `ist_erreichbar()` hat eine harte Grenze (`timeout 3 ls`). Antwortet die
|
||||||
|
# Freigabe, sind ismount und schreibtest danach gefahrlos. Antwortet sie
|
||||||
|
# nicht, fällt es unten durch: lösen, frisch mounten. Das ist genau der Weg,
|
||||||
|
# den `reparieren()` geht — nur eben automatisch.
|
||||||
|
if ist_erreichbar(name):
|
||||||
try:
|
try:
|
||||||
if os.path.ismount(ziel):
|
if os.path.ismount(ziel):
|
||||||
return schreibtest(ziel)
|
return schreibtest(ziel)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass # toter Mount → unten erst lösen, dann frisch mounten (kein Stapeln)
|
pass # unten erst lösen, dann frisch mounten (kein Stapeln)
|
||||||
|
|
||||||
# Idempotent: etwaige (auch gestapelte/tote) Alt-Mounts erst lösen, damit der
|
# Idempotent: etwaige (auch gestapelte/tote) Alt-Mounts erst lösen, damit der
|
||||||
# folgende mount NICHT auf eine Leiche stapelt (Vorfall 24.07.: 12 Schichten).
|
# folgende mount NICHT auf eine Leiche stapelt (Vorfall 24.07.: 12 Schichten).
|
||||||
|
|||||||
@@ -163,3 +163,55 @@ def test_erzeugtes_mapping_uebersetzt_den_echten_fehlerfall(monkeypatch):
|
|||||||
assert pfad_lokal("/app/media/rippy/95afdc89/title_t00.mkv", mapping) == (
|
assert pfad_lokal("/app/media/rippy/95afdc89/title_t00.mkv", mapping) == (
|
||||||
"\\\\192.168.178.62\\rippy\\95afdc89\\title_t00.mkv"
|
"\\\\192.168.178.62\\rippy\\95afdc89\\title_t00.mkv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Der Mount kam nach einem Rebuild nicht zurueck (Befund 26.07.2026) ------
|
||||||
|
|
||||||
|
|
||||||
|
def test_mounten_geht_bei_totem_mount_den_reparatur_weg(monkeypatch):
|
||||||
|
"""Regression. Vorher galt `os.path.ismount` als Beweis, dass alles steht -
|
||||||
|
und nach `docker compose up -d --build` war die CIFS-Freigabe TOT (4 von 4
|
||||||
|
Zugriffen 10 s Timeout), der Mountpunkt aber weiter vorhanden. Damit brach
|
||||||
|
das Wiederherstellen genau dort ab, und `schreibtest()` (kein Timeout!)
|
||||||
|
blockierte den Start-Thread im Kernel.
|
||||||
|
|
||||||
|
Antwortet die Freigabe nicht, muss geloest und frisch gemountet werden -
|
||||||
|
genau wie reparieren() es tut, nur automatisch. Der Test faengt das an der
|
||||||
|
WIRKUNG: ismount darf dann gar nicht mehr gefragt werden."""
|
||||||
|
import types
|
||||||
|
|
||||||
|
import mounts
|
||||||
|
|
||||||
|
ablauf = []
|
||||||
|
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
|
||||||
|
monkeypatch.setattr(mounts, "_stale_mounts_loesen",
|
||||||
|
lambda ziel: ablauf.append("loesen"))
|
||||||
|
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
|
||||||
|
|
||||||
|
def fake_run(cmd, **kwargs):
|
||||||
|
ablauf.append(cmd[0])
|
||||||
|
return types.SimpleNamespace(returncode=0, stdout="", stderr="")
|
||||||
|
|
||||||
|
monkeypatch.setattr(mounts.subprocess, "run", fake_run)
|
||||||
|
# ismount darf hier gar nicht mehr gefragt werden
|
||||||
|
monkeypatch.setattr(mounts.os.path, "ismount",
|
||||||
|
lambda p: (_ for _ in ()).throw(AssertionError("zu frueh gefragt")))
|
||||||
|
|
||||||
|
assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True
|
||||||
|
assert ablauf == ["loesen", "mount"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_mounten_laesst_gesunden_mount_in_ruhe(monkeypatch):
|
||||||
|
"""Antwortet die Freigabe, bleibt sie unangetastet - kein Loesen, kein
|
||||||
|
zweites Mounten (das wuerde stapeln)."""
|
||||||
|
import mounts
|
||||||
|
|
||||||
|
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
|
||||||
|
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: True)
|
||||||
|
monkeypatch.setattr(mounts.os.path, "ismount", lambda p: True)
|
||||||
|
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
|
||||||
|
monkeypatch.setattr(mounts, "_stale_mounts_loesen",
|
||||||
|
lambda ziel: (_ for _ in ()).throw(AssertionError("nicht loesen!")))
|
||||||
|
|
||||||
|
assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True
|
||||||
|
|||||||
Reference in New Issue
Block a user