Kode-Animator commited on
Commit
b17615c
·
1 Parent(s): 75dc996

feat: prewarm thought engine node on bridge startup

Browse files
Files changed (2) hide show
  1. server.py +41 -1
  2. tests/test_modern_mcp.py +73 -3
server.py CHANGED
@@ -13,9 +13,11 @@ and visualize reasoning trees through this bridge.
13
  ==========================================================================
14
  """
15
 
 
16
  import os
17
  import json
18
  import httpx
 
19
  from typing import Optional
20
  from mcp.server.mcpserver import MCPServer
21
  from starlette.applications import Starlette
@@ -30,6 +32,44 @@ NODE_URL = os.getenv(
30
  )
31
 
32
  mcp = MCPServer("thought-engine-bridge")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
 
35
  # ── HTTP Helper ────────────────────────────────────────────────
@@ -397,7 +437,7 @@ def build_app() -> Starlette:
397
  *modern.routes,
398
  *legacy.routes,
399
  ],
400
- lifespan=modern.router.lifespan_context,
401
  )
402
 
403
 
 
13
  ==========================================================================
14
  """
15
 
16
+ import asyncio
17
  import os
18
  import json
19
  import httpx
20
+ from contextlib import asynccontextmanager
21
  from typing import Optional
22
  from mcp.server.mcpserver import MCPServer
23
  from starlette.applications import Starlette
 
32
  )
33
 
34
  mcp = MCPServer("thought-engine-bridge")
35
+ _node_prewarm_started = False
36
+ _node_prewarm_task: asyncio.Task | None = None
37
+
38
+
39
+ async def _prewarm_node() -> None:
40
+ """Best-effort bounded wake/readiness probe for the downstream Node."""
41
+ for attempt in range(3):
42
+ try:
43
+ async with httpx.AsyncClient(timeout=5.0) as client:
44
+ response = await client.get(f"{NODE_URL}/health")
45
+ if response.status_code == 200:
46
+ payload = response.json()
47
+ if payload.get("status") == "ok":
48
+ return
49
+ except Exception:
50
+ pass
51
+
52
+ if attempt < 2:
53
+ await asyncio.sleep(0.25)
54
+
55
+
56
+ @asynccontextmanager
57
+ async def _bridge_lifespan(app: Starlette, modern_lifespan):
58
+ global _node_prewarm_started, _node_prewarm_task
59
+
60
+ async with modern_lifespan(app):
61
+ if not _node_prewarm_started:
62
+ _node_prewarm_started = True
63
+ _node_prewarm_task = asyncio.create_task(_prewarm_node())
64
+ try:
65
+ yield
66
+ finally:
67
+ if _node_prewarm_task is not None and not _node_prewarm_task.done():
68
+ _node_prewarm_task.cancel()
69
+ try:
70
+ await _node_prewarm_task
71
+ except asyncio.CancelledError:
72
+ pass
73
 
74
 
75
  # ── HTTP Helper ────────────────────────────────────────────────
 
437
  *modern.routes,
438
  *legacy.routes,
439
  ],
440
+ lifespan=lambda app: _bridge_lifespan(app, modern.router.lifespan_context),
441
  )
442
 
443
 
tests/test_modern_mcp.py CHANGED
@@ -132,7 +132,7 @@ async def test_thought_node_identity_preserves_node_rest_mapping() -> None:
132
  payload = json.loads(result.content[0].text)
133
  assert client.protocol_version == "2026-07-28"
134
  assert payload == EXPECTED_IDENTITY
135
- assert calls == [("GET", "/"), ("GET", "/")]
136
 
137
 
138
  @pytest.mark.asyncio
@@ -189,10 +189,15 @@ async def test_modern_remains_available_after_legacy_disconnect() -> None:
189
 
190
 
191
  @pytest.mark.asyncio
192
- async def test_health_is_bridge_local_and_advertises_both_transports() -> None:
193
  bridge = _load_bridge()
194
  node_calls = 0
195
 
 
 
 
 
 
196
  class FailingAsyncClient:
197
  async def __aenter__(self):
198
  nonlocal node_calls
@@ -202,7 +207,7 @@ async def test_health_is_bridge_local_and_advertises_both_transports() -> None:
202
  async def __aexit__(self, *args):
203
  return None
204
 
