[verified] feat: complete Ralph task workstreams

This commit is contained in:
Dobromir Popov
2026-07-12 11:17:03 +03:00
parent 9a1b15c020
commit 377346c301
37 changed files with 5862 additions and 199 deletions

View File

@@ -46,11 +46,12 @@ def ws_max_size() -> int | None:
# inflation, no JSON re-encode of megabytes per hop. Text JSON frames remain the
# control plane (gossip, peer-register, streamed SSE responses).
BINARY_FRAME_MAGIC = b"MRF1"
_RPC_PEER_DISCONNECTED = object()
def encode_binary_frame(header: dict, body: bytes) -> bytes:
header_bytes = json.dumps(header, separators=(",", ":")).encode()
return BINARY_FRAME_MAGIC + len(header_bytes).to_bytes(4, "big") + header_bytes + body
return b"".join((BINARY_FRAME_MAGIC, len(header_bytes).to_bytes(4, "big"), header_bytes, body))
def decode_binary_frame(frame: bytes) -> tuple[dict, bytes]:
@@ -58,7 +59,7 @@ def decode_binary_frame(frame: bytes) -> tuple[dict, bytes]:
raise ValueError("not a meshnet binary relay frame")
header_len = int.from_bytes(frame[4:8], "big")
header = json.loads(frame[8:8 + header_len].decode())
return header, bytes(frame[8 + header_len:])
return header, frame[8 + header_len:]
class RelayServer:
@@ -79,12 +80,16 @@ class RelayServer:
ssl_cert: Path | None = None,
ssl_key: Path | None = None,
max_peers: int = 500,
rpc_timeout: float = 310.0,
rpc_idle_timeout: float = 120.0,
):
self.host = host
self.port = port
self.ssl_cert = ssl_cert
self.ssl_key = ssl_key
self.max_peers = max_peers
self.rpc_timeout = rpc_timeout
self.rpc_idle_timeout = rpc_idle_timeout
self._registry = PeerRegistry()
self._loop: asyncio.AbstractEventLoop | None = None
@@ -94,9 +99,10 @@ class RelayServer:
self._ready = threading.Event()
self._running = False
self._stop_event: asyncio.Event | None = None
# request_id → queue of relay-http-response frames (US-036: a streamed
# response is a sequence of frames; a frame without "stream" is terminal).
self._pending_rpc: dict[str, asyncio.Queue] = {}
# relay request id → (target peer, requester request id, response queue).
# The relay-generated id prevents two Route Sessions using the same
# legacy request_id from overwriting each other's pending response.
self._pending_rpc: dict[str, tuple[str, str, asyncio.Queue]] = {}
@property
def registry(self) -> PeerRegistry:
@@ -118,6 +124,12 @@ class RelayServer:
if self._thread:
self._thread.join(timeout=3.0)
def _fail_pending_for_peer(self, peer_id: str) -> None:
"""Wake requesters immediately when their selected bridge disconnects."""
for target, _, queue in tuple(self._pending_rpc.values()):
if target == peer_id:
queue.put_nowait(_RPC_PEER_DISCONNECTED)
def _run(self) -> None:
asyncio.set_event_loop(self._loop)
self._loop.run_until_complete(self._serve())
@@ -192,9 +204,9 @@ class RelayServer:
header, _ = decode_binary_frame(bytes(raw))
except (ValueError, json.JSONDecodeError):
continue
queue = self._pending_rpc.get(header.get("request_id"))
if queue is not None:
queue.put_nowait(bytes(raw))
pending = self._pending_rpc.get(header.get("request_id"))
if pending is not None:
pending[2].put_nowait(bytes(raw))
continue
try:
envelope = json.loads(raw)
@@ -226,9 +238,9 @@ class RelayServer:
if topic == "relay-http-response":
payload = envelope.get("payload", {})
request_id = payload.get("request_id")
queue = self._pending_rpc.get(request_id)
if queue is not None:
queue.put_nowait(payload)
pending = self._pending_rpc.get(request_id)
if pending is not None:
pending[2].put_nowait(payload)
continue
# Fan out to all other registered peers
@@ -242,6 +254,9 @@ class RelayServer:
finally:
if peer_id:
self._registry.unregister(peer_id)
# Do not leave a requester waiting for its full timeout after
# the selected bridge goes away.
self._fail_pending_for_peer(peer_id)
log.debug("Peer unregistered: %s", peer_id)
async def _handle_circuit_relay(self, ws_requester, target_peer_id: str) -> None:
@@ -290,18 +305,19 @@ class RelayServer:
except Exception:
return
request_id: str | None = None
requester_request_id: str | None = None
relay_request_id = uuid.uuid4().hex
try:
if isinstance(raw, (bytes, bytearray)):
header, body = decode_binary_frame(bytes(raw))
request_id = str(header.get("request_id") or uuid.uuid4())
header["request_id"] = request_id
requester_request_id = str(header.get("request_id") or uuid.uuid4())
header["request_id"] = relay_request_id
header["target_peer"] = target_peer_id
outbound: str | bytes = encode_binary_frame(header, body)
else:
payload = json.loads(raw)
request_id = str(payload.get("request_id") or uuid.uuid4())
payload["request_id"] = request_id
requester_request_id = str(payload.get("request_id") or uuid.uuid4())
payload["request_id"] = relay_request_id
payload["target_peer"] = target_peer_id
outbound = json.dumps({
"topic": "relay-http-request",
@@ -314,16 +330,18 @@ class RelayServer:
return
queue: asyncio.Queue = asyncio.Queue()
self._pending_rpc[request_id] = queue
overall_timeout = 310.0
idle_timeout = 120.0
self._pending_rpc[relay_request_id] = (
target_peer_id, requester_request_id, queue,
)
overall_timeout = self.rpc_timeout
idle_timeout = self.rpc_idle_timeout
loop = asyncio.get_running_loop()
deadline = loop.time() + overall_timeout
target = self._registry.get(target_peer_id)
try:
if target is None:
await ws_requester.send(json.dumps({
"request_id": request_id,
"request_id": requester_request_id,
"status": 503,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": f"peer {target_peer_id!r} disconnected"}),
@@ -339,21 +357,34 @@ class RelayServer:
frame = await asyncio.wait_for(
queue.get(), timeout=min(idle_timeout, remaining)
)
if frame is _RPC_PEER_DISCONNECTED:
raise ConnectionError(f"peer {target_peer_id!r} disconnected")
if isinstance(frame, (bytes, bytearray)):
await ws_requester.send(frame)
header, body = decode_binary_frame(bytes(frame))
header["request_id"] = requester_request_id
await ws_requester.send(encode_binary_frame(header, body))
break
await ws_requester.send(json.dumps(frame))
if not frame.get("stream") or frame.get("done"):
response = dict(frame)
response["request_id"] = requester_request_id
await ws_requester.send(json.dumps(response))
if not response.get("stream") or response.get("done"):
break
except asyncio.TimeoutError:
await ws_requester.send(json.dumps({
"request_id": request_id,
"request_id": requester_request_id,
"status": 504,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": "relay rpc timed out"}),
}))
except ConnectionError as exc:
await ws_requester.send(json.dumps({
"request_id": requester_request_id,
"status": 503,
"headers": {"Content-Type": "application/json"},
"body": json.dumps({"error": str(exc)}),
}))
finally:
self._pending_rpc.pop(request_id, None)
self._pending_rpc.pop(relay_request_id, None)
async def _broadcast(raw: str | bytes, peers: list) -> None: