[verified] feat: complete Ralph task workstreams
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user