205
- bridge.httpx.AsyncClient = FailingAsyncClient
206
  async with _serve(bridge.app) as port:
207
  async with httpx2.AsyncClient() as client:
208
  response = await client.get(f"http://127.0.0.1:{port}/health")
@@ -212,3 +217,68 @@ async def test_health_is_bridge_local_and_advertises_both_transports() -> None:
212
  assert payload["streamable_http_endpoint"] == "/mcp"
213
  assert payload["sse_endpoint"] == "/sse"
214
  assert node_calls == 0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  payload = json.loads(result.content[0].text)
133
  assert client.protocol_version == "2026-07-28"
134
  assert payload == EXPECTED_IDENTITY
135
+ assert calls == [("GET", "/health"), ("GET", "/"), ("GET", "/")]
136
 
137
 
138
  @pytest.mark.asyncio
 
189
 
190
 
191
  @pytest.mark.asyncio
192
+ async def test_health_is_bridge_local_and_advertises_both_transports(monkeypatch) -> None:
193
  bridge = _load_bridge()
194
  node_calls = 0
195
 
196
+ async def no_prewarm() -> None:
197
+ return None
198
+
199
+ monkeypatch.setattr(bridge, "_prewarm_node", no_prewarm)
200
+
201
  class FailingAsyncClient:
202
  async def __aenter__(self):
203
  nonlocal node_calls
 
207
  async def __aexit__(self, *args):
208
  return None
209
 
210
+ monkeypatch.setattr(bridge.httpx, "AsyncClient", FailingAsyncClient)
211
  async with _serve(bridge.app) as port:
212
  async with httpx2.AsyncClient() as client:
213
  response = await client.get(f"http://127.0.0.1:{port}/health")
 
217
  assert payload["streamable_http_endpoint"] == "/mcp"
218
  assert payload["sse_endpoint"] == "/sse"
219
  assert node_calls == 0
220
+
221
+
222
+ @pytest.mark.asyncio
223
+ async def test_bridge_lifespan_starts_one_nonblocking_node_prewarm_per_process() -> None:
224
+ bridge = _load_bridge()
225
+ started = 0
226
+ blocker = asyncio.Event()
227
+
228
+ async def fake_prewarm() -> None:
229
+ nonlocal started
230
+ started += 1
231
+ await blocker.wait()
232
+
233
+ bridge._prewarm_node = fake_prewarm
234
+
235
+ first_app = bridge.build_app()
236
+ async with first_app.router.lifespan_context(first_app):
237
+ await asyncio.sleep(0)
238
+ assert started == 1
239
+ assert not blocker.is_set()
240
+
241
+ second_app = bridge.build_app()
242
+ async with second_app.router.lifespan_context(second_app):
243
+ await asyncio.sleep(0)
244
+ assert started == 1
245
+
246
+ blocker.set()
247
+
248
+
249
+ @pytest.mark.asyncio
250
+ async def test_node_prewarm_retries_health_until_ready() -> None:
251
+ calls = 0
252
+
253
+ async def node_health(request: Request) -> JSONResponse:
254
+ nonlocal calls
255
+ calls += 1
256
+ if calls == 1:
257
+ return JSONResponse({"status": "warming"}, status_code=503)
258
+ return JSONResponse({"status": "ok"})
259
+
260
+ node_app = Starlette(routes=[Route("/health", node_health)])
261
+ async with _serve(node_app) as node_port:
262
+ bridge = _load_bridge()
263
+ bridge.NODE_URL = f"http://127.0.0.1:{node_port}"
264
+ await bridge._prewarm_node()
265
+
266
+ assert calls == 2
267
+
268
+
269
+ @pytest.mark.asyncio
270
+ async def test_node_prewarm_exhaustion_is_best_effort() -> None:
271
+ calls = 0
272
+
273
+ async def node_health(request: Request) -> JSONResponse:
274
+ nonlocal calls
275
+ calls += 1
276
+ return JSONResponse({"status": "warming"}, status_code=503)
277
+
278
+ node_app = Starlette(routes=[Route("/health", node_health)])
279
+ async with _serve(node_app) as node_port:
280
+ bridge = _load_bridge()
281
+ bridge.NODE_URL = f"http://127.0.0.1:{node_port}"
282
+ await bridge._prewarm_node()
283
+
284
+ assert calls == 3