fix(mounts): DIE URSACHE gefunden - die CIFS-Verbindung stirbt mit dem Container
Ampel / ampel (push) Successful in 31s

Der Savepoint v3.17 fuehrte das als "Ursache liegt beim NAS, nicht gefunden".
Gemessen ist es etwas ganz anderes, und es liegt bei uns:

  /proc/fs/cifs/DebugData  ->  Net namespace: 4026532653
  api-Container            ->  net:[4026532653]   DIESELBE
  worker-Container         ->  net:[4026532540]   andere

Die CIFS-Verbindung lebt in der NETZ-NAMESPACE DES API-CONTAINERS - dort wird
sie eingehaengt, weil nur dieser Container CAP_SYS_ADMIN hat. Wird der Container
neu gebaut, stirbt sein Netz-Namespace und mit ihm der Socket. Der Mount steht
danach weiter in /proc/mounts (per rshared auf den Host propagiert) und sieht
vollkommen gesund aus - aber jeder Zugriff laeuft in den CIFS-Timeout.

Damit erklaert sich alles, was vorher widerspruechlich aussah: warum es nach
JEDEM Deploy passiert, warum `mount` Erfolg meldet, warum /proc/mounts genau eine
korrekte Schicht zeigt, und warum nur ein echtes Neu-Verbinden hilft. Das NAS ist
unschuldig (eine Sitzung, Status 1, 630 Credits, Ping 0,47 ms).

Zweiter Fund, der den Rest erklaert: Direkt nach einem frischen Mount antwortete
die Freigabe - und Sekunden spaeter nicht mehr. Das ist ein Wettlauf mit
`umount -l`: lazy heisst, der Abbau passiert spaeter, und faellt er samt
Propagation hinter den neuen Mount, zeigt der Pfad wieder auf die Leiche. Eine
einzige Probe kann das nicht sehen - deshalb prueft `wirklich_erreichbar()`
zweimal mit drei Sekunden Abstand, und zwar sowohl beim Mounten als auch in der
Wache.

Dazu: erste Pruefung der Wache schon nach 10 s statt 60 s. Genau dann ist die
Lage nach einem Deploy kaputt.

Ehrlich offen bleibt die strukturelle Folge: Der api-Container HAELT die
NAS-Verbindung. Startet er mitten in einem Rip neu, verliert auch der Worker sein
Ziel. Das saubere Gegenmittel waere ein Mount auf dem HOST statt im Container -
ein eigener Umbau, und er widerspraeche "Speicherziele ueber das UI einhaengen".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 14:32:51 +02:00
parent 549727f648
commit 4cf7acbb96
3 changed files with 93 additions and 14 deletions
+35 -2
View File
@@ -183,9 +183,11 @@ def test_mounten_geht_bei_totem_mount_den_reparatur_weg(monkeypatch):
import mounts
ablauf = []
antworten = iter([False, True]) # vorher tot, nach dem Mount erreichbar
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: next(antworten))
# 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)
monkeypatch.setattr(mounts, "wirklich_erreichbar", lambda name: True)
monkeypatch.setattr(mounts, "_stale_mounts_loesen",
lambda ziel: ablauf.append("loesen"))
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
@@ -216,6 +218,7 @@ def test_mounten_prueft_das_ergebnis_und_versucht_es_zweimal(monkeypatch):
# nie erreichbar: vorher, nach Versuch 1, nach Versuch 2
monkeypatch.setattr(mounts.os, "makedirs", lambda *a, **k: None)
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)
monkeypatch.setattr(mounts, "_lazy_umount", lambda ziel: ablauf.append("lazy"))
monkeypatch.setattr(mounts, "schreibtest", lambda p: True)
@@ -248,3 +251,33 @@ def test_mounten_laesst_gesunden_mount_in_ruhe(monkeypatch):
lambda ziel: (_ for _ in ()).throw(AssertionError("nicht loesen!")))
assert mounts.mounten("rippy", "cifs", "//nas/rippy") is True
def test_wirklich_erreichbar_prueft_zweimal_mit_abstand(monkeypatch):
"""Befund 26.07.2026: Direkt nach einem frischen `mount` antwortete die
Freigabe - und Sekunden spaeter lief jeder Zugriff in die Zeitgrenze
(Wettlauf mit dem lazy umount, dessen Abbau hinter den neuen Mount fiel).
Eine EINZIGE Probe kann das nicht sehen."""
import mounts
antworten = iter([True, False]) # erst ja, dann nein
gewartet = []
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: next(antworten))
assert mounts.wirklich_erreichbar("rippy", warten=gewartet.append) is False
assert gewartet == [3]
def test_wirklich_erreichbar_bei_gesunder_freigabe(monkeypatch):
import mounts
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: True)
assert mounts.wirklich_erreichbar("rippy", warten=lambda s: None) is True
def test_wirklich_erreichbar_spart_das_warten_wenn_schon_die_erste_probe_faellt(monkeypatch):
import mounts
gewartet = []
monkeypatch.setattr(mounts, "ist_erreichbar", lambda name: False)
assert mounts.wirklich_erreichbar("rippy", warten=gewartet.append) is False
assert gewartet == [] # nicht drei Sekunden fuer nichts