fix(relay): 502 на сбой протокола, work/ чистится один раз на папку, тест serve --host (#13)
M-1, M-3, M-4 (hub#8). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
bd8c769e75
commit
615530aa1b
@ -1,6 +1,7 @@
|
||||
"""Реле через общую папку (specs/hub-api, «Реле через общую папку»): файлы-запросы агента из <relay_dir>/req/
|
||||
пересылаются в HTTP этого же хаба, ответы кладутся в <relay_dir>/resp/. HTTP API, обработчики и токены не меняются;
|
||||
реле для них — ещё один клиент. Нужно, когда VPN на виртуалке закрывает сеть до хаба (design.md folder-transport)."""
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
@ -60,6 +61,7 @@ class Relay:
|
||||
self._cleanup_at = 0.0
|
||||
self._ready = False
|
||||
self._warned: str | None = None
|
||||
self._cleaned: Path | None = None # папка, чей work/ вычищен после старта реле
|
||||
self.workers: list[threading.Thread] = []
|
||||
|
||||
# --- цикл ---------------------------------------------------------------------
|
||||
@ -122,9 +124,15 @@ class Relay:
|
||||
def _prepare(self) -> None:
|
||||
for sub in ("req", "resp", "work"):
|
||||
(self.root / sub).mkdir(exist_ok=True)
|
||||
for stale in (self.root / "work").iterdir():
|
||||
stale.unlink(missing_ok=True)
|
||||
say(f"relay: удалён незавершённый запрос {stale.name} (перезапуск реле)")
|
||||
if self._cleaned != self.root:
|
||||
# первая подготовка этой папки после старта: взятые прошлым процессом запросы уже никто не обработает
|
||||
for stale in (self.root / "work").iterdir():
|
||||
stale.unlink(missing_ok=True)
|
||||
say(f"relay: удалён незавершённый запрос {stale.name} (старт реле)")
|
||||
self._cleaned = self.root
|
||||
else:
|
||||
# пропал подкаталог или не было доступа: взятые запросы ещё обрабатываются своими потоками — work/ не трогаем
|
||||
say(f"relay: папка {self.root} подготовлена заново")
|
||||
self._ready, self._warned = True, None
|
||||
|
||||
def _take(self, path: Path) -> None:
|
||||
@ -193,7 +201,7 @@ class Relay:
|
||||
e.close()
|
||||
except ValueError as e: # недопустимый заголовок (перевод строки) или URL
|
||||
return 400, _error("bad_request", str(e))
|
||||
except (urllib.error.URLError, OSError) as e:
|
||||
except (urllib.error.URLError, http.client.HTTPException, OSError) as e:
|
||||
return 502, _error("relay", f"хаб недоступен изнутри: {e}")
|
||||
|
||||
# --- уборка ------------------------------------------------------------------------
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import http.client
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
@ -324,3 +325,45 @@ def test_removed_resp_dir_recreated_before_forward(env):
|
||||
put_request(root, "r1", "GET", "/agent/settings", env["agent"])
|
||||
run_once(r)
|
||||
assert response(root, "r1")["status"] == 200
|
||||
|
||||
|
||||
def test_protocol_error_gives_502(env, monkeypatch):
|
||||
r, root = env["relay"], env["root"]
|
||||
r.step()
|
||||
|
||||
def broken(*a, **k):
|
||||
raise http.client.BadStatusLine("мусор")
|
||||
monkeypatch.setattr("urllib.request.urlopen", broken)
|
||||
put_request(root, "b1", "GET", "/agent/settings", env["agent"])
|
||||
run_once(r)
|
||||
resp = response(root, "b1")
|
||||
assert resp["status"] == 502 and "хаб недоступен" in resp["body"]
|
||||
|
||||
|
||||
def test_reprepare_keeps_work(env, capsys):
|
||||
r, root = env["relay"], env["root"]
|
||||
r.step() # первая подготовка
|
||||
(root / "work" / "busy.json").write_text("{}", encoding="utf-8")
|
||||
(root / "req").rmdir() # пропал подкаталог — реле подготовит папку заново
|
||||
for _ in range(3):
|
||||
r.step()
|
||||
assert (root / "req").is_dir() and (root / "work" / "busy.json").exists()
|
||||
out = capsys.readouterr().out
|
||||
assert "перезапуск реле" not in out and "подготовлена заново" in out
|
||||
|
||||
|
||||
def test_relay_targets_serve_host(tmp_path, monkeypatch):
|
||||
seen = []
|
||||
|
||||
class Spy(relay.Relay):
|
||||
def __init__(self, hub, port, host="127.0.0.1", poll=0.1):
|
||||
seen.append(host)
|
||||
super().__init__(hub, port, host, poll)
|
||||
|
||||
monkeypatch.setattr(relay, "Relay", Spy)
|
||||
h = api.Hub(tmp_path / "hub.sqlite", backups_dir=tmp_path / "backups")
|
||||
for host in ("0.0.0.0", "localhost"):
|
||||
srv, stop = api.start(h, host, 0)
|
||||
stop.set()
|
||||
srv.server_close()
|
||||
assert seen == ["127.0.0.1", "localhost"] # конкретный адрес serve — туда же ходит реле (M-1 #8)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user