STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
test_config_endpoints_offload.py
Go to the documentation of this file.
1"""Config endpoints must not run set_config on the event loop.
2
3set_config builds/loads the CEA cache (PintleEngineRunner). For a propellant
4whose cache is not yet committed that is a multi-minute *synchronous* build, and
5running it on the asyncio event loop freezes every other request -- health
6included -- which is the "stuck on Connecting…" hang the frontend showed.
7
8The fix is to offload set_config via run_in_threadpool in the async config
9endpoints. This test pins that invariant behaviorally without any HTTP/async test
10harness (none is in the CI subset): it records the thread set_config runs on and
11asserts it is NOT the event-loop thread. Revert the offload and set_config runs
12inline on the loop thread -> this fails.
13
14Mirrors test_session_isolation.py: real sessions via get_session, endpoint
15coroutines driven directly with asyncio.run.
16"""
17
18from __future__ import annotations
19
20import asyncio
21import threading
22
23from starlette.requests import Request
24
25from backend.session import get_session
26from backend.routers import config as config_router
27from backend import state as state_mod
28
29
30def _request(email: str | None) -> Request:
31 headers = [(b"x-auth-email", email.encode())] if email else []
32 return Request(
33 {
34 "type": "http",
35 "method": "POST",
36 "path": "/",
37 "query_string": b"",
38 "headers": headers,
39 }
40 )
41
42
44 """Drive `run_endpoint(session)` on a fresh event loop, capturing the thread
45 id set_config executed on and the event-loop thread id. Returns (loop_tid,
46 set_config_tid)."""
47 captured: dict[str, int] = {}
48 real_set_config = state_mod.AppState.set_config
49
50 def spy(self, *args, **kwargs):
51 captured["set_config_tid"] = threading.get_ident()
52 return real_set_config(self, *args, **kwargs)
53
54 async def main():
55 captured["loop_tid"] = threading.get_ident()
56 session = get_session(_request("offload-test@berkeley.edu"))
57 assert session.app_state.has_config(), "default config should be loaded"
58 state_mod.AppState.set_config = spy
59 try:
60 await run_endpoint(session)
61 finally:
62 state_mod.AppState.set_config = real_set_config
63
64 asyncio.run(main())
65 return captured.get("loop_tid"), captured.get("set_config_tid")
66
67
69 async def run(session):
70 # Switch to the same propellant the default already uses: exercises the
71 # real switch_config reconcile + set_config path without depending on
72 # which caches are committed. The offload is what we are asserting, not
73 # the physics.
74 await config_router.switch_config_endpoint(
75 {"propellant_preset": "methalox"}, session=session
76 )
77
78 loop_tid, set_config_tid = _thread_used_by_set_config(run)
79 assert set_config_tid is not None, "switch_config_endpoint did not call set_config"
80 assert set_config_tid != loop_tid, (
81 "set_config ran on the event-loop thread -- a cold CEA build here would "
82 "block every other request. Offload it with run_in_threadpool."
83 )
84
85
87 async def run(session):
88 current = config_router.config_to_dict(session.app_state.config)
89 await config_router.load_config_json(current, session=session)
90
91 loop_tid, set_config_tid = _thread_used_by_set_config(run)
92 assert set_config_tid is not None, "load_config_json did not call set_config"
93 assert set_config_tid != loop_tid, (
94 "set_config ran on the event-loop thread in /config/load -- offload it "
95 "with run_in_threadpool."
96 )
Definition run.py:1
int main()