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/
|
"""Реле через общую папку (specs/hub-api, «Реле через общую папку»): файлы-запросы агента из <relay_dir>/req/
|
||||||
пересылаются в HTTP этого же хаба, ответы кладутся в <relay_dir>/resp/. HTTP API, обработчики и токены не меняются;
|
пересылаются в HTTP этого же хаба, ответы кладутся в <relay_dir>/resp/. HTTP API, обработчики и токены не меняются;
|
||||||
реле для них — ещё один клиент. Нужно, когда VPN на виртуалке закрывает сеть до хаба (design.md folder-transport)."""
|
реле для них — ещё один клиент. Нужно, когда VPN на виртуалке закрывает сеть до хаба (design.md folder-transport)."""
|
||||||
|
import http.client
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
@ -60,6 +61,7 @@ class Relay:
|
|||||||
self._cleanup_at = 0.0
|
self._cleanup_at = 0.0
|
||||||
self._ready = False
|
self._ready = False
|
||||||
self._warned: str | None = None
|
self._warned: str | None = None
|
||||||
|
self._cleaned: Path | None = None # папка, чей work/ вычищен после старта реле
|
||||||
self.workers: list[threading.Thread] = []
|
self.workers: list[threading.Thread] = []
|
||||||
|
|
||||||
# --- цикл ---------------------------------------------------------------------
|
# --- цикл ---------------------------------------------------------------------
|
||||||
@ -122,9 +124,15 @@ class Relay:
|
|||||||
def _prepare(self) -> None:
|
def _prepare(self) -> None:
|
||||||
for sub in ("req", "resp", "work"):
|
for sub in ("req", "resp", "work"):
|
||||||
(self.root / sub).mkdir(exist_ok=True)
|
(self.root / sub).mkdir(exist_ok=True)
|
||||||
for stale in (self.root / "work").iterdir():
|
if self._cleaned != self.root:
|
||||||
stale.unlink(missing_ok=True)
|
# первая подготовка этой папки после старта: взятые прошлым процессом запросы уже никто не обработает
|
||||||
say(f"relay: удалён незавершённый запрос {stale.name} (перезапуск реле)")
|
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
|
self._ready, self._warned = True, None
|
||||||
|
|
||||||
def _take(self, path: Path) -> None:
|
def _take(self, path: Path) -> None:
|
||||||
@ -193,7 +201,7 @@ class Relay:
|
|||||||
e.close()
|
e.close()
|
||||||
except ValueError as e: # недопустимый заголовок (перевод строки) или URL
|
except ValueError as e: # недопустимый заголовок (перевод строки) или URL
|
||||||
return 400, _error("bad_request", str(e))
|
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}")
|
return 502, _error("relay", f"хаб недоступен изнутри: {e}")
|
||||||
|
|
||||||
# --- уборка ------------------------------------------------------------------------
|
# --- уборка ------------------------------------------------------------------------
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import http.client
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import threading
|
import threading
|
||||||
@ -324,3 +325,45 @@ def test_removed_resp_dir_recreated_before_forward(env):
|
|||||||
put_request(root, "r1", "GET", "/agent/settings", env["agent"])
|
put_request(root, "r1", "GET", "/agent/settings", env["agent"])
|
||||||
run_once(r)
|
run_once(r)
|
||||||
assert response(root, "r1")["status"] == 200
|
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