Skip to content

Commit 6fb3416

Browse files
Merge pull request #85 from QueryaHub/issue-75-routing-compiled-snapshot
perf(routing): auto-enable compiled snapshot on request path
2 parents c8a5311 + d7abc9e commit 6fb3416

4 files changed

Lines changed: 72 additions & 0 deletions

File tree

docs/routing.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ Each is a decorator that registers a route and returns the handler unchanged (so
2121
- If no route matches, the response is **404** with a plain `Not Found` body (from the Rust dispatch layer).
2222
- Non-`http` RSGI scopes are ignored in the current implementation (no response is sent for unknown `proto` values).
2323

24+
## Compiled routing snapshot
25+
26+
- OxyRoute now **auto-compiles** the route snapshot on the first HTTP request, so hot-path
27+
matching uses the lock-free compiled tables even if `freeze()` was not called explicitly.
28+
- Calling `freeze()` is still supported and remains the strict "no more route registration" switch.
29+
- If routes are added before `freeze()`, OxyRoute invalidates the previous compiled snapshot and
30+
lazily rebuilds it on the next request.
31+
2432
## OpenAPI and discovery
2533

2634
If OpenAPI is enabled, route registration also updates a minimal OpenAPI document. See [openapi.md](openapi.md).

src/dispatch.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@ async fn send_python_error(
145145
send_internal_error(protocol, method, path, err).await
146146
}
147147

148+
fn ensure_compiled_snapshot(state: &Arc<RwLock<AppState>>) {
149+
let mut st = state.write();
150+
if st.compiled.is_none() {
151+
st.compiled = Some(Arc::new(st.snapshot_routers()));
152+
}
153+
}
154+
148155
pub async fn run_rsgi(
149156
state: Arc<RwLock<AppState>>,
150157
scope: Py<PyAny>,
@@ -249,6 +256,9 @@ pub async fn run_rsgi(
249256
return send_handler_map(&protocol, is_head, mapped).await;
250257
}
251258
}
259+
// Auto-enable compiled route snapshot on first request to keep hot-path matching lock-free
260+
// even when users forget to call `freeze()` explicitly.
261+
ensure_compiled_snapshot(&state);
252262
let route_out: Option<(usize, HashMap<String, String>)> = {
253263
let st = state.read();
254264
match match_route(&st, &method, &path) {

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,8 @@ impl App {
277277
m.insert(&path, idx)
278278
.map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("{e}")))?;
279279
}
280+
// Keep auto-compiled routing snapshots fresh when routes are added before explicit freeze().
281+
st.compiled = None;
280282
Ok(())
281283
}
282284

tests/test_routing_autocompile.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""Issue #75: routing snapshot auto-compiles on first request."""
2+
3+
from __future__ import annotations
4+
5+
import asyncio
6+
7+
import httpx
8+
from oxyroute import App
9+
10+
11+
def test_routes_added_after_first_request_still_resolve() -> None:
12+
app = App()
13+
14+
@app.get("/a")
15+
def a() -> str:
16+
return "a"
17+
18+
async def _run() -> None:
19+
tr = httpx.ASGITransport(app=app)
20+
async with httpx.AsyncClient(transport=tr, base_url="http://test") as c:
21+
r1 = await c.get("/a")
22+
assert r1.status_code == 200
23+
assert r1.text == "a"
24+
25+
# This route is registered after first request/auto-compile snapshot.
26+
@app.get("/b")
27+
def b() -> str:
28+
return "b"
29+
30+
r2 = await c.get("/b")
31+
assert r2.status_code == 200
32+
assert r2.text == "b"
33+
34+
asyncio.run(_run())
35+
36+
37+
def test_autocompile_keeps_405_behavior() -> None:
38+
app = App()
39+
40+
@app.post("/x")
41+
def x() -> str:
42+
return "ok"
43+
44+
async def _run() -> None:
45+
tr = httpx.ASGITransport(app=app)
46+
async with httpx.AsyncClient(transport=tr, base_url="http://test") as c:
47+
r = await c.get("/x")
48+
assert r.status_code == 405
49+
allow = (r.headers.get("allow") or "").upper()
50+
assert "POST" in allow
51+
52+
asyncio.run(_run())

0 commit comments

Comments
 (0)