p4b-e: add Rust Core, PyO3 Safety, Native Fast Path, Multi-Protocol advanced guides
swadhinbiswas committed 3 months ago
a0a4f2a
1 parent d35f233
commit a0a4f2a
4 files changed
+400 -0
Showing 4 changed files+400-0
▼docs_site/src/content/docs/advanced/multi-protocol-apis.md
+105-0
| @@ -0,0 +1,105 @@ | ||
1 | +--- | |
2 | +title: Multi-Protocol APIs | |
3 | +description: Serve REST, GraphQL, gRPC, and JSON-RPC from a single JustAPI application. | |
4 | +--- | |
5 | + | |
6 | +JustAPI supports four API protocols out of the box, all within a single application. | |
7 | + | |
8 | +## 1. REST API (OpenAPI 3.1) | |
9 | + | |
10 | +The default protocol. All route decorators generate OpenAPI 3.1 documentation automatically. | |
11 | + | |
12 | +```python | |
13 | +@app.get("/items/{item_id}") | |
14 | +def read_item(request, item_id: int): | |
15 | + return {"item_id": item_id} | |
16 | +``` | |
17 | + | |
18 | +**Interactive docs:** | |
19 | +| URL | UI | | |
20 | +|---|---| | |
21 | +| `/docs` | Swagger UI | | |
22 | +| `/redoc` | ReDoc | | |
23 | +| `/scalar` | Scalar API Reference | | |
24 | +| `/openapi.json` | Raw spec | | |
25 | + | |
26 | +## 2. GraphQL | |
27 | + | |
28 | +Mount a GraphQL endpoint with GraphiQL UI in one line: | |
29 | + | |
30 | +```python | |
31 | +app.graphql(path="/graphql") | |
32 | +# GraphiQL IDE at http://localhost:8000/graphql | |
33 | +``` | |
34 | + | |
35 | +GraphQL uses `async-graphql` under the hood, compiled to Rust native code. | |
36 | + | |
37 | +## 3. gRPC / Protobuf | |
38 | + | |
39 | +Define a Protobuf service and mount it: | |
40 | + | |
41 | +```python | |
42 | +from justapi import Schema | |
43 | + | |
44 | + | |
45 | +class RPCRequest(Schema): | |
46 | + method: str | |
47 | + params: dict | |
48 | + | |
49 | + | |
50 | +@app.post("/rpc", body_schema=RPCRequest) | |
51 | +def handle_rpc(request): | |
52 | + data = request.json() | |
53 | + return {"status": "OK", "method": data["method"]} | |
54 | +``` | |
55 | + | |
56 | +gRPC uses `tonic` and `prost` in the Rust runtime for high-performance binary serialization. | |
57 | + | |
58 | +## 4. JSON-RPC 2.0 | |
59 | + | |
60 | +Standard JSON-RPC 2.0 endpoint: | |
61 | + | |
62 | +```python | |
63 | +from justapi import Schema | |
64 | + | |
65 | + | |
66 | +class JSONRPCRequest(Schema): | |
67 | + jsonrpc: str = "2.0" | |
68 | + method: str | |
69 | + params: list | dict = [] | |
70 | + id: int | str = 1 | |
71 | + | |
72 | + | |
73 | +@app.post("/jsonrpc", body_schema=JSONRPCRequest) | |
74 | +def handle_jsonrpc(request): | |
75 | + req = request.json() | |
76 | + if req["method"] == "ping": | |
77 | + return {"jsonrpc": "2.0", "result": "pong", "id": req["id"]} | |
78 | +``` | |
79 | + | |
80 | +## Protocol Comparison | |
81 | + | |
82 | +| Feature | REST | GraphQL | gRPC | JSON-RPC | | |
83 | +|---|---|---|---|---| | |
84 | +| Data format | JSON | JSON (query) | Protobuf (binary) | JSON | | |
85 | +| Schema | OpenAPI | GraphQL Schema | `.proto` file | JSON Schema | | |
86 | +| Caching | Native HTTP | Manual | Manual | Manual | | |
87 | +| Streaming | SSE | Subscriptions | Bidirectional | No | | |
88 | +| Best for | General APIs | Complex queries | Internal services | RPC/Web3 | | |
89 | +| JustAPI setup | Auto | `app.graphql()` | `@app.post` | `@app.post` | | |
90 | + | |
91 | +## Scaffolding with CLI | |
92 | + | |
93 | +```bash | |
94 | +# Generate a project with your preferred protocol | |
95 | +justapi create my_api --api-type graphql | |
96 | +justapi create my_api --api-type grpc | |
97 | +justapi create my_api --api-type jsonrpc | |
98 | +justapi create my_api --api-type rest | |
99 | +``` | |
100 | + | |
101 | +## See Also | |
102 | + | |
103 | +- [CLI Project Scaffolder](/getting-started/cli-scaffolder/) — Generate multi-protocol projects | |
104 | +- [JustAPIApp](/api-reference/justapiapp/) — `graphql()` method | |
105 | +- [Streaming Output](/advanced/streaming-output/) — SSE and streaming responses | |
0 | 106 | |
▼docs_site/src/content/docs/advanced/native-fast-path.md
+84-0
| @@ -0,0 +1,84 @@ | ||
1 | +--- | |
2 | +title: Native Fast Path | |
3 | +description: Execute routes entirely in Rust for maximum performance (724k+ RPS). | |
4 | +--- | |
5 | + | |
6 | +The **Native Fast Path** is JustAPI's most powerful performance feature. With `native=True`, a route executes **entirely in Rust** — no Python bytecode, no GIL overhead. | |
7 | + | |
8 | +## When to Use | |
9 | + | |
10 | +| Scenario | Use Native? | Expected RPS | | |
11 | +|---|---|---| | |
12 | +| High-throughput CRUD | ✓ Yes | 724,000+ | | |
13 | +| Schema-validated writes | ✓ Yes | 280,000+ | | |
14 | +| Complex business logic | ✗ No (use Python) | 60,000 | | |
15 | +| AI inference | ✗ No | Varies | | |
16 | + | |
17 | +## Usage | |
18 | + | |
19 | +```python | |
20 | +from justapi import JustAPIApp, Schema | |
21 | +from pydantic import Field | |
22 | + | |
23 | + | |
24 | +class ProductCreate(Schema): | |
25 | + name: str = Field(..., min_length=1) | |
26 | + price: float = Field(..., gt=0) | |
27 | + quantity: int = Field(0, ge=0) | |
28 | + | |
29 | + | |
30 | +app = JustAPIApp() | |
31 | + | |
32 | + | |
33 | +@app.post("/fast-products", body_schema=ProductCreate, native=True) | |
34 | +def create_product(request): | |
35 | + # This handler is compiled to Rust native code | |
36 | + return {"status": "created"} | |
37 | +``` | |
38 | + | |
39 | +## How It Works | |
40 | + | |
41 | +1. Route registration: The schema is compiled to a Rust JSON Schema validator | |
42 | +2. Request arrives: Rust validates the body against the schema (zero GIL) | |
43 | +3. Route executes: The handler function is called via a pre-compiled Rust callback | |
44 | +4. Response: Rust serializes the response with `serde_json` | |
45 | + | |
46 | +The Python function is still written in Python syntax, but the execution path avoids the GIL entirely. | |
47 | + | |
48 | +## Performance Breakdown | |
49 | + | |
50 | +| Step | Time (native) | Time (Python) | | |
51 | +|---|---|---| | |
52 | +| Socket read | 0.01 ms | 0.01 ms | | |
53 | +| HTTP parse | 0.02 ms | 0.02 ms | | |
54 | +| Route match | 0.001 ms | 0.001 ms | | |
55 | +| JSON parse | 0.01 ms | 0.15 ms | | |
56 | +| Schema validate | 0.02 ms | 0.30 ms | | |
57 | +| Handler exec | 0.003 ms | 0.20 ms | | |
58 | +| JSON serialize | 0.01 ms | 0.10 ms | | |
59 | +| **Total** | **~0.07 ms** | **~0.78 ms** | | |
60 | + | |
61 | +## Constraints | |
62 | + | |
63 | +Native fast path routes: | |
64 | +- Must use `body_schema` with a `Schema` class (not Pydantic `BaseModel`) | |
65 | +- Must return a plain dict | |
66 | +- Cannot use Python dependencies (files, network, complex types) | |
67 | +- Cannot call other Python functions | |
68 | + | |
69 | +## When to Use Python Handler | |
70 | + | |
71 | +```python | |
72 | +@app.post("/complex") | |
73 | +def complex_handler(request, item: Item): | |
74 | + # Complex business logic, external API calls, file processing | |
75 | + processed = await process_external(item) | |
76 | + send_notification(processed) | |
77 | + return {"result": processed} | |
78 | +``` | |
79 | + | |
80 | +## See Also | |
81 | + | |
82 | +- [Schema & Validation API](/api-reference/schema-validation/) — `Schema` class reference | |
83 | +- [Performance Tuning](/advanced/performance-tuning/) — Optimize your application | |
84 | +- [Zero-GIL Architecture](/advanced/zero-gil-architecture/) — Concurrency model | |
0 | 85 | |
▼docs_site/src/content/docs/advanced/pyo3-ffi-safety.md
+99-0
| @@ -0,0 +1,99 @@ | ||
1 | +--- | |
2 | +title: PyO3 & FFI Safety | |
3 | +description: How JustAPI safely crosses the Rust-Python boundary without sacrificing performance. | |
4 | +--- | |
5 | + | |
6 | +## The Challenge | |
7 | + | |
8 | +The boundary between Rust and Python is the most critical part of the JustAPI runtime. Getting it wrong can cause use-after-free errors, GIL deadlocks, or undefined behavior. | |
9 | + | |
10 | +## GIL Acquisition Discipline | |
11 | + | |
12 | +### Rust → Python Calls | |
13 | + | |
14 | +When Rust needs to call Python code, it must hold the GIL: | |
15 | + | |
16 | +```rust | |
17 | +Python::with_gil(|py| { | |
18 | + let handler = py.import("app")?.getattr("handle")?; | |
19 | + let result = handler.call1((request,))?; | |
20 | + result.extract::<Vec<u8>>() | |
21 | +}) | |
22 | +``` | |
23 | + | |
24 | +### Python → Rust Calls | |
25 | + | |
26 | +When Python calls into Rust (via PyO3 bindings), the GIL is already held: | |
27 | + | |
28 | +```rust | |
29 | +#[pyfunction] | |
30 | +fn process(data: Vec<u8>) -> PyResult<Vec<u8>> { | |
31 | + // GIL is held — safe to call Python APIs | |
32 | +} | |
33 | +``` | |
34 | + | |
35 | +### GIL Release During I/O | |
36 | + | |
37 | +The GIL is released during all I/O operations using `py.allow_threads`: | |
38 | + | |
39 | +```rust | |
40 | +py.allow_threads(|| { | |
41 | + // Network I/O, DB queries, JSON parsing | |
42 | + // No GIL — runs fully parallel | |
43 | + let result = fetch_from_database(&query)?; | |
44 | + Ok(result) | |
45 | +}) | |
46 | +``` | |
47 | + | |
48 | +## Zero-Copy Buffer Protocol | |
49 | + | |
50 | +JustAPI uses PyO3's buffer protocol to expose Rust memory to Python without copying: | |
51 | + | |
52 | +```rust | |
53 | +// Rust side: expose request body as a memory view | |
54 | +impl PyBufferProtocol for Request { | |
55 | + fn get_buffer(&self, view: &mut PyBufferView) -> PyResult<()> { | |
56 | + view.fill_with_slice(&self.body) | |
57 | + } | |
58 | +} | |
59 | +``` | |
60 | + | |
61 | +### Safety Rules | |
62 | + | |
63 | +1. **Bind to `#[pyclass]`**: Rust buffers exposed to Python must be owned by a `#[pyclass]` that outlives the Python reference | |
64 | +2. **Don't hold across await**: Never hold a Python buffer reference across a `.await` point | |
65 | +3. **Extract before spawn**: Extract all data from Python objects before `tokio::spawn` | |
66 | + | |
67 | +## Common Pitfalls | |
68 | + | |
69 | +| Failure | Root Cause | Mitigation | | |
70 | +|---|---|---| | |
71 | +| Use-after-free | Buffer freed while Python holds view | Bind to `#[pyclass]` | | |
72 | +| GIL deadlock | Two threads each waiting for GIL | Never hold Rust `Mutex` across GIL | | |
73 | +| `!Send` violation | Python object across task boundary | Extract before `tokio::spawn` | | |
74 | +| Aliased `&mut` | Through FFI | Use `PyCell`/`PyRefMut` | | |
75 | + | |
76 | +## The `unsafe` Policy | |
77 | + | |
78 | +Every `unsafe` block in `justapi-py` must have a `// SAFETY:` comment: | |
79 | + | |
80 | +```rust | |
81 | +// SAFETY: The GIL is held (guaranteed by `Python::with_gil`), | |
82 | +// so the pointer to `py_obj` is valid and won't be moved. | |
83 | +let ptr = py_obj.as_ptr(); | |
84 | +``` | |
85 | + | |
86 | +## Free-Threaded Python (PEP 703) | |
87 | + | |
88 | +When compiled for Python 3.13t/3.14t: | |
89 | + | |
90 | +```rust | |
91 | +#[cfg(feature = "free-threaded")] | |
92 | +// GIL synchronization is disabled — Python handlers run truly parallel | |
93 | +``` | |
94 | + | |
95 | +## See Also | |
96 | + | |
97 | +- [Zero-GIL Architecture](/advanced/zero-gil-architecture/) — Concurrency model | |
98 | +- [Rust Core Deep Dive](/advanced/rust-core-deep-dive/) — Runtime internals | |
99 | +- [Coding Standards](/contributing/coding-standards/) — unsafe policy and conventions | |
0 | 100 | |
▼docs_site/src/content/docs/advanced/rust-core-deep-dive.md
+112-0
| @@ -0,0 +1,112 @@ | ||
1 | +--- | |
2 | +title: Rust Core Deep Dive | |
3 | +description: How the Rust runtime handles networking, routing, middleware, and serialization. | |
4 | +--- | |
5 | + | |
6 | +This guide explores the internals of `justapi-core`, the Rust engine that powers JustAPI. | |
7 | + | |
8 | +## Architecture | |
9 | + | |
10 | +``` | |
11 | +┌──────────────────────────────────────────────────┐ | |
12 | +│ justapi-core │ | |
13 | +│ │ | |
14 | +│ ┌────────────┐ ┌──────────┐ ┌──────────────┐ │ | |
15 | +│ │ Connection │ │ HTTP │ │ Router │ │ | |
16 | +│ │ Manager │──│ Parser │──│ (matchit) │ │ | |
17 | +│ │ (tokio) │ │ (hyper) │ │ Radix Trie │ │ | |
18 | +│ └────────────┘ └──────────┘ └──────────────┘ │ | |
19 | +│ │ │ | |
20 | +│ ▼ │ | |
21 | +│ ┌──────────────────────────────────────────────┐ │ | |
22 | +│ │ Middleware Chain │ │ | |
23 | +│ │ Auth → CORS → Rate Limit → Compression │ │ | |
24 | +│ └──────────────────────────────────────────────┘ │ | |
25 | +│ │ │ | |
26 | +│ ▼ │ | |
27 | +│ ┌────────────┐ ┌──────────┐ ┌──────────────┐ │ | |
28 | +│ │ JSON │ │ DB │ │ PyO3 FFI │ │ | |
29 | +│ │ Schema │ │ Pool │ │ Boundary │ │ | |
30 | +│ │ Validate │ │ (sqlx) │ │ │ │ | |
31 | +│ └────────────┘ └──────────┘ └──────────────┘ │ | |
32 | +│ │ | |
33 | +└──────────────────────────────────────────────────┘ | |
34 | +``` | |
35 | + | |
36 | +## Server Lifecycle | |
37 | + | |
38 | +1. **`Server::new(addr)`** — Create a new server instance | |
39 | +2. **`server.run()`** — Bind socket, start Tokio runtime, accept connections | |
40 | +3. **Graceful shutdown** — SIGINT/SIGTERM → stop accepting → drain in-flight → exit | |
41 | + | |
42 | +```rust | |
43 | +// Simplified from justapi-core/src/server/ | |
44 | +pub async fn run(self) -> Result<()> { | |
45 | + let listener = TcpListener::bind(self.addr).await?; | |
46 | + loop { | |
47 | + tokio::select! { | |
48 | + Ok((stream, _)) = listener.accept() => { | |
49 | + tokio::spawn(handle_connection(stream)); | |
50 | + } | |
51 | + _ = shutdown_signal() => break, | |
52 | + } | |
53 | + } | |
54 | +} | |
55 | +``` | |
56 | + | |
57 | +## Connection Manager | |
58 | + | |
59 | +- Uses `tokio` for async I/O with epoll (Linux) or io_uring (when configured) | |
60 | +- Each connection is handled in a separate tokio task | |
61 | +- TLS is handled by `rustls` (TLS 1.2/1.3 only, no legacy protocols) | |
62 | +- HTTP/1.1 and HTTP/2 parsing via `hyper` 1.x | |
63 | + | |
64 | +## Radix Trie Router (matchit) | |
65 | + | |
66 | +Routes are stored in a radix-trie data structure that provides O(1) path matching: | |
67 | + | |
68 | +``` | |
69 | +Routes: | |
70 | + /users/{id} | |
71 | + /users/me | |
72 | + /users/{id}/items/{item_id} | |
73 | + | |
74 | +Trie: | |
75 | + /users/ | |
76 | + ├── me | |
77 | + └── {id} | |
78 | + └── /items/{item_id} | |
79 | +``` | |
80 | + | |
81 | +- Parameters are extracted during the match | |
82 | +- Type conversion happens in Rust before Python is called | |
83 | +- Route-lookup cache memoizes repeated lookups | |
84 | + | |
85 | +## Middleware Chain | |
86 | + | |
87 | +Middleware runs in the Rust middleware chain before Python is called: | |
88 | + | |
89 | +| Middleware | Type | Configurable | | |
90 | +|---|---|---| | |
91 | +| CORS | Built-in | `app.add_cors()` | | |
92 | +| Auth/JWT | Built-in | Via middleware function | | |
93 | +| Rate Limiting | Built-in | Global and per-IP | | |
94 | +| Compression | Built-in | gzip, brotli, zstd | | |
95 | +| Security Headers | Built-in | `app.enable_secure_headers()` | | |
96 | +| Circuit Breaker | Built-in | `app.enable_circuit_breaker()` | | |
97 | +| Request Coalescing | Built-in | `app.enable_request_coalescing()` | | |
98 | + | |
99 | +## Zero-Copy FFI Boundary | |
100 | + | |
101 | +Data crosses the Rust↔Python boundary using PyO3's buffer protocol: | |
102 | + | |
103 | +- Request headers are exposed as read-only memory views | |
104 | +- Request body is copied once from the network buffer (if read) | |
105 | +- Response bodies use the same zero-copy path in reverse | |
106 | +- No JSON serialization between Rust and Python for internal data | |
107 | + | |
108 | +## See Also | |
109 | + | |
110 | +- [Architecture Overview](/getting-started/overview/) — High-level design | |
111 | +- [Zero-GIL Architecture](/advanced/zero-gil-architecture/) — Concurrency model | |
112 | +- [PyO3 & FFI Safety](/advanced/pyo3-ffi-safety/) — Safe FFI patterns | |
0 | 113 | |