Serve Crello corpus aafdb0e86816 from the mounted bucket
Browse files- server/router.py +41 -14
server/router.py
CHANGED
|
@@ -90,7 +90,7 @@ class Router:
|
|
| 90 |
async def http_proxy(self, request: Request) -> Response:
|
| 91 |
body = await request.body()
|
| 92 |
worker = self.for_path(request.url.path)
|
| 93 |
-
|
| 94 |
if worker is None and request.url.path.rstrip("/") == "/mcp" and request.method == "POST":
|
| 95 |
try:
|
| 96 |
rpc = json.loads(body or b"{}")
|
|
@@ -99,9 +99,10 @@ class Router:
|
|
| 99 |
sid = (rpc.get("params") or {}).get("session_id") if isinstance(rpc, dict) else None
|
| 100 |
if sid and sid in self.sessions:
|
| 101 |
worker = self.sessions[sid]
|
|
|
|
|
|
|
| 102 |
elif isinstance(rpc, dict) and rpc.get("method") == "openenv/session/create":
|
| 103 |
-
|
| 104 |
-
self.load[worker] += 1
|
| 105 |
if worker is None:
|
| 106 |
worker = next(self.rr)
|
| 107 |
url = self.backends[worker] + request.url.path + (f"?{request.url.query}" if request.url.query else "")
|
|
@@ -110,21 +111,13 @@ class Router:
|
|
| 110 |
req = self.http.build_request(request.method, url, headers=headers, content=body)
|
| 111 |
resp = await self.http.send(req, stream=True)
|
| 112 |
except httpx.HTTPError as e:
|
| 113 |
-
if creating:
|
| 114 |
-
self.load[worker] -= 1
|
| 115 |
return JSONResponse({"error": f"worker {worker} unreachable: {e}"}, status_code=502)
|
| 116 |
out_headers = {k: v for k, v in resp.headers.items() if k.lower() not in HOP}
|
| 117 |
-
if
|
|
|
|
|
|
|
| 118 |
data = await resp.aread()
|
| 119 |
await resp.aclose()
|
| 120 |
-
if creating:
|
| 121 |
-
self.load[worker] -= 1
|
| 122 |
-
try:
|
| 123 |
-
sid = json.loads(data).get("result", {}).get("session_id")
|
| 124 |
-
except ValueError:
|
| 125 |
-
sid = None
|
| 126 |
-
if sid:
|
| 127 |
-
self.sessions[sid] = worker
|
| 128 |
self.learn(worker, data)
|
| 129 |
return Response(data, status_code=resp.status_code, headers=out_headers)
|
| 130 |
|
|
@@ -137,6 +130,40 @@ class Router:
|
|
| 137 |
|
| 138 |
return StreamingResponse(stream(), status_code=resp.status_code, headers=out_headers)
|
| 139 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
# ------------------------------------------------------------------ WebSocket
|
| 141 |
|
| 142 |
async def ws_proxy(self, ws: WebSocket) -> None:
|
|
|
|
| 90 |
async def http_proxy(self, request: Request) -> Response:
|
| 91 |
body = await request.body()
|
| 92 |
worker = self.for_path(request.url.path)
|
| 93 |
+
closing = None
|
| 94 |
if worker is None and request.url.path.rstrip("/") == "/mcp" and request.method == "POST":
|
| 95 |
try:
|
| 96 |
rpc = json.loads(body or b"{}")
|
|
|
|
| 99 |
sid = (rpc.get("params") or {}).get("session_id") if isinstance(rpc, dict) else None
|
| 100 |
if sid and sid in self.sessions:
|
| 101 |
worker = self.sessions[sid]
|
| 102 |
+
if rpc.get("method") == "openenv/session/close":
|
| 103 |
+
closing = sid
|
| 104 |
elif isinstance(rpc, dict) and rpc.get("method") == "openenv/session/create":
|
| 105 |
+
return await self.create_session(request, body)
|
|
|
|
| 106 |
if worker is None:
|
| 107 |
worker = next(self.rr)
|
| 108 |
url = self.backends[worker] + request.url.path + (f"?{request.url.query}" if request.url.query else "")
|
|
|
|
| 111 |
req = self.http.build_request(request.method, url, headers=headers, content=body)
|
| 112 |
resp = await self.http.send(req, stream=True)
|
| 113 |
except httpx.HTTPError as e:
|
|
|
|
|
|
|
| 114 |
return JSONResponse({"error": f"worker {worker} unreachable: {e}"}, status_code=502)
|
| 115 |
out_headers = {k: v for k, v in resp.headers.items() if k.lower() not in HOP}
|
| 116 |
+
if closing:
|
| 117 |
+
self.sessions.pop(closing, None)
|
| 118 |
+
if request.url.path.startswith("/api/episodes"):
|
| 119 |
data = await resp.aread()
|
| 120 |
await resp.aclose()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
self.learn(worker, data)
|
| 122 |
return Response(data, status_code=resp.status_code, headers=out_headers)
|
| 123 |
|
|
|
|
| 130 |
|
| 131 |
return StreamingResponse(stream(), status_code=resp.status_code, headers=out_headers)
|
| 132 |
|
| 133 |
+
async def create_session(self, request: Request, body: bytes) -> Response:
|
| 134 |
+
"""A new OpenEnv session: the least busy worker first, the next one if it is full (each worker caps its
|
| 135 |
+
sessions at MAX_CONCURRENT_ENVS), and the session's worker remembered for /mcp and /ws."""
|
| 136 |
+
headers = {k: v for k, v in request.headers.items() if k.lower() not in HOP}
|
| 137 |
+
order = sorted(range(len(self.backends)), key=lambda i: (self.load[i], i))
|
| 138 |
+
first = self.least_busy()
|
| 139 |
+
order.remove(first)
|
| 140 |
+
data, status, out_headers = b"", 502, {}
|
| 141 |
+
for worker in [first, *order]:
|
| 142 |
+
self.load[worker] += 1
|
| 143 |
+
try:
|
| 144 |
+
resp = await self.http.post(self.backends[worker] + "/mcp", headers=headers, content=body)
|
| 145 |
+
except httpx.HTTPError as e:
|
| 146 |
+
data, status = json.dumps({"error": f"worker {worker} unreachable: {e}"}).encode(), 502
|
| 147 |
+
continue
|
| 148 |
+
finally:
|
| 149 |
+
self.load[worker] -= 1
|
| 150 |
+
data, status = resp.content, resp.status_code
|
| 151 |
+
out_headers = {k: v for k, v in resp.headers.items() if k.lower() not in HOP}
|
| 152 |
+
try:
|
| 153 |
+
reply = json.loads(data)
|
| 154 |
+
except ValueError:
|
| 155 |
+
reply = {}
|
| 156 |
+
sid = (reply.get("result") or {}).get("session_id") if isinstance(reply, dict) else None
|
| 157 |
+
if sid:
|
| 158 |
+
self.sessions[sid] = worker
|
| 159 |
+
if len(self.sessions) > 200_000: # sessions closed without telling us, oldest first
|
| 160 |
+
for k in list(self.sessions)[:50_000]:
|
| 161 |
+
del self.sessions[k]
|
| 162 |
+
break
|
| 163 |
+
if "capacity" not in json.dumps(reply.get("error", "")).lower():
|
| 164 |
+
break # a real error, not a full worker: pass it on
|
| 165 |
+
return Response(data, status_code=status, headers=out_headers)
|
| 166 |
+
|
| 167 |
# ------------------------------------------------------------------ WebSocket
|
| 168 |
|
| 169 |
async def ws_proxy(self, ws: WebSocket) -> None:
|