diff --git a/src/jira_hub/relay.py b/src/jira_hub/relay.py index a6f93eb..0c36f58 100644 --- a/src/jira_hub/relay.py +++ b/src/jira_hub/relay.py @@ -1,6 +1,7 @@ """Реле через общую папку (specs/hub-api, «Реле через общую папку»): файлы-запросы агента из /req/ пересылаются в HTTP этого же хаба, ответы кладутся в /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}") # --- уборка ------------------------------------------------------------------------ diff --git a/tests/test_relay.py b/tests/test_relay.py index 57c6f66..f35506a 100644 --- a/tests/test_relay.py +++ b/tests/test_relay.py @@ -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)