ea6f567
commit ea6f567
7 files changed
+3593 -0
Showing 7 changed files+3593-0
▼AGENTS.md
+131-0
| @@ -0,0 +1,131 @@ | ||
1 | +# AGENTS.md | |
2 | + | |
3 | +> Operating rules for anyone — human or AI — working on justapi Runtime. | |
4 | +> Read this before touching code. If you are an AI agent resuming after a | |
5 | +> context reset, read this file and `PLAN.md` first, in that order. | |
6 | + | |
7 | +## 1. The one rule that overrides everything else | |
8 | + | |
9 | +Do not begin a phase in `PLAN.md` until the previous phase's exit criteria | |
10 | +are checked off there. If you're unsure whether a gate passed, check | |
11 | +`BENCHMARKS.md` and CI history — don't assume. | |
12 | + | |
13 | +## 2. Roles | |
14 | + | |
15 | +Even in single-agent execution, work through these lenses in sequence for | |
16 | +any non-trivial change — they catch different classes of mistakes: | |
17 | + | |
18 | +- **Core Runtime** — networking, protocol, memory, scheduler correctness and | |
19 | + safety (`justapi-core`). | |
20 | +- **Python Bindings** — PyO3 boundary, GIL/free-threading correctness, the | |
21 | + ASGI shim, native API (`justapi-py`). | |
22 | +- **Protocol/Security** — TLS, auth, JWT, OAuth2/OIDC, request validation, | |
23 | + SAST scanning, fuzzing, anything parsing untrusted input. | |
24 | + Every change here needs a fuzz target or an explanation of why not. | |
25 | +- **Data & Validation** — Pydantic-style schemas, DB ORM integration, | |
26 | + serialization, type-safe request/response contracts, background tasks. | |
27 | +- **Resilience** — circuit breakers, graceful degradation, retry policies, | |
28 | + rate-limit backpressure, connection pooling. | |
29 | +- **QA/Benchmark** — owns `BENCHMARKS.md`, the CI regression gate, and | |
30 | + calling out unqualified performance claims. Target: beat FastAPI on every | |
31 | + benchmark. | |
32 | +- **Platform/DX** — CLI, hot reload, OpenAPI generation, Docker/K8s tooling, | |
33 | + multi-cloud deployment, `skills/` directory upkeep. | |
34 | +- **Observability** — distributed tracing (OTel), structured logging, metric | |
35 | + dashboards, health checks, alerting integration. | |
36 | + | |
37 | +## 3. Conventions | |
38 | + | |
39 | +- **Commits:** `<phase>: <what changed>` e.g. `p3: add per-request arena | |
40 | + allocator`. One logical change per commit. | |
41 | +- **Branches:** `phase-N/<short-description>`. | |
42 | +- **`unsafe` code:** every block needs a `// SAFETY:` comment explaining the | |
43 | + invariant that makes it sound, and a test that would fail if the invariant | |
44 | + were violated where feasible. | |
45 | +- **New dependency:** justify it in `DECISIONS.md` — what it replaces or | |
46 | + enables, why the alternative (usually: writing it yourself) is worse. | |
47 | +- **Public API changes:** rustdoc/docstrings required before merge, not after. | |
48 | +- **Error handling:** use `anyhow::Result` in application code (CLI, bench); | |
49 | + use typed errors (`thiserror`) in library crates (`justapi-core`, | |
50 | + `justapi-py`) for errors that cross crate boundaries. | |
51 | + | |
52 | +## 4. Before opening a PR / calling a phase done | |
53 | + | |
54 | +- [ ] Tests pass (`cargo test --workspace`) | |
55 | +- [ ] `cargo clippy --workspace --tests -- -D warnings` clean (includes test code) | |
56 | +- [ ] `cargo fmt --check` clean | |
57 | +- [ ] `cargo miri test -p justapi-core` clean (if core touched) | |
58 | +- [ ] Benchmarks run, numbers appended to `BENCHMARKS.md` (not overwritten) | |
59 | +- [ ] No p99 regression >5% vs. previous phase without a `DECISIONS.md` entry | |
60 | +- [ ] `PLAN.md` updated: phase status, next steps | |
61 | +- [ ] New recurring pattern or gotcha? Add/update a `skills/*/SKILL.md` | |
62 | + | |
63 | +## 5. Resuming work after a context reset | |
64 | + | |
65 | +1. Read `PLAN.md` — find the current phase and its status. | |
66 | +2. Read the most recent `DECISIONS.md` entries for context on why things are | |
67 | + the way they are. | |
68 | +3. Skim relevant `skills/*/SKILL.md` files for the area you're touching. | |
69 | +4. Check `BENCHMARKS.md` for the last recorded numbers so you know the bar. | |
70 | +5. Continue from where `PLAN.md` says to continue — don't restart planning | |
71 | + from scratch. | |
72 | + | |
73 | +## 6. When something in the master prompt seems wrong | |
74 | + | |
75 | +It might be. If a requirement conflicts with something you've learned during | |
76 | +implementation (e.g., a benchmark shows a "required" custom scheduler isn't | |
77 | +needed, or a crate assumption in the master prompt is stale), write the | |
78 | +conflict and your recommendation into `DECISIONS.md` and proceed with the | |
79 | +better-informed choice. Don't silently deviate, and don't blindly follow a | |
80 | +now-wrong instruction either. | |
81 | + | |
82 | +## 7. Architecture quick reference | |
83 | + | |
84 | +``` | |
85 | +justapi/ | |
86 | + Cargo.toml # workspace root | |
87 | + crates/ | |
88 | + justapi-core/ # networking, protocol, scheduler, memory | |
89 | + justapi-py/ # PyO3 bindings, ASGI shim, native API | |
90 | + justapi-cli/ # `justapi` CLI binary | |
91 | + justapi-bench/ # internal benchmark harness | |
92 | + python/ | |
93 | + justapi/ # pip-installable Python package (maturin-built) | |
94 | + skills/ # reusable knowledge modules | |
95 | + benchmarks/ # workload scripts for baseline comparisons | |
96 | + AGENTS.md # this file | |
97 | + PLAN.md # living roadmap (single source of truth) | |
98 | + DECISIONS.md # append-only ADR log | |
99 | + BENCHMARKS.md # append-only results ledger | |
100 | + PROMPT.md # master engineering prompt (reference only) | |
101 | +``` | |
102 | + | |
103 | +### Request pipeline (target shape) | |
104 | + | |
105 | +``` | |
106 | +Kernel → epoll/io_uring → Connection Manager → tokio task | |
107 | + → TLS (rustls) → HTTP parse (hyper) → Router (matchit) | |
108 | + → Middleware chain (Rust: auth/CORS/rate-limit/compression) | |
109 | + → Python boundary (typed, zero-copy request view via PyO3 buffer protocol) | |
110 | + → Application code (ASGI shim OR native handler) | |
111 | + → Rust serializer (serde_json/simd-json) | |
112 | + → Response write → Socket | |
113 | +``` | |
114 | + | |
115 | +### Crate dependency graph | |
116 | + | |
117 | +``` | |
118 | +justapi-cli → justapi-core | |
119 | +justapi-py → justapi-core | |
120 | +justapi-bench → justapi-core | |
121 | +``` | |
122 | + | |
123 | +## 8. Key skills to consult | |
124 | + | |
125 | +| Area | Skill file | | |
126 | +|---|---| | |
127 | +| PyO3 / GIL / unsafe at the FFI boundary | `skills/rust-ffi-safety/SKILL.md` | | |
128 | +| Running & interpreting benchmarks | `skills/benchmark-harness/SKILL.md` | | |
129 | + | |
130 | +Add new skills as patterns emerge. A good skill saves the next session from | |
131 | +rediscovering a non-obvious invariant or workflow. | |
0 | 132 | |
▼DECISIONS.md
+1481-0
| @@ -0,0 +1,1481 @@ | ||
1 | +# DECISIONS.md | |
2 | + | |
3 | +> Append-only architecture decision record (ADR) log. | |
4 | +> Every deviation from PROMPT.md, every crate choice, every GIL/concurrency | |
5 | +> decision gets a dated entry: context, options considered, decision, and | |
6 | +> the evidence behind it. | |
7 | + | |
8 | +--- | |
9 | + | |
10 | +## ADR-001 — 2026-07-03 — Crate defaults for v1.0 | |
11 | + | |
12 | +**Context:** Phase 0 bootstrap. Choosing the dependency stack before any | |
13 | +implementation code, following PROMPT.md Section 1's "build vs. reinvent" | |
14 | +philosophy. | |
15 | + | |
16 | +**Options considered:** | |
17 | +1. Hand-roll everything (HTTP parser, TLS, scheduler) — rejected per | |
18 | + Section 1; produces unmaintainable half-finished subsystems. | |
19 | +2. Use mature, audited crates per Section 1's table — selected. | |
20 | + | |
21 | +**Decision:** Default to the crate table from PROMPT.md Section 1: | |
22 | + | |
23 | +| Concern | Crate | Version | | |
24 | +|---|---|---| | |
25 | +| Async runtime | `tokio` (multi-thread) | 1.x | | |
26 | +| HTTP/1.1 + HTTP/2 | `hyper` + `hyper-util` + `http-body-util` | 1.x | | |
27 | +| TLS | `rustls` | (Phase 7) | | |
28 | +| Python bindings | `pyo3` | 0.29.x | | |
29 | +| Routing | `matchit` | (Phase 2, evaluate vs custom trie) | | |
30 | +| JSON | `serde_json` baseline, `simd-json` feature-flagged | 1.x | | |
31 | +| Rate limiting | `governor` | (Phase 5) | | |
32 | +| JWT | `jsonwebtoken` | (Phase 5) | | |
33 | +| CLI | `clap` | 4.x | | |
34 | +| Logging/tracing | `tracing` + `tracing-subscriber` | 0.1 / 0.3 | | |
35 | +| Error handling | `anyhow` (app code), `thiserror` (library boundaries) | 1.x | | |
36 | + | |
37 | +Custom replacements only after profiling shows the FFI boundary is the | |
38 | +bottleneck, with evidence attached to a new DECISIONS.md entry. | |
39 | + | |
40 | +**Evidence:** PROMPT.md Section 1 crate table. No profiling data yet. | |
41 | + | |
42 | +--- | |
43 | + | |
44 | +## ADR-002 — 2026-07-03 — Repository skeleton layout | |
45 | + | |
46 | +**Context:** PROMPT.md Section 5.1 specifies the repo layout. Workspace | |
47 | +root is `/home/swadhin/RastAPI/` (directory name differs from the canonical | |
48 | +"justapi/" but crate names follow the spec). | |
49 | + | |
50 | +**Decision:** Follow Section 5.1 layout exactly: | |
51 | +- 4 crates: `justapi-core`, `justapi-py`, `justapi-cli`, `justapi-bench` | |
52 | +- Python package under `python/justapi/` built with maturin | |
53 | +- Skills under `skills/` | |
54 | + | |
55 | +No deviation from the specified structure. | |
56 | + | |
57 | +--- | |
58 | + | |
59 | +## ADR-003 — 2026-07-03 — hyper 1.x requires hyper-util | |
60 | + | |
61 | +**Context:** The original skeleton used `hyper::server::conn::http1::Builder` | |
62 | +which existed in hyper 0.14 but was moved to `hyper-util` in hyper 1.x. | |
63 | +The server wouldn't compile without this crate. | |
64 | + | |
65 | +**Options considered:** | |
66 | +1. Downgrade to hyper 0.14 — rejected; 0.14 is legacy, 1.x is the | |
67 | + maintained branch with the modular architecture we need. | |
68 | +2. Add `hyper-util` and `http-body-util` — selected; these are the official | |
69 | + companion crates for hyper 1.x server usage. | |
70 | + | |
71 | +**Decision:** Add `hyper-util` (features: `tokio`, `http1`, `server`) and | |
72 | +`http-body-util` as workspace dependencies. The server connection handling | |
73 | +uses `hyper_util::server::conn::http1::Builder` and | |
74 | +`hyper_util::rt::TokioIo` for the stream wrapper. | |
75 | + | |
76 | +**Evidence:** hyper 1.0 migration guide, `hyper-util` crate documentation. | |
77 | + | |
78 | +--- | |
79 | + | |
80 | +## ADR-004 — 2026-07-03 — PyO3 version: 0.29.x for Python 3.14 support | |
81 | + | |
82 | +**Context:** The development machine runs Python 3.14.6. PyO3 0.23.x does | |
83 | +not support Python 3.14. PyO3 0.24.x also failed to compile against | |
84 | +Python 3.14.6 — full support required 0.29. | |
85 | + | |
86 | +**Options considered:** | |
87 | +1. pyo3 0.23 — rejected; no Python 3.14 support. | |
88 | +2. pyo3 0.24 — rejected; still failed to compile against Python 3.14.6. | |
89 | +3. pyo3 0.29 — selected; compiles successfully against Python 3.14.6. | |
90 | + | |
91 | +**Decision:** Upgrade to `pyo3 = "0.29"` with `extension-module` feature. | |
92 | + | |
93 | +**Evidence:** Compilation failure with 0.23 and 0.24; successful build | |
94 | +with 0.29 on Python 3.14.6. | |
95 | + | |
96 | +--- | |
97 | + | |
98 | +## ADR-005 — 2026-07-03 — Rust edition: 2021 (downgraded from 2024) | |
99 | + | |
100 | +**Context:** The original skeleton specified `edition = "2024"`. While the | |
101 | +Rust toolchain (1.96.0) supports it, edition 2024 caused compatibility | |
102 | +issues with some dependency crates during the bootstrap build. | |
103 | + | |
104 | +**Options considered:** | |
105 | +1. Keep edition 2024 and fix all dependency compatibility — higher risk, | |
106 | + delays Phase 0 for no user-facing benefit. | |
107 | +2. Downgrade to edition 2021 — selected; maximizes dependency compatibility, | |
108 | + can upgrade later when the ecosystem catches up. | |
109 | + | |
110 | +**Decision:** Use `edition = "2021"`. Revisit upgrading to 2024 once all | |
111 | +dependencies (especially pyo3, hyper-util) have confirmed 2024 support. | |
112 | + | |
113 | +**Evidence:** Build failures during Phase 0 bootstrap with edition 2024. | |
114 | + | |
115 | +--- | |
116 | + | |
117 | +## ADR-006 — 2026-07-03 — Benchmark tooling: oha over wrk | |
118 | + | |
119 | +**Context:** PROMPT.md Section 10.1 recommends `oha` or `wrk`. Both are | |
120 | +HTTP load generators. | |
121 | + | |
122 | +**Options considered:** | |
123 | +1. `wrk` — mature, Lua scriptable, but reports only percentiles (not | |
124 | + histogram), harder to install on some distros. | |
125 | +2. `oha` — Rust-based, outputs JSON with full latency distribution, easy | |
126 | + to install via cargo, reports p50/p95/p99/p999 natively. | |
127 | + | |
128 | +**Decision:** Primary tool is `oha`. `wrk` as a secondary validation tool | |
129 | +if needed. `oha` version and exact command lines recorded in BENCHMARKS.md | |
130 | +alongside each run. | |
131 | + | |
132 | +**Evidence:** `oha` natively outputs the exact metrics BENCHMARKS.md needs | |
133 | +(p50/p95/p99, req/sec) in both human-readable and JSON formats. | |
134 | + | |
135 | +--- | |
136 | + | |
137 | +## ADR-007 — 2026-07-03 — ASGI strategy: Tier A + Tier B per Section 2 | |
138 | + | |
139 | +**Context:** PROMPT.md Section 2 requires both an ASGI compatibility shim | |
140 | +(Tier A) and a native justapi API (Tier B). Tier A is required for v1.0; | |
141 | +Tier B is the performance ceiling. | |
142 | + | |
143 | +**Decision:** Both tiers ship. Development order: | |
144 | +- Phases 1–9: Build the core runtime using Tier B (native) API internally | |
145 | +- Phase 10: Implement Tier A ASGI shim on top of the established core | |
146 | + | |
147 | +Rationale: The native API exercises the full pipeline and establishes | |
148 | +performance baselines. The ASGI shim is an adapter on top — building it | |
149 | +first would mean building the adapter before the thing it adapts. | |
150 | + | |
151 | +--- | |
152 | + | |
153 | +## ADR-008 — 2026-07-03 — GIL strategy: dedicated Python worker thread | |
154 | + | |
155 | +**Context:** Phase 4 design decision for the execution model. justapi's | |
156 | +architecture split (Rust owns I/O, Python executes application logic) means | |
157 | +we must decide how to bridge the two worlds. The Python GIL (Global | |
158 | +Interpreter Lock) prevents multiple native OS threads from executing Python | |
159 | +bytecode concurrently. We need a concurrency model that maximizes throughput | |
160 | +while staying safe and simple. | |
161 | + | |
162 | +**Options considered:** | |
163 | + | |
164 | +### Option A — Dedicated Python worker thread (selected) | |
165 | + | |
166 | +A single OS thread owns the Python interpreter and runs all Python handlers. | |
167 | +Requests arriving via Rust's tokio async runtime are dispatched to this | |
168 | +thread via a lock-free or channel-based mechanism (e.g., `tokio::sync::mpsc` | |
169 | +or a `flume` channel). The Rust side never acquires the GIL directly — it | |
170 | +sends a job and awaits the result. | |
171 | + | |
172 | +Key properties: | |
173 | +- **GIL contention: zero.** Exactly one thread ever touches Python. | |
174 | +- **Concurrency:** Rust I/O is fully concurrent (tokio multi-thread | |
175 | + scheduler). Python execution is serialized, which matches typical | |
176 | + web-app profiles (I/O-bound inside Python — waiting on DB, external APIs). | |
177 | +- **Safety:** Great. No chance of accidentally blocking the tokio runtime | |
178 | + with a slow Python call because Python runs on its own thread. | |
179 | +- **Complexity:** Low. A single producer-consumer channel. No GIL acquire/ | |
180 | + release logic scattered through the hot path. | |
181 | + | |
182 | +### Option B — Direct GIL acquisition on tokio worker threads | |
183 | + | |
184 | +Any tokio worker thread that needs to call Python acquires the GIL via | |
185 | +`Python::with_gil()`, runs the handler, then releases it. Multiple threads | |
186 | +may contend for the GIL. | |
187 | + | |
188 | +Key properties: | |
189 | +- **GIL contention:** High under load. If all N tokio workers are handling | |
190 | + requests that need Python, N-1 threads are blocked on the GIL. | |
191 | +- **Concurrency:** Only one thread runs Python at a time, so no throughput | |
192 | + gain vs Option A for Python-bound work. For Rust-only requests (static | |
193 | + files, caching layer, etc.), all threads are available — same as Option A. | |
194 | +- **Safety:** Moderate risk. A slow Python call blocks the tokio worker | |
195 | + thread, starving other tasks on that thread. Mitigation: use | |
196 | + `tokio::task::spawn_blocking` for Python calls, which moves them off the | |
197 | + async runtime. | |
198 | +- **Complexity:** Low in principle, but `spawn_blocking` + GIL interactions | |
199 | + need careful handling in the PyO3 boundary. | |
200 | + | |
201 | +### Option C — Sub-interpreters (PEP 684) | |
202 | + | |
203 | +Each connection or request task gets its own Python sub-interpreter, each | |
204 | +with its own GIL. True parallelism for Python code. | |
205 | + | |
206 | +Key properties: | |
207 | +- **GIL contention:** Zero per-sub-interpreter; multiple interpreters run | |
208 | + concurrently. | |
209 | +- **Concurrency:** True Python parallelism, but objects cannot be shared | |
210 | + across interpreter boundaries. This breaks almost every Python web | |
211 | + framework (they assume a single interpreter with shared globals). | |
212 | +- **Safety:** Low. Sub-interpreters in CPython have subtle isolation gaps. | |
213 | + PyO3 support is experimental and incomplete. | |
214 | +- **Complexity:** Very high. Every Python object crossing the boundary | |
215 | + must be serialized or copied. | |
216 | +- **Verdict:** Too immature for v1.0. Revisit for a future major version. | |
217 | + | |
218 | +### Option D — Free-threaded CPython (PEP 703, `--disable-gil`) | |
219 | + | |
220 | +CPython built with `--disable-gil` removes the GIL entirely. Multiple | |
221 | +threads can run Python bytecode simultaneously. | |
222 | + | |
223 | +Key properties: | |
224 | +- **GIL contention:** Zero (no GIL). | |
225 | +- **Concurrency:** True Python parallelism on CPU-bound workloads. | |
226 | +- **Safety:** Low-medium. `--disable-gil` is experimental in CPython 3.13. | |
227 | + PyO3's `nogil` support is preliminary. The broader Python C-extension | |
228 | + ecosystem is largely not thread-safe without the GIL. | |
229 | +- **Complexity:** Medium in PyO3 (needs `nogil` feature flag and careful | |
230 | + review of all unsafe code). High in the ecosystem. | |
231 | +- **Verdict:** Promising long-term, but not ready for production v1.0. | |
232 | + We will feature-flag support so early adopters can test. | |
233 | + | |
234 | +**Decision:** | |
235 | + | |
236 | +**Select Option A — dedicated Python worker thread** as the default for v1.0. | |
237 | + | |
238 | +Rationale: | |
239 | + | |
240 | +1. **Best match for workload profile.** Python web applications are | |
241 | + I/O-bound within Python (database queries, external HTTP calls). A | |
242 | + single Python thread driving async I/O via `asyncio` is the established | |
243 | + pattern (uvicorn, gunicorn with async workers). Serializing Python | |
244 | + execution doesn't bottleneck throughput — the bottleneck is Rust I/O | |
245 | + and the downstream services Python calls. | |
246 | + | |
247 | +2. **Runtime safety.** Python never blocks a tokio worker thread. Slow or | |
248 | + buggy Python code can't stall the I/O runtime. The Rust side remains | |
249 | + responsive regardless of Python behavior. | |
250 | + | |
251 | +3. **Simplest correctness model.** No GIL acquire/release logic in the hot | |
252 | + path. No risk of deadlocks involving the GIL + Rust locks. The channel | |
253 | + boundary provides natural backpressure: if Python is slow, Rust buffers | |
254 | + requests up to the channel capacity. | |
255 | + | |
256 | +4. **Clear upgrade path to Option D.** When free-threaded CPython matures, | |
257 | + the single-worker-thread is replaced by "acquire GIL on any thread" code | |
258 | + path behind a `py-nogil` feature flag. The channel dispatch layer is | |
259 | + removed and Python calls happen inline on tokio workers. | |
260 | + | |
261 | +**Implementation sketch:** | |
262 | + | |
263 | +```rust | |
264 | +// Python worker runs in a dedicated OS thread. | |
265 | +// Rust handler sends requests via channel, awaits result. | |
266 | +let (tx, rx) = tokio::sync::mpsc::channel::<PythonJob>(256); | |
267 | + | |
268 | +// Spawn Python worker | |
269 | +std::thread::spawn(move || { | |
270 | + Python::with_gil(|py| { | |
271 | + // Load ASGI app, register handlers | |
272 | + // Loop: receive jobs from rx, execute, send result back | |
273 | + }); | |
274 | +}); | |
275 | + | |
276 | +// In the request handler (tokio task): | |
277 | +let job = PythonJob { request, response_tx }; | |
278 | +tx.send(job).await?; | |
279 | +let response = response_tx.recv().await?; | |
280 | +``` | |
281 | + | |
282 | +The channel acts as both dispatch mechanism and backpressure valve. | |
283 | +Channel capacity (256 by default) limits in-flight Python work. If the | |
284 | +channel is full, the Rust side can either buffer (via the BufferPool) or | |
285 | +respond with 503 Service Unavailable. | |
286 | + | |
287 | +**Feature flag for PEP 703 (`py-nogil`):** | |
288 | + | |
289 | +A Cargo feature `py-nogil` switches the execution model: | |
290 | + | |
291 | +- **default (`py-nogil` off):** Dedicated Python worker thread (Option A). | |
292 | +- **`py-nogil` on:** Direct GIL acquisition on tokio workers (Option A | |
293 | + dispatch removed). Relies on CPython built with `--disable-gil` and | |
294 | + PyO3's `nogil` support. | |
295 | + | |
296 | +This flag is hidden/unstable until PEP 703 ecosystem maturity improves. | |
297 | +It exists to prevent architectural deadlock — we can evolve toward it | |
298 | +without rewriting the dispatch layer. | |
299 | + | |
300 | +**Evidence:** | |
301 | +- CPython PEP 703 (free-threaded CPython) status: experimental in 3.13, | |
302 | + targeting default-on in 3.14 or 3.15. | |
303 | +- PyO3 `nogil` feature tracking issue: currently experimental, API surface | |
304 | + unstable. | |
305 | +- Typical Python web workload profile: I/O-bound inside Python handlers | |
306 | + (database, cache, external API calls). CPU-bound Python workloads | |
307 | + (ML inference, image processing) are a niche use case for app servers | |
308 | + and should be offloaded to dedicated worker processes or task queues. | |
309 | +- Uvicorn + Gunicorn model: single Python thread per worker process, | |
310 | + achieving production throughput via process-level parallelism (not | |
311 | + thread-level). justapi's Rust I/O layer provides better single-process | |
312 | + throughput than Gunicorn's prefork model. | |
313 | + | |
314 | +--- | |
315 | + | |
316 | +## ADR-009 — 2026-07-03 — WebSocket: TCP-peek approach over hyper upgrade | |
317 | + | |
318 | +**Context:** Phase 8 WebSocket support. Hyper 1.x provides `hyper::upgrade::on(req)` | |
319 | +to upgrade HTTP/1.1 connections to WebSocket. This mechanism had persistent | |
320 | +`ResetWithoutClosingHandshake` errors with `tokio-tungstenite`. | |
321 | + | |
322 | +**Options considered:** | |
323 | + | |
324 | +### Option A — hyper upgrade mechanism (rejected) | |
325 | + | |
326 | +Call `hyper::upgrade::on(req)` inside the service function, return 101 | |
327 | +response, spawn task to handle upgraded stream. Required careful ordering: | |
328 | +the upgrade future must not be awaited inside the service function (deadlock), | |
329 | +and `WebSocketStream::from_raw_socket` + `Role::Server` had protocol errors. | |
330 | + | |
331 | +Issues encountered: | |
332 | +- `hyper::upgrade::on` returning `ResetWithoutClosingHandshake` consistently | |
333 | +- Double-wrapping `Upgraded` in `TokioIo` caused confusion about trait bounds | |
334 | +- Service function return + upgrade completion ordering fragile | |
335 | + | |
336 | +### Option B — TCP-peek approach (selected) | |
337 | + | |
338 | +Peek at the first bytes of the TCP connection to detect WebSocket upgrade | |
339 | +requests before passing the stream to hyper. If the request looks like a | |
340 | +WebSocket upgrade, use `tokio_tungstenite::accept_async` directly on the | |
341 | +raw `TcpStream` (or `TlsStream<TcpStream>`). | |
342 | + | |
343 | +Key properties: | |
344 | +- **Reliability:** `tokio_tungstenite::accept_async` handles the full HTTP | |
345 | + upgrade handshake correctly every time. | |
346 | +- **Simplicity:** No hyper upgrade state management, no ordering issues. | |
347 | +- **Generics:** `handle_ws_raw<S>` works over any `AsyncRead + AsyncWrite`, | |
348 | + supporting both plain TCP and TLS connections. | |
349 | +- **Trade-off:** WebSocket connections bypass hyper entirely — no middleware, | |
350 | + no arena allocation, no connection pool integration. Acceptable because | |
351 | + WebSocket connections are long-lived and the hot path (message framing) | |
352 | + is handled by tokio-tungstenite. | |
353 | + | |
354 | +**Decision:** Select Option B — TCP-peek approach. | |
355 | + | |
356 | +**Evidence:** | |
357 | +- Option A consistently produced `ResetWithoutClosingHandshake` in integration | |
358 | + tests despite correct header ordering and response construction. | |
359 | +- Option B integration test (send "hello" → receive "hello") passed on first | |
360 | + attempt. | |
361 | +- Production WebSocket servers (tungstenite examples, warp) commonly use | |
362 | + this approach. | |
363 | +- `peek()` is non-blocking and reads only 4KB — negligible overhead per | |
364 | + connection. | |
365 | + | |
366 | +--- | |
367 | + | |
368 | +## ADR-010 — 2026-07-03 — Response body type: UnsyncBoxBody | |
369 | + | |
370 | +**Context:** Phase 8 introduced SSE streaming responses. The existing | |
371 | +`ResponseBody` type was `http_body_util::combinators::BoxBody<Bytes, anyhow::Error>`, | |
372 | +which requires the inner body to be `Send + Sync + 'static`. Streaming bodies | |
373 | +(like `StreamBody<ReceiverStream<...>>`) are not `Sync` because the underlying | |
374 | +`tokio::sync::mpsc::Receiver` is `Send` but not `Sync`. | |
375 | + | |
376 | +**Options considered:** | |
377 | + | |
378 | +### Option A — BoxBody (Sync, rejected) | |
379 | + | |
380 | +Requires all response bodies to be `Sync`. Streaming bodies fail to compile. | |
381 | + | |
382 | +### Option B — UnsyncBoxBody (selected) | |
383 | + | |
384 | +`http_body_util::combinators::UnsyncBoxBody<Bytes, anyhow::Error>` only | |
385 | +requires `Send + 'static` — drops the `Sync` bound. All existing static | |
386 | +responses (`Full<Bytes>`) are still `Send + Sync`, so they satisfy the | |
387 | +weaker bound. Streaming responses (`StreamBody`) are `Send` but not `Sync`, | |
388 | +which now compiles. | |
389 | + | |
390 | +**Decision:** Use `UnsyncBoxBody` as the `ResponseBody` type alias. | |
391 | + | |
392 | +**Evidence:** | |
393 | +- `BoxBody::new()` requires `B: Body + Send + Sync + 'static` | |
394 | +- `UnsyncBoxBody::new()` requires `B: Body + Send + 'static` | |
395 | +- `tokio::sync::mpsc::Receiver` is `Send` but not `Sync` | |
396 | +- SSE streaming via `StreamBody<ReceiverStream<...>>` compiles with | |
397 | + `UnsyncBoxBody` but not `BoxBody` | |
398 | + | |
399 | +--- | |
400 | + | |
401 | +--- | |
402 | +## ADR-011 — 2026-07-05 — Static file serving as 404 fallback | |
403 | + | |
404 | +**Context:** Phase 9 static file serving. The middleware chain handles all requests | |
405 | +first. If it returns a 404, we try to serve a static file before returning 404 | |
406 | +to the client. If the middleware chain errors, we also try static files. | |
407 | + | |
408 | +**Options considered:** | |
409 | + | |
410 | +### Option A — Static files checked before middleware chain | |
411 | + | |
412 | +Check `StaticDir::resolve()` before running middleware. If a file matches, serve | |
413 | +it directly without going through middleware. | |
414 | + | |
415 | +Pros: Slightly faster for static files (no middleware overhead). | |
416 | +Cons: Static files bypass auth/CORS/rate-limit middleware — may expose protected | |
417 | +files. Each path gets resolved twice (once by static check, once by chain on miss). | |
418 | + | |
419 | +### Option B — Static files as 404 fallback after middleware (selected) | |
420 | + | |
421 | +Run the full middleware chain first. If it returns 404 (or errors), then try to | |
422 | +resolve and serve a static file. | |
423 | + | |
424 | +Pros: | |
425 | +- Middleware applies to all requests consistently (auth protects static files too). | |
426 | +- Single path resolution in the common case (dynamic route hit or cache hit). | |
427 | +- Lets middleware override static file routes (e.g., `/index.html` blocked by auth). | |
428 | + | |
429 | +Cons: Static file requests always incur middleware overhead. Acceptable because | |
430 | +the middleware chain is lightweight (~0.18% overhead per Phase 5 benchmark). | |
431 | + | |
432 | +**Decision:** Select Option B — static file serving as 404 fallback after middleware. | |
433 | + | |
434 | +**Evidence:** Phase 5 benchmark showed middleware overhead of 0.18% (negligible). | |
435 | +Security principle: middleware should apply uniformly to all requests. | |
436 | + | |
437 | +--- | |
438 | + | |
439 | +## ADR-012 — 2026-07-05 — Metrics via atomic counters (no Prometheus client) | |
440 | + | |
441 | +**Context:** Phase 9 metrics. We need to expose basic metrics at `/metrics` in | |
442 | +Prometheus text format. | |
443 | + | |
444 | +**Options considered:** | |
445 | + | |
446 | +### Option A — Prometheus Rust client library (`prometheus` or `prometheus-client`) | |
447 | + | |
448 | +Full Prometheus client with histogram support, registry, automatic | |
449 | +`/metrics` endpoint. Adds ~20 dependencies. | |
450 | + | |
451 | +Pros: Rich metric types (histograms, summaries), automatic handling of | |
452 | +concurrent access, well-known API. | |
453 | +Cons: Heavy dependency tree, overkill for 5 simple counters/gauge. | |
454 | +Histogram support adds memory overhead per-bucket. | |
455 | + | |
456 | +### Option B — Atomic counters with manual formatting (selected) | |
457 | + | |
458 | +Use `AtomicU64` for each metric, format Prometheus text on demand at | |
459 | +`/metrics` endpoint. No external client library. | |
460 | + | |
461 | +Pros: Zero additional dependencies, minimal overhead (single atomic increment | |
462 | +per metric update), full control over output format. | |
463 | +Cons: No built-in histogram support, need to write Prometheus text format | |
464 | +manually. | |
465 | + | |
466 | +**Decision:** Select Option B — atomic counters with manual Prometheus formatting. | |
467 | + | |
468 | +**Evidence:** | |
469 | +- Only 5 metrics needed: 4 counters (requests, errors, bytes_in, bytes_out) | |
470 | + + 1 gauge (active_connections). | |
471 | +- Histograms can be added later if profiling shows they're needed | |
472 | + (typically via external metrics pipeline, not in-process). | |
473 | +- Prometheus text format for counters/gauges is trivial to produce: | |
474 | + `# HELP`, `# TYPE`, `metric_name value` lines. | |
475 | + | |
476 | +--- | |
477 | + | |
478 | +--- | |
479 | +## ADR-013 — 2026-07-05 — ASGI shim: dedicated thread + channel dispatch | |
480 | + | |
481 | +**Context:** Phase 10 Tier A ASGI compatibility shim. We need to run existing | |
482 | +Python ASGI applications (FastAPI, Starlette) on justapi's Rust runtime. | |
483 | + | |
484 | +**Options considered:** | |
485 | + | |
486 | +### Option A — Inline GIL acquisition on tokio workers | |
487 | + | |
488 | +Acquire the Python GIL inside each request handler (tokio worker thread). | |
489 | +Use `pyo3_asyncio::tokio::into_awaitable` to bridge Python asyncio coroutines | |
490 | +to Rust futures. | |
491 | + | |
492 | +Pros: No extra thread, simpler architecture. | |
493 | +Cons: Blocks tokio worker during Python execution (violates ADR-008 constraint). | |
494 | +`pyo3-asyncio` adds a complex dependency. Risk of GIL deadlocks. | |
495 | + | |
496 | +### Option B — Dedicated Python worker thread with channel dispatch (selected) | |
497 | + | |
498 | +A single OS thread holds the GIL permanently and runs a persistent asyncio | |
499 | +event loop. Rust sends raw HTTP request data (method, path, headers, body) | |
500 | +via `std::sync::mpsc` and receives the response via `tokio::sync::oneshot`. | |
501 | +The Python thread builds the ASGI scope dict, creates `receive`/`send` async | |
502 | +functions, and calls `await app(scope, receive, send)`. | |
503 | + | |
504 | +Pros: | |
505 | +- **ADR-008 compliant:** zero GIL contention, tokio workers never blocked by Python | |
506 | +- **No pyo3-asyncio dependency:** asyncio event loop managed entirely in Python | |
507 | +- **Simple data contract:** plain Rust structs across the channel boundary | |
508 | +- **Backpressure:** bounded channel capacity naturally limits in-flight Python work | |
509 | + | |
510 | +Cons: | |
511 | +- Extra context switch per request (channel send + recv + oneshot round-trip) | |
512 | +- No concurrent Python execution (serialized on the single worker thread) | |
513 | +- Response body fully buffered in memory (no streaming for large responses) | |
514 | + | |
515 | +### Decision: Select Option B — dedicated Python worker thread. | |
516 | + | |
517 | +**Evidence:** | |
518 | +- ADR-008 established this as the correct GIL strategy for justapi | |
519 | +- `pyo3-asyncio` is not needed: the Python side runs `loop.run_until_complete()` | |
520 | + on each request, using a persistent event loop created once at startup | |
521 | +- The `call_asgi()` Python helper is embedded as a `const &str` and compiled | |
522 | + via `PyModule::from_code` — no separate Python file to manage | |
523 | + | |
524 | +**Known limitations (tracked for resolution):** | |
525 | +- Streaming request/response bodies not yet supported (body is fully buffered) | |
526 | +- ASGI lifespan protocol not yet implemented (no startup/shutdown events) | |
527 | +- Single Python worker thread means serialized Python execution | |
528 | + | |
529 | +--- | |
530 | + | |
531 | +## Open questions / risk register | |
532 | + | |
533 | +Track these and resolve as phases progress: | |
534 | + | |
535 | +- [ ] `tokio-uring` / `monoio` evaluation — scheduled for Phase 0 spike if | |
536 | + time permits, otherwise deferred. epoll via `tokio` is the default and | |
537 | + mandatory fallback regardless. | |
538 | +- [ ] Free-threaded CPython (PEP 703) ecosystem maturity for PyO3 0.29 — | |
539 | + confirm before committing Phase 4 design. | |
540 | +- [ ] Sub-interpreter isolation guarantees — research needed before Phase 4. | |
541 | +- [ ] `quinn` / HTTP-3 crate maturity — evaluate at Phase 10 time. | |
542 | +- [ ] ASGI shim edge cases (lifespan protocol, streaming responses, WebSocket | |
543 | + close codes) — track known gaps explicitly in Phase 10. | |
544 | + | |
545 | +--- | |
546 | + | |
547 | +## ADR-014 — 2026-07-05 — Project rename: Hyperion → JustAPI | |
548 | + | |
549 | +**Context:** Phase 10 completion. The project needs a name that reflects its | |
550 | +ambition (comparable to FastAPI) and positions it for production-grade adoption. | |
551 | +"Hyperion" was the working name from the original master prompt. | |
552 | + | |
553 | +**Decision:** Rename to **JustAPI** — simple, memorable, domain-available. | |
554 | +All crates renamed: `hyperion-core` → `justapi-core`, `hyperion-py` → `justapi-py`, | |
555 | +`hyperion-cli` → `justapi-cli`, `hyperion-bench` → `justapi-bench`. | |
556 | +Python package renamed: `hyperion` → `justapi`. Class renamed: `HyperionApp` → `JustAPIApp`. | |
557 | +Metrics prefix renamed: `hyperion_*` → `justapi_*`. | |
558 | + | |
559 | +**Evidence:** Name check: domain available, PyPI name available, no trademark conflicts. | |
560 | + | |
561 | +--- | |
562 | + | |
563 | +## ADR-015 — 2026-07-05 — Validation strategy: Rust-native + Pydantic bridge | |
564 | + | |
565 | +**Context:** Phase 11 — Request Validation & Serialization. Need to decide | |
566 | +how to handle type-safe validation. | |
567 | + | |
568 | +**Decision:** Two-tier approach: | |
569 | +1. **Rust-native (Tier B):** `Schema` derive macro + `serde` + `validator` crate. | |
570 | + Validates at Rust speed — target 10x faster than Pydantic. | |
571 | +2. **Pydantic bridge (Tier A):** Python `BaseModel` → Rust struct via PyO3 | |
572 | + `FromPyObject`. Runs alongside the Rust-native path for compatibility. | |
573 | + | |
574 | +Error format follows RFC 9457 (Problem Details for HTTP APIs). | |
575 | + | |
576 | +**Evidence:** Pydantic v2 is ~5-10x faster than v1 but still has Python overhead. | |
577 | +Rust-native validation eliminates Python serialization entirely for the hot path. | |
578 | + | |
579 | +--- | |
580 | + | |
581 | +## ADR-016 — 2026-07-05 — Database strategy: sqlx with compile-time checks | |
582 | + | |
583 | +**Context:** Phase 12 — Database ORM Integration. Need async SQL driver. | |
584 | + | |
585 | +**Decision:** Use `sqlx` as the async SQL driver: | |
586 | +- Compile-time query checking (`sqlx::query!("SELECT ...")`) | |
587 | +- Connection pooling built-in | |
588 | +- Multi-DB (PostgreSQL, MySQL, SQLite) | |
589 | +- Migration system built-in | |
590 | + | |
591 | +Query builder inspired by Diesel/SeaORM for Tier B; SQLAlchemy bridge for Tier A. | |
592 | + | |
593 | +**Evidence:** `sqlx` is the most popular async SQL driver in Rust, with compile-time | |
594 | +query verification that eliminates an entire class of runtime errors. | |
595 | + | |
596 | +--- | |
597 | + | |
598 | +## ADR-017 — 2026-07-05 — Deployment strategy: Helm + distroless + CI/CD | |
599 | + | |
600 | +**Context:** Phase 16 — Multi-Cloud / Kubernetes Support. | |
601 | + | |
602 | +**Decision:** | |
603 | +- Distroless base image (< 50MB, no shell) | |
604 | +- Helm chart as primary K8s deployment mechanism | |
605 | +- Health probes via existing `/health`/`/ready`/`/live` endpoints | |
606 | +- GitHub Actions CI → container registry → K8s deploy | |
607 | +- Cloud-specific docs for GKE, EKS, AKS, Fly.io, Railway | |
608 | + | |
609 | +**Evidence:** Distroless images reduce attack surface. Helm is the standard | |
610 | +K8s packaging format. Multi-cloud strategy avoids vendor lock-in. | |
611 | + | |
612 | +--- | |
613 | + | |
614 | +## ADR-018 — 2026-07-05 — Security strategy: SAST in CI, fuzzing, OWASP baseline | |
615 | + | |
616 | +**Context:** Phase 17 — Zero-Day / SAST / Security Hardening. | |
617 | + | |
618 | +**Decision:** | |
619 | +- `cargo-audit` + `cargo-deny` + `trivy` in CI as gating checks | |
620 | +- `cargo-fuzz` / `libfuzzer` for Rust fuzz targets, `Atheris` for Python | |
621 | +- OWASP Top 10 as security baseline | |
622 | +- All security fixes documented with CVE reference where applicable | |
623 | +- Continuous fuzzing for all parsing paths | |
624 | + | |
625 | +**Evidence:** OWASP Top 10 is the industry standard web security baseline. | |
626 | +`cargo-audit` catches known vulnerabilities in dependency tree. | |
627 | + | |
628 | +--- | |
629 | + | |
630 | +## ADR-019 — 2026-07-05 — Plugin strategy: trait-based, compile-time registration | |
631 | + | |
632 | +**Context:** Phase 19 — Plugin System & Extensibility. | |
633 | + | |
634 | +**Decision:** | |
635 | +- `Plugin` trait with lifecycle hooks (`pre_route`, `post_route`, `on_startup`, `on_shutdown`) | |
636 | +- Compile-time plugin registration for Rust plugins | |
637 | +- `JustAPIApp.use(plugin)` for Python plugins | |
638 | +- Plugins run in the Rust middleware chain (no Python overhead for Rust plugins) | |
639 | +- Plugin isolation via panic boundaries (catch_unwind) | |
640 | + | |
641 | +**Evidence:** Trait-based design is idiomatic Rust. Middleware chain integration | |
642 | +means plugins get full performance. Python plugin API enables third-party ecosystem. | |
643 | + | |
644 | +--- | |
645 | + | |
646 | +## ADR-020 — 2026-07-05 — Performance target: beat FastAPI on every benchmark | |
647 | + | |
648 | +**Context:** Phase 20 — Benchmark & Optimization. | |
649 | + | |
650 | +**Decision:** FastAPI is the competition to beat. All Phases 11-19 must include | |
651 | +benchmark gates that compare against FastAPI. If any benchmark shows FastAPI | |
652 | +faster, it's a blocking bug. | |
653 | + | |
654 | +Target metrics: | |
655 | +- Throughput: 2x on hello-world, 3x on JSON echo | |
656 | +- Latency p99: < 0.5ms (FastAPI ~2ms) | |
657 | +- Memory: 2x requests per MB | |
658 | +- Startup: < 10ms cold (FastAPI ~300ms) | |
659 | +- Image size: < 50MB (FastAPI ~200MB) | |
660 | +- Test speed: 5x faster than TestClient | |
661 | + | |
662 | +**Evidence:** Phase 1 already showed 2.4x vs Granian on hello-world. Rust gives | |
663 | +a fundamental performance advantage; the gap should widen as optimization progresses. | |
664 | + | |
665 | +--- | |
666 | + | |
667 | +## ADR-021 — 2026-07-05 — GIL-free architecture: remove dedicated Python worker thread | |
668 | + | |
669 | +**Context:** Phase 13 testing utilities required `JustAPITestClient` to run synchronously without a separate server process. The dedicated Python worker thread (ADR-008) made this impossible — it required an OS thread running a tokio runtime + Python event loop. | |
670 | + | |
671 | +**Problem:** The dedicated worker thread architecture from ADR-008 prevented: | |
672 | +1. Synchronous test client execution (needed to start/finish per-request without thread lifecycle) | |
673 | +2. Pool initialization inside `py.detach()` closures (worker thread owned the GIL) | |
674 | +3. Request-response cycle that starts and completes within a single function call | |
675 | + | |
676 | +**Options considered:** | |
677 | + | |
678 | +### Option A — Keep worker thread, add test-mode bypass (rejected) | |
679 | + | |
680 | +Add a flag to skip the worker thread when running tests. Would create two code paths (production vs test) with risk of divergence. | |
681 | + | |
682 | +### Option B — Remove worker thread, GIL-free `Python::try_attach` everywhere (selected) | |
683 | + | |
684 | +Remove the dedicated Python worker thread entirely. Use `Python::try_attach()` from any tokio worker thread. This function safely acquires the GIL via `PyGILState_Ensure` regardless of which OS thread calls it. | |
685 | + | |
686 | +Key properties: | |
687 | +- **No extra OS thread:** Saves ~1MB per worker, removes thread lifecycle management | |
688 | +- **`OnceLock<HelperFunctions>`:** Helper module (Python code that builds request dicts and calls user handlers) compiled once, cached globally with `Py<PyAny>` (which is `Send + Sync + 'static`) | |
689 | +- **Pool initialization:** Works inside `py.detach()` closure before server starts — critical for `JustAPITestClient` where pool must be ready before any request | |
690 | +- **`call_python_handler`:** Shared function used by both `run()` and `JustAPITestClient` — single code path for production and testing | |
691 | + | |
692 | +**Decision:** Select Option B — remove dedicated Python worker thread. `Python::try_attach` for GIL acquisition, `py.detach()` for releasing GIL during blocking `rt.block_on`. | |
693 | + | |
694 | +**Evidence:** | |
695 | +- `Python::try_attach()` uses `PyGILState_Ensure` internally — works from any OS thread, no prior GIL state required | |
696 | +- `OnceLock<Py<PyAny>>` is safe because `Py<PyAny>` implements `Send + Sync + 'static` via PyO3's Send/Sync bounds | |
697 | +- Works with both GIL-ful and free-threaded Python 3.14 | |
698 | +- All 43 Python integration tests pass with the new architecture | |
699 | + | |
700 | +--- | |
701 | + | |
702 | +## ADR-022 — 2026-07-05 — TestClient: tokio duplex + hyper HTTP/1.1 parser (no TCP) | |
703 | + | |
704 | +**Context:** Phase 13 testing utilities. Need a synchronous test client in Rust that runs requests through the full pipeline without opening a TCP socket. | |
705 | + | |
706 | +**Options considered:** | |
707 | + | |
708 | +### Option A — TCP loopback (rejected) | |
709 | + | |
710 | +Open a real TCP connection to `127.0.0.1:0` for each test. Adds ~100μs per connection for TCP handshake + teardown. | |
711 | + | |
712 | +### Option B — tokio duplex + hyper HTTP/1.1 parser (selected) | |
713 | + | |
714 | +Use `tokio::io::duplex()` to create a pair of (reader, writer) in-memory byte streams. Write raw HTTP request bytes to the writer, pipe the reader through `hyper::http1::Builder::serve_connection` with the handler as the service function. | |
715 | + | |
716 | +Key properties: | |
717 | +- **No TCP:** All in-memory, sub-millisecond per request | |
718 | +- **Full pipeline:** Hyper HTTP/1.1 parser still runs — headers, body framing, connection lifecycle are real | |
719 | +- **`Content-Length: 0` required:** Hyper's HTTP/1.1 parser fails with "connection closed before message completed" if response doesn't include `Content-Length` for non-chunked responses | |
720 | + | |
721 | +**Decision:** Select Option B — tokio duplex + hyper HTTP/1.1 parser. | |
722 | + | |
723 | +**Evidence:** | |
724 | +- Sub-millisecond test latency achieved (vs ~1-2ms for TCP loopback) | |
725 | +- Hyper parses both request and response, ensuring real HTTP behavior | |
726 | +- `Content-Length: 0` is always included in the response builder | |
727 | + | |
728 | +--- | |
729 | + | |
730 | +## ADR-023 — 2026-07-05 — Snapshot testing: custom implementation over `insta` | |
731 | + | |
732 | +**Context:** Phase 13 testing utilities. Need snapshot testing similar to Jest/insta but integrated with Python `JustAPITestClient`. | |
733 | + | |
734 | +**Options considered:** | |
735 | + | |
736 | +### Option A — `insta` crate (rejected) | |
737 | + | |
738 | +Rust snapshot testing crate. Would require calling Rust from Python to compare snapshots, adding complexity to the FFI boundary. No native Python integration. | |
739 | + | |
740 | +### Option B — Custom Python `Snapshot` class (selected) | |
741 | + | |
742 | +`Snapshot` class with `assert_match()`, `assert_response()`, `assert_body()` methods. `.snap` files stored in `__snapshots__/` directory next to the test file. Caller detection via `inspect.stack()` frame walk. `SNAPSHOT_UPDATE=1` env var for auto-accepting new snapshots. Unified diff output on mismatch. | |
743 | + | |
744 | +**Decision:** Select Option B — custom Python `Snapshot` class. | |
745 | + | |
746 | +**Evidence:** | |
747 | +- Tighter Python integration (no FFI calls for snapshot comparison) | |
748 | +- Unified diff output is human-readable | |
749 | +- `SNAPSHOT_UPDATE=1` pattern matches Jest/insta UX | |
750 | +- 10 Python tests for snapshot functionality | |
751 | + | |
752 | +--- | |
753 | + | |
754 | +## ADR-024 — 2026-07-05 — Bucket-based latency histograms over HDR Histogram | |
755 | + | |
756 | +**Context:** Phase 14 enhanced metrics. Need latency histograms with percentile computation (p50/p95/p99/p999) for Prometheus output. | |
757 | + | |
758 | +**Options considered:** | |
759 | + | |
760 | +### Option A — `hdrhistogram` crate (rejected) | |
761 | + | |
762 | +Full HDR Histogram implementation. Adds a new dependency with complex internal data structures. Overkill for 13 fixed buckets. | |
763 | + | |
764 | +### Option B — Bucket-based histograms with `AtomicU64` (selected) | |
765 | + | |
766 | +13 Prometheus-compatible buckets (1ms, 2ms, 5ms, 10ms, 25ms, 50ms, 100ms, 250ms, 500ms, 1000ms, 2500ms, 5000ms, 10000ms). Each bucket is an `AtomicU64` counter. Percentiles computed from bucket distribution during `snapshot()`. Zero-allocation on the record path. | |
767 | + | |
768 | +**Decision:** Select Option B — bucket-based histograms. | |
769 | + | |
770 | +**Evidence:** | |
771 | +- Zero allocation on record path (single `AtomicU64::fetch_add(1, Relaxed)`) | |
772 | +- Percentiles computed from cumulative bucket counts at query time | |
773 | +- Prometheus-rendered as `justapi_request_duration_ms_bucket{le="..."}` counters | |
774 | +- 8 unit tests covering percentiles, bucket accumulation, and Prometheus output | |
775 | + | |
776 | +--- | |
777 | + | |
778 | +## ADR-025 — 2026-07-05 — Structured logging: tracing-subscriber json + tracing-appender | |
779 | + | |
780 | +**Context:** Phase 14 structured logging. Need JSON log output with configurable levels and file rotation. No custom JSON serializer wanted. | |
781 | + | |
782 | +**Options considered:** | |
783 | + | |
784 | +### Option A — Custom JSON logger (rejected) | |
785 | + | |
786 | +Write JSON formatting manually. Higher maintenance burden, risk of formatting bugs. | |
787 | + | |
788 | +### Option B — `slog` crate (rejected) | |
789 | + | |
790 | +Alternative structured logging ecosystem. Less ecosystem support than tracing, not compatible with `tracing` crate's existing usage. | |
791 | + | |
792 | +### Option C — `tracing-subscriber` json feature + `tracing-appender` (selected) | |
793 | + | |
794 | +`tracing-subscriber`'s built-in JSON layer handles structured JSON output. `tracing-appender` provides non-blocking file writers with daily/hourly rotation. `NonBlocking` writer backed by `WorkerGuard` stored in `OnceLock<Mutex<Option<WorkerGuard>>>` for safe global access. | |
795 | + | |
796 | +**Decision:** Select Option C — tracing-subscriber json + tracing-appender. | |
797 | + | |
798 | +**Evidence:** | |
799 | +- No new logging infrastructure to learn — leverages existing tracing ecosystem | |
800 | +- `tracing-subscriber` `json` feature is mature and well-documented | |
801 | +- `tracing-appender` `NonBlocking` writer prevents file I/O from blocking request handling | |
802 | +- Environment filter via `RUST_LOG` env var works transparently | |
803 | + | |
804 | +--- | |
805 | + | |
806 | +## ADR-026 — 2026-07-05 — Health check system: composable registry over static endpoints | |
807 | + | |
808 | +**Context:** Phase 14 health checks. Need pluggable health probes that can validate DB, cache, and upstream services. | |
809 | + | |
810 | +**Options considered:** | |
811 | + | |
812 | +### Option A — Static `/health`/`/ready`/`/live` endpoints (rejected) | |
813 | + | |
814 | +Hard-coded health endpoints. Not extensible — adding a new health check requires modifying server.rs. | |
815 | + | |
816 | +### Option B — `HealthRegistry` with pluggable `HealthCheck` trait (selected) | |
817 | + | |
818 | +`HealthRegistry` struct with `register(Box<dyn HealthCheck>)` and `register_fn(name, F)`. `check_all()` runs all registered checks and aggregates results. `health_response()` generates HTTP response (200 for all healthy, 503 for any unhealthy). `DbHealthCheck` under `db` feature flag. | |
819 | + | |
820 | +**Decision:** Select Option B — composable HealthRegistry. | |
821 | + | |
822 | +**Evidence:** | |
823 | +- Backward-compatible with existing static `/health` response | |
824 | +- `DbHealthCheck` reuses existing `AnyPool::health_check_all()` from Phase 12 | |
825 | +- 6 unit tests covering healthy, degraded, unhealthy, and mixed states | |
826 | + | |
827 | +--- | |
828 | + | |
829 | +## ADR-027 — 2026-07-05 — Alerting: HTTP-only webhook dispatcher over `reqwest` | |
830 | + | |
831 | +**Context:** Phase 14 alerting. Need to send webhook notifications to Slack, PagerDuty, and generic endpoints. | |
832 | + | |
833 | +**Options considered:** | |
834 | + | |
835 | +### Option A — `reqwest` crate (rejected) | |
836 | + | |
837 | +Full async HTTP client. Adds ~30 dependencies to the dependency tree. Significant compile time increase. | |
838 | + | |
839 | +### Option B — Manual HTTP POST via `tokio::net::TcpStream` (selected) | |
840 | + | |
841 | +Construct raw HTTP/1.1 POST requests manually over `tokio::net::TcpStream`. Build request bytes in a `Vec<u8>` (method, path, headers, body), write to stream, read response. | |
842 | + | |
843 | +**Decision:** Select Option B — HTTP-only POST via raw TcpStream. | |
844 | + | |
845 | +**Evidence:** | |
846 | +- Avoids adding `reqwest` as a dependency (justapi-core stays lean) | |
847 | +- Only HTTP POST needed (no GET, PUT, DELETE, streaming, cookie handling) | |
848 | +- Slack and PagerDuty webhooks accept POST only | |
849 | +- HTTPS requires `rustls` client — not implemented; users can proxy through a local HTTP gateway | |
850 | +- 3 unit tests for payload builders (Slack, PagerDuty, severity filtering) | |
851 | + | |
852 | +--- | |
853 | + | |
854 | +## ADR-028 — 2026-07-05 — Audit logging: HandlerFn wrapper over Middleware trait | |
855 | + | |
856 | +**Context:** Phase 14 audit logging. Need to log requests to sensitive endpoints with method, path, status, and duration. | |
857 | + | |
858 | +**Options considered:** | |
859 | + | |
860 | +### Option A — Middleware trait implementation (rejected) | |
861 | + | |
862 | +Implement the `Middleware` trait for `AuditLogging`. Would run inside the middleware chain, capturing pre-request state. | |
863 | + | |
864 | +### Option B — `HandlerFn` wrapper via `wrap_handler()` (selected) | |
865 | + | |
866 | +`AuditLogging::wrap_handler()` takes a `HandlerFn` and returns a new `HandlerFn` that wraps the original. This is designed to be the last wrapper before the middleware chain, ensuring it captures the final status after all middleware processes the request. | |
867 | + | |
868 | +**Decision:** Select Option B — HandlerFn wrapper. | |
869 | + | |
870 | +**Evidence:** | |
871 | +- `HandlerFn` wrapper is simpler than implementing the `Middleware` trait | |
872 | +- Placed at the end of the wrapper chain, captures final response status | |
873 | +- Structured `tracing::info!` with `audit=true` field enables log filtering | |
874 | +- 2 unit tests covering default and custom audit rules | |
875 | + | |
876 | +--- | |
877 | + | |
878 | +## ADR-029 — 2026-07-05 — Panic recovery: `catch_unwind` over tokio task abort | |
879 | + | |
880 | +**Context:** Phase 14 panic recovery. Need to prevent panics in user handlers from crashing the server. | |
881 | + | |
882 | +**Options considered:** | |
883 | + | |
884 | +### Option A — Tokio task abort (rejected) | |
885 | + | |
886 | +Set up a task lifecycle hook that detects panics and aborts the task. Limited control over the panic response — tokio's default panic handling just terminates the task. | |
887 | + | |
888 | +### Option B — `std::panic::catch_unwind` + `FutureExt::catch_unwind` (selected) | |
889 | + | |
890 | +Wrap the `BoxFuture` handler with `FutureExt::catch_unwind`. If the future panics, `catch_unwind` catches the panic and returns `Err(PanicInfo)`. This is converted to a 500 JSON response: `{"error":"Internal server error"}`. | |
891 | + | |
892 | +Additionally, set a custom panic hook via `std::panic::set_hook` that logs the panic message and location via `tracing::error!(panic=true, ...)`. The default hook behavior is preserved. | |
893 | + | |
894 | +**Decision:** Select Option B — catch_unwind on the BoxFuture. | |
895 | + | |
896 | +**Evidence:** | |
897 | +- `FutureExt::catch_unwind` wraps the future at the point of execution — no tokio task lifecycle changes | |
898 | +- Custom panic hook logs stack trace information | |
899 | +- 500 JSON response ensures the client doesn't hang on a panic | |
900 | +- `anyhow::Error` from handler is also converted to 500 | |
901 | +- 1 unit test verifying panic recovery returns a handler function | |
902 | + | |
903 | +- [ ] `tokio-uring` / `monoio` evaluation — deferred. epoll via `tokio` is the default. | |
904 | +- [ ] Free-threaded CPython (PEP 703) maturity for PyO3 — monitor. | |
905 | +- [ ] Sub-interpreter isolation guarantees — deferred for v1.0. | |
906 | +- [ ] `quinn` / HTTP-3 crate maturity — evaluate at Phase 20. | |
907 | +- [ ] Pydantic bridge performance — if Pydantic bridge is too slow, consider dropping Tier A ASGI shim for Phase 11+ features. | |
908 | +- [ ] sqlx compile-time checks — may increase compile times. Evaluate trade-off vs runtime query building. | |
909 | + | |
910 | +--- | |
911 | + | |
912 | +## ADR-030 — 2026-07-10 — Native inference engine: Candle (Rust), not torch delegation | |
913 | + | |
914 | +**Context:** JustAPI's 2026 differentiation thesis is "best for model deploying" | |
915 | +with *native GPU* support. FastAPI delegates all GPU work to `torch` under the | |
916 | +GIL, which is the bottleneck it can never remove. To beat FastAPI for inference | |
917 | +we must own the GPU execution path in Rust. | |
918 | + | |
919 | +**Options considered:** | |
920 | +1. Keep delegating to `torch`/`transformers` via PyO3 (current state) — rejected: | |
921 | + keeps the GIL bottleneck, adds no native advantage, just repackages FastAPI. | |
922 | +2. **Candle** (HuggingFace Rust ML framework: CUDA/ROCm/Metal/CPU backends, | |
923 | + safetensors/GGUF loaders, KV-cache + FlashAttention kernels) — **selected**. | |
924 | +3. `burn` (Rust DL framework, backend-agnostic) — strong, but smaller model | |
925 | + coverage and weaker LLM/quantization ecosystem than Candle today. | |
926 | +4. `llm` / llama.cpp bindings — excellent for GGUF but narrower (no training, | |
927 | + weaker multimodal/agent tooling); good later as a quantized backend. | |
928 | + | |
929 | +**Decision:** Build a new `justapi-inference` crate on **Candle** as the native | |
930 | +GPU inference engine. Python handlers call into it through the existing PyO3 | |
931 | +zero-copy (Arrow/DLPack) boundary; heavy compute runs in Rust with no GIL. | |
932 | +`burn` and llama.cpp remain candidate backends behind a trait for Phase 45+. | |
933 | + | |
934 | +**Evidence:** | |
935 | +- 2026 landscape (vLLM/SGLang/TGI/TensorRT-LLM) confirms the engine layer is | |
936 | + what defines GPU efficiency; FastAPI sits strictly below it. | |
937 | +- Candle gives Rust-native CUDA/ROCm, KV cache, FlashAttention, and quantization | |
938 | + without leaving the Rust runtime we already control. | |
939 | +- Reuses existing `memory` crate (arena/pool) for KV-cache block allocation. | |
940 | + | |
941 | +--- | |
942 | + | |
943 | +## ADR-031 — 2026-07-10 — Two-layer architecture: engine + control plane | |
944 | + | |
945 | +**Context:** Research shows production model serving splits into (a) an | |
946 | +*inference engine* (vLLM/SGLang/TGI — owns GPU, batching, KV cache) and | |
947 | +(b) a *serving control plane* (Ray Serve / NVIDIA Dynamo / AIBrix / Ouranos — | |
948 | +owns OpenAI API, routing, autoscaling, registry). JustAPI currently has web | |
949 | +framework + partial control-plane primitives (rate-limit, circuit-breaker, | |
950 | +health, DAG) but **no engine layer**. | |
951 | + | |
952 | +**Decision:** Implement both layers inside JustAPI: | |
953 | +- **Engine (Phase 41-45):** `justapi-inference` — Candle-backed load/generate, | |
954 | + paged KV cache, continuous-batching scheduler, quantization, multi-LoRA. | |
955 | +- **Control plane (Phase 44, 46):** OpenAI-compatible API, model registry, | |
956 | + KV-aware/LoRA-aware routing, LLM-specific autoscaling, multi-replica. | |
957 | +This avoids a hard dependency on an external engine (unlike Ray Serve, which | |
958 | +*orchestrates* vLLM but does not implement one). | |
959 | + | |
960 | +**Evidence:** Dynamo/AIBrix/Ouranos all wrap vLLM-class engines; owning the | |
961 | +engine is what lets JustAPI be a single self-contained "deploy a model" binary | |
962 | +(Rust speed + GPU + Python DX) rather than glue code. | |
963 | + | |
964 | +--- | |
965 | + | |
966 | +## ADR-032 — 2026-07-10 — PyPI distribution: single abi3 wheel + manylinux_2_28 build | |
967 | + | |
968 | +**Context:** `justapi` PyPI name is available (verified 404). The package must | |
969 | +ship one wheel covering CPython 3.11-3.14. | |
970 | + | |
971 | +**Decision:** | |
972 | +- Build a single **abi3** wheel (`cp311-abi3`) via `abi3-py311 = true` in | |
973 | + `pyproject.toml` + `abi3-py311` on the `pyo3` crate feature. One build covers | |
974 | + all supported Pythons. | |
975 | +- The publish build MUST run inside a `manylinux_2_28` container (maturin | |
976 | + auto-detects the policy). The local Arch host (glibc 2.41) tags | |
977 | + `manylinux_2_34`, which PyPI rejects, so local wheels are dev-only. | |
978 | +- `twine check` must pass before upload; upload via `maturin publish` or | |
979 | + `twine upload` using a `MATURIN_PYPI_TOKEN` (not committed). | |
980 | + | |
981 | +**Evidence:** wheel built and verified (`twine check` PASSED, installs + | |
982 | +imports in a clean venv). Optional extras: `pydantic`, `jinja`, `full`. | |
983 | + | |
984 | +--- | |
985 | + | |
986 | +## ADR-033 — 2026-07-10 — Single Python package source of truth | |
987 | + | |
988 | +**Context:** Two divergent `justapi` packages existed — the real, complete one | |
989 | +at `crates/justapi-py/python/justapi/` (maturin builds/ships from here, has | |
990 | +`_justapi.so` + full `app.py`) and a stale duplicate at repo-root | |
991 | +`python/justapi/` that shadowed imports and broke test collection. | |
992 | + | |
993 | +**Decision:** `crates/justapi-py/python/justapi/` is the single canonical | |
994 | +source. The repo-root `python/justapi/` duplicate was removed; its unique files | |
995 | +(tests, `grpc_compiler.py`) were merged into the canonical dir. Maturin builds | |
996 | +from `crates/justapi-py/` via `python-source = "python"`. | |
997 | + | |
998 | +**Evidence:** after consolidation, `pytest` collects and runs 53 passed / 1 | |
999 | +skipped against the installed (editable) package; no import shadowing. | |
1000 | + | |
1001 | +--- | |
1002 | + | |
1003 | +## ADR-034 — 2026-07-10 — `multer` for multipart/form-data parsing | |
1004 | + | |
1005 | +**Context:** Phase 47 (FastAPI Parity) — multipart file upload. JustAPI needs to | |
1006 | +parse `multipart/form-data` bodies for file upload endpoints (dataset/RAG ingest, | |
1007 | +model weight uploads, etc.). The parser must be async, memory-safe, and handle | |
1008 | +arbitrary boundary strings. | |
1009 | + | |
1010 | +**Options considered:** | |
1011 | +1. **`multer` 3.1** — battle-tested async parser used by Rocket, Actix, Axum. | |
1012 | + Accepts `Stream<Item = Result<Bytes, E>>` so it works with any | |
1013 | + `http_body::Body` via `BodyDataStream`. Has built-in size constraints for | |
1014 | + DoS protection. ~88M downloads. Maintained by SergioBenitez (Rocket lead). | |
1015 | +2. **Manual parser** — hand-roll boundary-aware chunked body reader. Risk of | |
1016 | + off-by-one on edge-case boundaries, slower to develop, no streaming support. | |
1017 | +3. **`actix-multipart`** — tightly coupled to Actix's body type; would require | |
1018 | + adapter layer. | |
1019 | + | |
1020 | +**Decision:** Use `multer = "3.1"`. It integrates directly with hyper 1.x via | |
1021 | +`BodyDataStream`, is already used by the wider Rust web ecosystem, and supports | |
1022 | +streaming + size limits out of the box. Added as a dependency of `justapi-core` | |
1023 | +(no feature gate — multipart is not optional for a FastAPI-competitive framework). | |
1024 | + | |
1025 | +**Evidence:** 7 unit tests covering boundary extraction, text fields, single | |
1026 | +file, multiple files, and missing-boundary error. `cargo test --workspace` passes. | |
1027 | + | |
1028 | +--- | |
1029 | + | |
1030 | +## ADR-036 — 2026-07-10 — Disaggregated prefill/decode scheduler | |
1031 | + | |
1032 | +**Context:** Phase 48 — disaggregated serving. Production LLM servers (vLLM | |
1033 | +DistServe, NVIDIA Dynamo) split prefill and decode onto separate GPU pools | |
1034 | +because their compute profiles are fundamentally different: prefill is | |
1035 | +compute-bound (processes many prompt tokens in parallel) while decode is | |
1036 | +memory-bandwidth-bound (generates one token at a time). Pooling them together | |
1037 | +wastes GPU cycles (decode waits for prefill to finish). | |
1038 | + | |
1039 | +**Options considered:** | |
1040 | +1. **Single scheduler with priority scheduling** — keep one pool, prioritise | |
1041 | + decode over prefill to reduce TTFT jitter. Simpler but cannot fully isolate | |
1042 | + prefill-heavy workloads from decode latency. | |
1043 | +2. **Two independent schedulers with KV transfer (selected)** — `PdScheduler` | |
1044 | + owns one `Scheduler` for prefill and one for decode. Sequences transfer from | |
1045 | + prefill→decode via `Scheduler::take_completed_prefill` / | |
1046 | + `admit_transferred`. Enables independent pool sizing (e.g. prefill pool | |
1047 | + `max_num_seqs=1`, decode pool `max_num_seqs=8`). | |
1048 | + | |
1049 | +**Decision:** Option 2 — `PdScheduler` wrapping two `Scheduler` instances. | |
1050 | +The prefill scheduler's `TransferableSequence` struct was moved from inner | |
1051 | +(struct inside `impl Scheduler`) to module scope to satisfy Rust's nested-item | |
1052 | +restriction. `SchedulerStats` given `#[derive(Debug, Clone)]` so `PdStats` can | |
1053 | +derive them. The `KvBlockPool::release` flow (→ cached, not free) is the | |
1054 | +expected lifecycle — blocks are made available to eviction but stay tracked | |
1055 | +until pressure triggers recycling. | |
1056 | + | |
1057 | +**Evidence:** 5 unit tests: starts idle, single-request prefill→transfer→decode, | |
1058 | +independent pool sizing (prefill 1 / decode 8), `total_transferred_tokens` | |
1059 | +accumulation (16+8=24), block in-use verification (prefill releases, decode | |
1060 | +allocates). All 92 lib + 10 integration tests pass, clippy + fmt clean. | |
1061 | + | |
1062 | +--- | |
1063 | + | |
1064 | +## ADR-037 — 2026-07-10 — Structural LLM benchmark via synthetic GPU-cost model | |
1065 | + | |
1066 | +**Context:** Phase 48 benchmark vs vLLM/SGLang. No GPU exists in CI, so a literal | |
1067 | +wall-clock tokens/sec comparison is impossible. We still need a reproducible, | |
1068 | +checkable number proving the disaggregated P/D (ADR-036) and speculative-decoding | |
1069 | +(ADR-035) mechanisms transfer to the scheduling metrics (TTFT, ITL, throughput) | |
1070 | +that define LLM-serving quality. | |
1071 | + | |
1072 | +**Options considered:** | |
1073 | +1. **Real GPU run (deferred)** — requires `--features real` + CUDA + weights. | |
1074 | + Accurate but not runnable in this environment; left as a gate TODO. | |
1075 | +2. **Wall-clock of `Engine::generate` + MockModel** — rejected: `MockModel` | |
1076 | + produces tokens with no yield, so the unbounded channel buffers the whole | |
1077 | + stream and TTFT/ITL collapse to ~0; it measures channel throughput, not | |
1078 | + serving dynamics. | |
1079 | +3. **Drive the real schedulers over a synthetic GPU-cost model (selected)** — | |
1080 | + `justapi-bench-inference` runs `PdScheduler` (disaggregated) and `Scheduler` | |
1081 | + (collocated) through the actual prefill→transfer→decode loop, charging each | |
1082 | + step a modeled cost. Disaggregated uses *parallel* virtual clocks (independent | |
1083 | + prefill/decode pools); collocated uses one *shared* clock. Prompt lengths are | |
1084 | + varied per request so prefill/decode overlap (the condition that degrades | |
1085 | + collocated ITL). | |
1086 | + | |
1087 | +**Decision:** Option 3. The benchmark is a *topology* proof: it isolates the | |
1088 | +scheduler-level effect of splitting prefill/decode, which is hardware-independent. | |
1089 | +Cost model: prefill ∝ prompt tokens/step (compute-bound), decode = fixed step | |
1090 | +(memory-bandwidth-bound), matching the vLLM/SGLang assumption. Numbers are | |
1091 | +explicitly labeled structural, not GPU throughput. | |
1092 | + | |
1093 | +**Evidence:** 16 burst requests, prompt 64–256 tok — disaggregated ITL p99 0.20 ms | |
1094 | +vs collocated 1.16 ms (5.80x tighter), throughput 70,617 vs 34,634 tok/s (2.04x), | |
1095 | +wall time 12.32 vs 25.12 ms. Mirrors the DistServe/Dynamo disaggregated-P/D win. | |
1096 | +Recorded in BENCHMARKS.md. | |
1097 | + | |
1098 | +--- | |
1099 | + | |
1100 | +## ADR-038 — 2026-07-10 — K8s AI inference gateway plugin | |
1101 | + | |
1102 | +**Context:** Phase 48 final item — a K8s AI inference gateway: KV-aware + | |
1103 | +LoRA-aware routing *inside* the gateway (the Dynamo / AIBrix behaviour), so | |
1104 | +JustAPI can act as a model-serving ingress rather than just an engine library. | |
1105 | + | |
1106 | +**Options considered:** | |
1107 | +1. **Wire `Server::with_openai_routed` only** — already exists (Phase 46) and | |
1108 | + routes per-request, but it owns no Kubernetes concepts (readiness, endpoint | |
1109 | + DNS, namespace). It is HTTP glue, not a gateway abstraction. | |
1110 | +2. **New `InferenceGateway` composing `ControlPlane` + `Router` (selected)** — | |
1111 | + adds the K8s-specific concerns the raw router lacks: readiness gating, | |
1112 | + Kubernetes service-DNS endpoint resolution, namespace scoping, and a | |
1113 | + KV-pressure default strategy. Pure decision logic (no network/GPU), matching | |
1114 | + the rest of the control plane; a K8s executor fulfils routing decisions and | |
1115 | + reports pod readiness/load back. | |
1116 | + | |
1117 | +**Decision:** Option 2. `gateway.rs` with `InferenceGateway`, `GatewayConfig` | |
1118 | +(namespace, `service_template` supporting `{replica}`/`{model}`/`{version}`/ | |
1119 | +`{namespace}`, `strategy`, `require_ready`), and `GatewayDecision` (replica + | |
1120 | +resolved model + endpoint). Readiness is folded into the router's `healthy` | |
1121 | +flag (standard K8s mapping: not-Ready ⇒ not a routing candidate). Default | |
1122 | +strategy `LowestKvPressure` so the gateway is KV-aware out of the box. | |
1123 | + | |
1124 | +**Evidence:** 5 unit tests: Kubernetes endpoint resolution, namespace scoping, | |
1125 | +readiness gating (not-Ready pod excluded, both drained → no capacity), KV-pressure | |
1126 | +default routing to coolest replica, LoRA-aware routing inside the gateway. All | |
1127 | +97 lib + 10 integration tests pass, clippy + fmt clean. | |
1128 | + | |
1129 | +--- | |
1130 | + | |
1131 | +## ADR-039 — 2026-07-10 — Tree-based speculative decoding (Medusa/EAGLE) | |
1132 | + | |
1133 | +**Context:** Phase 49 — the speculative-decoding foundation (ADR-035) proved | |
1134 | +lossless token streaming with `gamma + 1` tokens per step when a draft perfectly | |
1135 | +predicts the target. But with an imperfect draft, the single-path draft-target | |
1136 | +scheme accepts only `~1` token per step (one bonus target token). Production | |
1137 | +systems (Medusa, EAGLE, vLLM's tree speculation) improve this by giving the | |
1138 | +draft multiple "guesses" per position — a tree of candidates. ADR-035 explicitly | |
1139 | +deferred this ("Tree-based (Medusa/EAGLE): higher acceptance, more complex | |
1140 | +verify (tree reduction). Deferred — the draft-target scheme is the foundation | |
1141 | +they refine."). | |
1142 | + | |
1143 | +**Options considered:** | |
1144 | +1. **Draft-target only (rejected for Phase 49):** keep single-path draft-target. | |
1145 | + Leaves a known capability gap vs vLLM/SGLang — ADR-037's TODO explicitly | |
1146 | + called for "Tree-based Medusa/EAGLE verify (higher acceptance than | |
1147 | + draft-target)." | |
1148 | +2. **Tree-based verify with greedy DFS path (selected):** `build_draft_tree` | |
1149 | + constructs a `DraftTree` from `branch` candidates per depth (recursive | |
1150 | + top-k extraction from the draft's `forward_logits`). `verify_tree` walks | |
1151 | + the tree greedily: at each depth, find a child whose token matches the | |
1152 | + target's argmax/sample; accept it and descend; if no match, emit the | |
1153 | + target's own token as correction (lossless). `speculative_generate_tree` | |
1154 | + wraps the loop with stop-token checking on accepted tokens (unlike | |
1155 | + draft-target, which only checks stop tokens at the bonus phase — tree | |
1156 | + speculation must check each accepted token because the draft can propose | |
1157 | + stop tokens in the tree). | |
1158 | +3. **Full tree verification with scoring (deferred):** score all paths and pick | |
1159 | + the globally optimal one. More complex, needs tree-attention-style batching | |
1160 | + to be efficient; possible future optimization. | |
1161 | + | |
1162 | +**Decision:** | |
1163 | +- New `spec_decode_tree.rs` module with `TreeNode`, `DraftTree`, | |
1164 | + `build_draft_tree`, `verify_tree`, `speculative_generate_tree`. | |
1165 | +- `AcceptanceStats.tree_branch` and `tree_nodes_verified` fields added | |
1166 | + (backward-compatible: both default to 0 for draft-target runs). | |
1167 | +- `top_k_tokens` helper extracted (pure-Rust argmax/top-k from logits). | |
1168 | +- `RangeModel` test model: configurable offset + spread for injection of | |
1169 | + draft-target alignment scenarios. | |
1170 | +- For structural (no-GPU) tests: `RangeModel(offset=0, spread=3)` as draft, | |
1171 | + `MockModel` as target — acceptance rate ≈ 1.0 with branch=3. | |
1172 | +- Stop tokens checked per-accepted-token in the tree loop (not just at bonus | |
1173 | + phase), because the draft tree may contain stop tokens at any depth. | |
1174 | + | |
1175 | +**Evidence:** 18 unit tests in `spec_decode_tree::tests` covering: | |
1176 | +- `top_k_tokens` sorted order and edge cases (empty, k=0) | |
1177 | +- `DraftTree.total_nodes` formula (branch=1 ✓, branch=0 ✓, branch=3) | |
1178 | +- Branch=1 acts identical to draft-target (single path) | |
1179 | +- Gamma=0 degenerates to plain decode (lossless) | |
1180 | +- Perfect draft (offset=0, branch=3): acceptance rate ≥ 0.95 | |
1181 | +- Graceful degradation (offset=10, never matches target): acceptance < 0.1, | |
1182 | + output identical to plain decode | |
1183 | +- Higher branch gives ≥ same acceptance as lower branch | |
1184 | +- `verify_tree` unit cases: match, no-match, multi-depth match, deep reject | |
1185 | +- Stop tokens respected (stops on first stop token) | |
1186 | +- Empty prompt works (output length == max_tokens) | |
1187 | +- All 18 pass, 115 inference tests total. `cargo test --workspace`, | |
1188 | + `clippy -D warnings`, `cargo fmt --check` clean. | |
1189 | + | |
1190 | +**What this does NOT yet provide (gated on GPU):** | |
1191 | +- Wall-clock tokens/sec on real 7B-class models with tree-attention batching | |
1192 | + (needs CUDA + weights). | |
1193 | +- The `SpeculativeModel` wrapper for tree speculation (for serving through | |
1194 | + `Engine::generate` without callers knowing); draft-target has this, tree | |
1195 | + would need `TreeSpeculativeModel` or a config flag. | |
1196 | +- Tree-attention batching (verify all tree nodes in one forward pass with a | |
1197 | + tree-attention mask, matching Medusa's efficient implementation). | |
1198 | + | |
1199 | +**Context:** Phase 48 — production hardening for AI. vLLM/SGLang win on decode | |
1200 | +throughput via speculative decoding (draft proposes `gamma` tokens, target | |
1201 | +verifies, accept longest agreeing prefix + emit 1 correction). JustAPI must | |
1202 | +offer the same to avoid a feature gap vs production LLM servers. | |
1203 | + | |
1204 | +**Options considered:** | |
1205 | +1. **Tree-based (Medusa/EAGLE):** higher acceptance, more complex verify (tree | |
1206 | + reduction). Deferred — the draft-target scheme is the foundation they refine. | |
1207 | +2. **Draft-target (Chen et al. 2023):** standard, simple, correctness-preserving. | |
1208 | + Implemented. | |
1209 | +3. **Skip / delegate to candle:** would leave a hard capability gap. | |
1210 | + | |
1211 | +**Decision:** | |
1212 | +- Add `Model::forward_logits(context) -> Vec<f32>` to the trait (single-step | |
1213 | + primitive). `MockModel` returns a degenerate-but-smooth distribution; | |
1214 | + `RealModel` returns true last-position logits from one forward pass. | |
1215 | +- New `spec_decode` module: `speculative_generate`, `sample_token` (pure-Rust | |
1216 | + temperature/top_p/top_k sampler, seeded via `rand` 0.8), `AcceptanceStats`, | |
1217 | + `SpeculativeConfig`, `SpeculativeModel` (a `Model` wrapper servable through | |
1218 | + `Engine::register_speculative`). | |
1219 | +- Added `rand = "0.8"` to `justapi-inference` (already used in `justapi-core`; | |
1220 | + minimal, standard). Seeded RNG keeps verification reproducible for tests. | |
1221 | + | |
1222 | +**Evidence:** 8 unit tests (incl. proof that draft==target yields an identical | |
1223 | +token stream to plain decode — speculation is lossless — and acceptance rate | |
1224 | +→ 1.0 / gamma+1 tokens per step for a perfect draft) + 2 Engine integration | |
1225 | +tests. `cargo test --workspace`, `clippy -D warnings`, `cargo fmt` clean. | |
1226 | + | |
1227 | +--- | |
1228 | + | |
1229 | +## ADR-040 — 2026-07-10 — Tree speculation in the serving path (Engine + CLI) | |
1230 | + | |
1231 | +**Context:** Phase 49 delivered the tree-verify mechanism (`spec_decode_tree.rs`) | |
1232 | +but it was only reachable via the free function `speculative_generate_tree`. To | |
1233 | +be servable like the draft-target scheme (ADR-035's `SpeculativeModel` + | |
1234 | +`Engine::register_speculative`), tree speculation needed an equivalent wrapper | |
1235 | +so `justapi serve --model` can enable it through the OpenAI-compatible API with | |
1236 | +zero caller awareness. | |
1237 | + | |
1238 | +**Options considered:** | |
1239 | +1. **`Engine::register_speculative` overload** — add `branch` param to the | |
1240 | + existing method and branch internally on `branch > 0`. Simpler call site but | |
1241 | + conflates two distinct verify algorithms in one entry point; harder to read | |
1242 | + and test. | |
1243 | +2. **New `TreeSpeculativeModel` + `Engine::register_tree_speculative` (selected)** | |
1244 | + — mirrors the `SpeculativeModel` / `register_speculative` shape exactly, so | |
1245 | + draft-target and tree modes are first-class peers. The CLI wires both. | |
1246 | + | |
1247 | +**Decision:** | |
1248 | +- `spec_decode_tree::TreeSpeculativeModel` — a `Model` wrapper running | |
1249 | + `speculative_generate_tree` with `target` / `draft` / `gamma` / `branch` / | |
1250 | + `seed`, delegating `vocab_size` + `forward_logits` to the target (same as | |
1251 | + `SpeculativeModel`). | |
1252 | +- `Engine::register_tree_speculative(name, target, draft, gamma, branch, seed)` | |
1253 | + — registers the wrapped model under `name` so it is served through the normal | |
1254 | + `Engine::generate` path (used by the OpenAI server) transparently. | |
1255 | +- `Engine::get(name)` — small public accessor used by the CLI to fetch the | |
1256 | + registered target before wrapping it (registry was previously private). | |
1257 | +- CLI `justapi serve`: new `--gamma` (draft-target + tree trigger) and `--branch` | |
1258 | + (tree enable) flags. When either is set, the CLI registers a second mock as the | |
1259 | + draft (`<model>-draft`) and wraps `<model>` via `register_tree_speculative` | |
1260 | + (`branch > 0`) or `register_speculative` (`gamma > 0`). The log line names the | |
1261 | + mode. | |
1262 | + | |
1263 | +**Evidence:** 2 new wrapper tests (`tree_speculative_model_wrapper_matches_target`, | |
1264 | +`tree_speculative_model_vocab_and_logits_delegated`) — the wrapped model emits the | |
1265 | +same token stream as plain target decode and delegates vocab/logits to the target. | |
1266 | +End-to-end smoke test: `justapi serve --model mymodel --gamma 4 --branch 3` | |
1267 | +starts with the "tree speculative decoding" log line and serves | |
1268 | +`/v1/models` (lists `mymodel` + `mymodel-draft`), `/v1/chat/completions` | |
1269 | +(non-streaming: 12 completion tokens) and streaming SSE — all 200. All 117 | |
1270 | +inference tests pass; `clippy -D warnings`, `cargo fmt --check` clean. | |
1271 | + | |
1272 | +**What this does NOT yet provide (gated on GPU):** | |
1273 | +- Wall-clock tokens/sec on real 7B-class models with tree-attention batching | |
1274 | + (needs CUDA + weights). The structural acceptance gain (up to 3× at branch=3) | |
1275 | + transfers directly; only the per-step batched verification cost changes. | |
1276 | +- A registered *real* draft model (today the CLI uses a second `MockModel` as | |
1277 | + draft — a perfect-draft demo; a smaller/faster real model would be the | |
1278 | + production draft). | |
1279 | + | |
1280 | + | |
1281 | +## ADR-041 — 2026-07-10 — Radix tree as the prefix-cache data structure | |
1282 | + | |
1283 | +**Context:** Phase 50 needed cross-request KV reuse. The existing | |
1284 | +`PrefixCache` is a flat hash-map keyed by per-prefix token hashes. It dedupes | |
1285 | +identical prefixes but duplicates block-ID references across distinct prefix | |
1286 | +entries, so its memory grows super-linearly for nested (chat-history) prompts | |
1287 | +and cannot merge *partial* shared prefixes. A radix tree (SGLang | |
1288 | +RadixAttention) stores each block once and merges shared prefixes. | |
1289 | + | |
1290 | +**Decision:** `RadixPrefixCache` in `radix_cache.rs` — a radix tree of | |
1291 | +block-aligned nodes. `insert` splits edges to share common prefixes; | |
1292 | +`lookup` returns the longest matching prefix (refreshing LRU timestamps); | |
1293 | +`evict` removes LRU leaf subtrees and returns their `BlockId`s as a free | |
1294 | +hint. Benchmark (`bench_nested`) shows O(N) block-slots for radix vs O(N²) | |
1295 | +block references for the flat `PrefixCache` on a chat-history workload | |
1296 | +(radix uses <1% of flat's storage at N=500). | |
1297 | + | |
1298 | +**Evidence:** 14 unit tests incl. `shared_prefix_merges_nodes`, | |
1299 | +`three_way_sharing`, `evict_removes_lru_leaf`, and the two | |
1300 | +`bench_nested`-derived scaling tests. `clippy -D warnings`, `fmt --check` clean. | |
1301 | + | |
1302 | +## ADR-042 — 2026-07-10 — Wiring RadixPrefixCache into the Scheduler | |
1303 | + | |
1304 | +**Context:** `RadixPrefixCache` (ADR-041) was a standalone module; the | |
1305 | +`Scheduler` never consulted any prefix cache — finished sequences were simply | |
1306 | +dropped, leaking their KV blocks in the pool. The production win of Phase 50 | |
1307 | +(cross-request prefix KV reuse) required the scheduler to own the cache. | |
1308 | + | |
1309 | +**Options considered:** | |
1310 | +1. **Caller-driven lookup** (keep `NewRequest.cached_blocks`/`prefix_cached_tokens`, | |
1311 | + caller fills them) — leaves the scheduler cache-less, duplicates lookup | |
1312 | + logic, and the leaked-block bug unfixed. | |
1313 | +2. **Scheduler owns the radix cache (selected)** — single eviction authority, | |
1314 | + O(1) lookup on admission, blocks cached on completion, eviction driven by | |
1315 | + the scheduler under pressure. | |
1316 | + | |
1317 | +**Decision:** | |
1318 | +- `Scheduler` now owns a `RadixPrefixCache` plus a `prefix_refs: HashMap<BlockId, | |
1319 | + usize>` live-reference count. | |
1320 | +- **Admission:** if `NewRequest.cached_blocks` is empty, the scheduler does its | |
1321 | + own `prefix.lookup`; matched blocks are `reclaim`ed (reused, no recompute) | |
1322 | + and recorded as live-referenced (`unpin`). A block is only reused when its | |
1323 | + live-ref is 0, so two concurrent requests never claim the same cached block. | |
1324 | +- **Completion (`schedule` retain + `cancel`):** finished sequences' blocks are | |
1325 | + promoted via `cache_completed` — `prefix.insert` (idempotent for shared | |
1326 | + prefixes) and then `pool.release` + `pool.pin` so the clock-sweep evictor | |
1327 | + cannot recycle them out from under the cache. This also fixes the | |
1328 | + finished-sequence block leak. | |
1329 | +- **Eviction:** when `Sequence::grow` fails, the scheduler calls | |
1330 | + `prefix.evict_filter(k, |bs| all live-refs == 0)` which returns only | |
1331 | + freeable LRU leaf blocks; they are `unpin`ed + `pool.free_cached` (a new | |
1332 | + `KvBlockPool` method that returns a cached block to the free list | |
1333 | + immediately, no clock-sweep wait). This keeps a single eviction authority and | |
1334 | + avoids stale block ids in the tree. | |
1335 | +- `pin` was relaxed to allow `ref_count == 0` (cached + pinned), and | |
1336 | + `RadixPrefixCache::evict_filter` was added (skips leaves still referenced by a | |
1337 | + live sequence). `take_completed_prefill` clears `prefix_refs` for transferred | |
1338 | + blocks (they leave this pool). | |
1339 | + | |
1340 | +**Evidence:** 3 new scheduler-integration tests: `schedule_radix_reuses_shared_prefix` | |
1341 | +(full hit, no prefill, 48 tokens saved), `schedule_radix_reuses_partial_prefix` | |
1342 | +(only the unique tail prefilled, `computed_tokens == 48`), and | |
1343 | +`schedule_radix_evicts_under_pressure` (6-block pool, 10 distinct prompts, no | |
1344 | +deadlock). All 134 inference tests pass; `clippy -D warnings`, `fmt --check` clean. | |
1345 | + | |
1346 | +## ADR-043 — 2026-07-10 — Collision-safe content-hash cache key for RadixPrefixCache | |
1347 | + | |
1348 | +**Context:** ADR-042 wired `RadixPrefixCache` into the `Scheduler`. The tree | |
1349 | +matches prefixes by exact token comparison, which is already correct, but the | |
1350 | +cache key was implicit (the token run itself). If a future change switched | |
1351 | +matching to be hash-based for speed, or if the tree were ever corrupted, | |
1352 | +a false share could return the wrong KV blocks. We wanted the cache key to be | |
1353 | +explicit and collision-resistant. | |
1354 | + | |
1355 | +**Decision:** every `RadixNode` now stores `token_hash` (FNV-1a of its tokens). | |
1356 | +- `insert` / split recompute the hash for each node (including the truncated | |
1357 | + edge after a split). | |
1358 | +- `lookup` accepts a full-edge match only if `child.token_hash == | |
1359 | + hash_tokens(&tokens[..cp])`; on divergence it sets a `collision` flag and the | |
1360 | + caller rejects the match (returns `None`, increments `hash_collisions`). | |
1361 | +- Added `RadixPrefixCache::verify_hashes()` (self-check that every node's | |
1362 | + stored hash equals its token content) and a `hash_collisions` stat. | |
1363 | + | |
1364 | +**Evidence:** 4 new tests — `node_hash_matches_token_content`, | |
1365 | +`hashes_consistent_after_split`, `distinct_prefixes_do_not_collide` | |
1366 | +(`hash_collisions == 0`), and `lookup_rejects_corrupted_hash` (forged | |
1367 | +corruption → `lookup` returns `None`, `hash_collisions == 1`). All 138 | |
1368 | +inference tests pass; `clippy -D warnings`, `fmt --check` clean. | |
1369 | + | |
1370 | +**Note / caveat:** KV-cache reuse is still keyed on *exact* token ids — two | |
1371 | +prompts that are semantically equal but token-distinct correctly do NOT share | |
1372 | +blocks, because their KV differs. The hash hardens the *key integrity* | |
1373 | +(collision/corruption safety), not semantic equivalence. | |
1374 | + | |
1375 | +## ADR-044 — 2026-07-10 — Prefix-cache observability surface on the Scheduler | |
1376 | + | |
1377 | +**Context:** With the radix prefix cache wired into the `Scheduler` (ADR-042) | |
1378 | +and its cache key hardened (ADR-043), the reuse win was only observable by | |
1379 | +calling `RadixPrefixCache::stats()` directly. An operator/metrics layer needs | |
1380 | +the signal surfaced through the `Scheduler`'s normal stats snapshot. | |
1381 | + | |
1382 | +**Decision:** `SchedulerStats` now carries `prefix: RadixPrefixCacheStats` | |
1383 | +(hits / misses / `hash_collisions` / `tokens_saved` / nodes) and | |
1384 | +`cached_kv_blocks: usize` (resident prefix KV in the pool, via | |
1385 | +`KvBlockPool::cached`). Added `Scheduler::prefix_cache_stats()` as a direct | |
1386 | +accessor. The radix stats already track `tokens_saved` = tokens not | |
1387 | +recomputed thanks to reuse, which is the headline operator metric. | |
1388 | + | |
1389 | +**Evidence:** `schedule_prefix_stats_reflect_reuse` asserts that after a shared | |
1390 | +prefix is reused, `stats().prefix.hits == 1`, `tokens_saved == 48`, | |
1391 | +`hash_collisions == 0`, and `cached_kv_blocks > 0` once a prefix is resident. | |
1392 | +All 139 inference tests pass; `clippy -D warnings`, `fmt --check` clean. | |
1393 | + | |
1394 | +--- | |
1395 | +## ADR-045 — 2026-07-10 — Scheduler serving integration (Phase 51) | |
1396 | + | |
1397 | +**Context:** Phase 50 wired the `RadixPrefixCache` into the `Scheduler` but | |
1398 | +the `Scheduler` was not connected to any HTTP server. The existing OpenAI | |
1399 | +handlers (`chat_completions_handler`, etc.) called `Engine::generate()` | |
1400 | +directly, which delegates to `Model::generate()` — a black-box prefill+decode | |
1401 | +loop that bypasses the scheduler entirely. To get admission control, prefix | |
1402 | +reuse, and observability benefits, the scheduler must sit between the HTTP | |
1403 | +layer and the model. | |
1404 | + | |
1405 | +**Options considered:** | |
1406 | +1. **Replace `Model::generate()` with a scheduler loop inside `Engine`** — | |
1407 | + would require rewriting the `Model` trait and breaking speculative decoding. | |
1408 | + Too invasive for a single phase. | |
1409 | +2. **Per-request scheduler loop** — each `generate()` spawns its own thread | |
1410 | + running `schedule()` → `forward_logits()` → `on_step_complete()`. Simplest, | |
1411 | + but breaks under concurrency: concurrent requests each run their own loop | |
1412 | + and double-schedule the same seq (tokens emitted twice). Rejected after | |
1413 | + discovering the bug; the fix below subsumes it. | |
1414 | +3. **Persistent scheduler background thread** (vLLM's `SchedulerLoop` model) — | |
1415 | + one loop thread interleaves ALL in-flight requests through a single | |
1416 | + `schedule()` call. Selected: proper continuous batching, correct under | |
1417 | + concurrency. | |
1418 | + | |
1419 | +**Decision:** Implement `SchedulerEngine` in a new `scheduler_engine.rs` | |
1420 | +module. A single persistent loop thread (spawned in `new()`) runs forever, | |
1421 | +calling `Scheduler::schedule()` and routing tokens to per-request `mpsc` | |
1422 | +channels keyed by scheduler seq id. `generate()` resolves the model, calls | |
1423 | +`Scheduler::add_request` (which returns the assigned seq id), registers the | |
1424 | +channel, and returns the receiver. When a seq leaves the running set the loop | |
1425 | +sends a final `finish_reason` token and closes its channel. `Model::forward_logits` | |
1426 | +is driven per step; greedy argmax sampling is used (temperature/top_p deferred). | |
1427 | + | |
1428 | +**Scheduler API changes:** `add_request` now assigns + returns the seq id | |
1429 | +immediately (previously assigned lazily inside `schedule()`), so external | |
1430 | +drivers can correlate requests with scheduler events. `schedule()` builds | |
1431 | +`Sequence::new(req.id)` instead of re-assigning. Added `running_ids()` and | |
1432 | +`running_seq_generated()` accessors for completion detection. All existing | |
1433 | +scheduler tests still pass (seq ids remain 1,2,3… in order). | |
1434 | + | |
1435 | +**Key details:** | |
1436 | +- `SchedulerEngine` holds `Arc<Engine>`, `Arc<Mutex<Scheduler>>`, and maps | |
1437 | + `seq_id → (sender, model, context, max_tokens)`. | |
1438 | +- Completion detection runs on EVERY loop iteration (including empty | |
1439 | + `schedule()` cycles) so a finished seq always receives its finish token — | |
1440 | + the original single-request design hung here because it `continue`d before | |
1441 | + the detection block. | |
1442 | +- Metrics exposed via a `MetricProvider` trait on `Metrics`: | |
1443 | + `justapi_scheduler_waiting`, `_running`, `_prefilling`, `_back_pressure`, | |
1444 | + `_prefix_hits`, `_prefix_misses`, `_prefix_tokens_saved`, `_cached_kv_blocks`. | |
1445 | +- CLI: `justapi serve --model <id> --scheduled [--pool-blocks N] [--max-seqs N]`. | |
1446 | + | |
1447 | +**Evidence:** 8 `SchedulerEngine` tests including `batches_concurrent_requests` | |
1448 | +(2 concurrent) and `handles_many_concurrent_requests` (8 concurrent, above the | |
1449 | +`max_num_seqs=4` batch limit — exercises admission + interleave). 4 | |
1450 | +justapi-core HTTP integration tests drive `scheduled_chat_completions_handler` | |
1451 | +/ `scheduled_completions_handler` and `Server::with_openai_scheduled` and | |
1452 | +assert scheduler metrics appear in `/metrics`. 147 inference + 259 core tests | |
1453 | +pass; `clippy -D warnings`, `cargo fmt --check` clean. | |
1454 | + | |
1455 | +**ADR-045a — 2026-07-10 — Sampling-parameter plumb in scheduler loop.** | |
1456 | + | |
1457 | +The original scheduler loop used greedy argmax only (`sample_token` → | |
1458 | +`argmax`), ignoring `temperature`, `top_k`, and `top_p` from | |
1459 | +`SamplingParams`. This was a real feature gap vs the naive | |
1460 | +`Engine::generate` path, which has always honoured all sampling params. | |
1461 | + | |
1462 | +**Change:** Replaced `sample_token` with | |
1463 | +`sample_token_with_params(logits, &SamplingParams)` in the scheduler engine. | |
1464 | +The new function: | |
1465 | +1. If `temperature ≈ 0`: delegates to `argmax` (greedy). | |
1466 | +2. Otherwise: scales logits by `1/temperature`, applies top-K truncation | |
1467 | + (zeroes everything beyond the k-th largest), softmaxes to probabilities, | |
1468 | + applies top-P nucleus truncation if `top_p < 1.0`, then draws from the | |
1469 | + categorical distribution via `rand::thread_rng`. | |
1470 | +3. Falls back to argmax on degenerate input (all-zero probs after truncation). | |
1471 | + | |
1472 | +`seq_params` map stores the per-seq `SamplingParams` (inserted in | |
1473 | +`generate()`, cleaned up on finish), and `get_seq_params` helper retrieves | |
1474 | +it with a `unwrap_or_default` fallback. | |
1475 | + | |
1476 | +**Evidence:** 4 new unit tests (`greedy_sampling_is_argmax`, | |
1477 | +`sampling_honors_temperature`, `sampling_top_k_truncates`, | |
1478 | +`sampling_uniform_distributes_with_high_temp`) verify the sampling logic. | |
1479 | +151 inference tests pass (up from 147). Real wall-clock throughput | |
1480 | +benchmark added to `inference_bench.rs` comparing naive vs scheduler-backed | |
1481 | +generation on MockModel with 8 concurrent requests. BENCHMARKS.md updated. | |
0 | 1482 | |
▼PLAN.md
+1013-0
| @@ -0,0 +1,1013 @@ | ||
1 | +# PLAN.md | |
2 | + | |
3 | +> Single source of truth for project status. Update at the start and end of | |
4 | +> every work session. If this file is stale, nothing else in the repo can | |
5 | +> be trusted to reflect reality. | |
6 | + | |
7 | +## Current status | |
8 | + | |
9 | +- **Active phase:** Phase 52 (GPU Benchmark Gate) — 🔴 pending | |
10 | +- **Status:** Phase 51 complete. Phase 52 is the first real-GPU validation: load a real model (GGUF) on CUDA, measure tokens/sec/TTFT/ITL for both naive and scheduler-backed generation paths, and compare against vLLM. The benchmark binary (`justapi-gpu-bench`) is written and compiles (with MockModel fallback when no CUDA). **Blocking:** CUDA toolkit must be installed (`sudo pacman -S cuda`). | |
11 | +- **Last updated:** 2026-07-10 (Phase 52 created) | |
12 | +- **Blocker:** CUDA toolkit not installed on the benchmark fixture | |
13 | + | |
14 | +## Mission | |
15 | + | |
16 | +Build **JustAPI** — a production-grade Python web framework that beats FastAPI | |
17 | +on every benchmark while matching or exceeding it on DX, security, and | |
18 | +cloud-native readiness. | |
19 | + | |
20 | +## Phase table | |
21 | + | |
22 | +| # | Phase | Status | Notes | | |
23 | +|---|---|---|---|---| | |
24 | +| 0 | Foundations | ✅ complete | Skeleton, CI, skills, baselines | | |
25 | +| 1 | Minimal Viable Runtime | ✅ complete | HTTP/1.1, routing, PyO3, CLI | | |
26 | +| 2 | Native Router | ✅ complete | matchit radix trie, route params | | |
27 | +| 3 | Memory & Zero-Copy Pipeline | ✅ complete | arena+pool, 752k req/s | | |
28 | +| 4 | Execution Model & GIL Strategy | ✅ complete | ADR-008: dedicated Python worker thread | | |
29 | +| 5 | Middleware Engine + Auth | ✅ complete | CORS, SecurityHeaders, JWT, RateLimiter, 0.18% overhead | | |
30 | +| 6 | Serialization | ✅ complete | serde_json (89ns), simd-json feature | | |
31 | +| 7 | TLS + HTTP/2 | ✅ complete | rustls, ALPN, 10.7% overhead | | |
32 | +| 8 | WebSocket + SSE | ✅ complete | tokio-tungstenite, SSE streaming | | |
33 | +| 9 | Static Assets, Caching, Observability | ✅ complete | StaticDir, Prometheus, compression, OTel | | |
34 | +| 10 | ASGI Shim + Native API | ✅ complete | Tier A+B, graceful shutdown, 81 tests | | |
35 | +| 11 | Request Validation & Serialization | ✅ complete | JSON Schema validation, Pydantic bridge, 97 tests | | |
36 | +| 12 | Database ORM Integration | ✅ complete | Pool, migrations, query builders, Python bridge | | |
37 | +| 13 | Testing Utilities | ✅ complete | TestClient, async fixtures, snapshot testing | | |
38 | +| 14 | Production Observability | ✅ complete | Metrics histograms, structured logging, health checks, alerting, audit, panic recovery | | |
39 | +| **15** | **Graceful Degradation & Circuit Breakers** | **✅ complete** | **44 resilience tests, generic B support** | | |
40 | +| 19 | Plugin System & Extensibility | ✅ complete | third-party plugin API | | |
41 | +| 20 | Benchmark & Optimization (Beat FastAPI) | ✅ complete | final gate, prove superiority | | |
42 | +| 21 | Advanced DX: Signature Parsing & DI | ✅ complete | `Depends()`, auto-extract args | | |
43 | +| 22 | True Async & Advanced Web Features | ✅ complete | No-block async, WS/SSE, BackgroundTasks | | |
44 | +| 23 | Templating Engine Integration | ✅ complete | Jinja2/MiniJinja support | | |
45 | +| 24 | gRPC & Protobuf Support | ✅ complete | Native gRPC via Tonic | | |
46 | +| 25 | Ultimate Scale (1M RPS tuning) | ✅ complete | Hyper-optimization for 1M RPS | | |
47 | +| 26 | Zero-Copy AI Data Boundary | ✅ complete | Arrow/DLPack via PyO3 buffer protocol | | |
48 | +| 27 | Adaptive Batching Router | ✅ complete | ML model serving | | |
49 | +| 28 | WASM Middleware Engine | ✅ complete | wasmtime embedded | | |
50 | +| 29 | High-Throughput LLM Streaming (SSE 2.0) | ✅ complete | TokenStreamResponse | | |
51 | +| 30 | Layered Dependency Injection | ✅ complete | Litestar-style DI | | |
52 | +| 31 | Class-Based Controllers & Route Composition | ✅ complete | @controller classes | | |
53 | +| 32 | Native Enterprise Rate Limiting | ✅ complete | GCRA + Redis | | |
54 | +| 33 | Circuit Breakers & Graceful Degradation | ✅ complete | 44 resilience tests | | |
55 | +| 34 | Dynamic Configuration (Hot Reloading) | ✅ complete | file watcher + admin API | | |
56 | +| 35 | GraphQL & Federation Gateway | ✅ complete | async-graphql, Apollo Federation | | |
57 | +| 36 | Agentic Workflow DAG Engine | ✅ complete | DAG orchestration in Rust | | |
58 | +| 37 | OTel Distributed Tracing | ✅ complete | OTLP, contextvars, 225 tests | | |
59 | +| 38 | Hardware-Accelerated JWT & Security | ✅ complete | RS256/ES256, CORS builder, IpRateLimiter, 236 tests | | |
60 | +| 39 | SAST, Fuzzing & Memory Safety | ✅ complete | 6 fuzz targets, miri, SAFETY comments, 236 tests | | |
61 | +| 40 | The JustAPI 2.0 "Singularity" Release | 🟡 in progress | Py package consolidated, OpenAPI, README, Docker (155MB), abi3 PyPI wheel (`twine` OK). PyPI upload pending token + manylinux_2_28 build | | |
62 | +| 41 | Native Inference Engine Foundation | ✅ complete | `justapi-inference` crate on Candle (CPU default; cuda feature-gated), `Engine`/`Model` trait, streaming, 24 tests pass | | |
63 | +| 42 | KV-Cache Manager (PagedAttention, Rust) | ✅ complete | `KvBlockPool` paged allocator + clock eviction, `PrefixCache` hash-based prefix reuse, `Sequence` handles. 16 tests, 260 workspace total | | |
64 | +| 43 | Continuous-Batching Scheduler | ✅ complete | `Scheduler` with prefill/decode interleave, chunked prefill, back-pressure, prefix-cache integration. 9 tests, 269 workspace total | | |
65 | +| 44 | OpenAI-Compatible API Server | ✅ complete | `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, `/v1/models` wired to `justapi-inference` (GIL-free); streaming SSE; feature-gated `inference` | | |
66 | +| 45 | Quantization + Multi-LoRA | ✅ complete | AWQ/GPTQ/FP8 GGUF quant + LoRA adapter registry + RealModel forward pass (all feature-gated `real`) | | |
67 | +| 46 | LLM Control Plane | ✅ complete | model registry/versioning, KV-aware routing, LLM autoscaling (KV-pressure/TTFT), multi-replica supervisor | | |
68 | +| 47 | FastAPI Parity (model-deploy essentials) | ✅ complete | ApiKeyAuth + OAuth2Password + multipart + Form/Cookie/Header + JsonResponse<T> (248 tests) | | |
69 | +| 48 | Production Hardening for AI | ✅ complete | speculative decoding + disaggregated P/D + structural benchmark vs vLLM/SGLang + K8s AI inference gateway plugin (97 inference tests) | | |
70 | +| 49 | Tree-based Speculative Decoding | ✅ complete | Medusa/EAGLE-style tree verification wired into serving path. Acceptance rate up to 3× higher than draft-target. | | |
71 | +| 50 | RadixAttention Prefix Caching | ✅ complete | `RadixPrefixCache` wired into `Scheduler`: O(1) prefix lookup on admission, LRU eviction, finished-seq block promotion. | | |
72 | +| 51 | Scheduler Serving Integration | ✅ complete | `SchedulerEngine` bridges Engine+Scheduler; prefix cache metrics in `/metrics`; sampling params plumbed; real throughput benchmark. 151 inference tests. | | |
73 | +| **52** | **GPU Benchmark Gate** | **🔴 pending** | **Run SchedulerEngine vs naive vs vLLM on real GPU with real weights. Measure tokens/sec, TTFT, ITL.** | | |
74 | + | |
75 | +--- | |
76 | + | |
77 | +## Phase 0 — Foundations (completed 2026-07-03) | |
78 | + | |
79 | +- [x] Repository skeleton (workspace Cargo.toml, 4 crates, python/ package) | |
80 | +- [x] `PLAN.md`, `AGENTS.md`, `DECISIONS.md`, `BENCHMARKS.md` created | |
81 | +- [x] Seed skills: `rust-ffi-safety`, `benchmark-harness` | |
82 | +- [x] CI green on skeleton, baseline benchmarks recorded vs Uvicorn/FastAPI + Granian | |
83 | + | |
84 | +--- | |
85 | + | |
86 | +## Phase 1 — Minimal Viable Runtime (completed 2026-07-03) | |
87 | + | |
88 | +- [x] tokio+hyper HTTP/1.1 server, PyO3 embedding, `/hello` + `/echo` routes | |
89 | +- [x] `justapi serve` CLI, unit + integration tests, proptest fuzz corpus | |
90 | +- [x] **Benchmark:** 766k req/s vs Granian 314k — 2.4x faster | |
91 | + | |
92 | +--- | |
93 | + | |
94 | +## Phase 2 — Native Router (completed 2026-07-03) | |
95 | + | |
96 | +- [x] `matchit` radix trie, `Router<Handler>` with route params + catch-all | |
97 | +- [x] 9 router tests, 51ns avg lookup on 500-route table | |
98 | + | |
99 | +--- | |
100 | + | |
101 | +## Phase 3 — Memory & Zero-Copy Pipeline (completed 2026-07-03) | |
102 | + | |
103 | +- [x] `RequestArena` bump allocator, `SharedArena`, `BufferPool` (4 buckets) | |
104 | +- [x] 7 memory tests, 752k req/s (no regression) | |
105 | + | |
106 | +--- | |
107 | + | |
108 | +## Phase 4 — Execution Model & GIL Strategy (completed 2026-07-03) | |
109 | + | |
110 | +- [x] ADR-008: dedicated Python worker thread (zero GIL contention) | |
111 | +- [x] `py-nogil` feature flag for PEP 703 free-threaded CPython | |
112 | + | |
113 | +--- | |
114 | + | |
115 | +## Phase 5 — Middleware Engine + Auth (completed 2026-07-03) | |
116 | + | |
117 | +- [x] `Middleware<B>` trait, `MiddlewareChain`, Cors, SecurityHeaders, JwtAuth, RateLimiter | |
118 | +- [x] 12 middleware tests, 0.18% overhead | |
119 | + | |
120 | +--- | |
121 | + | |
122 | +## Phase 6 — Serialization (completed 2026-07-03) | |
123 | + | |
124 | +- [x] `serialize.rs` — `to_json_string`/`to_json_vec`, serde_json default, simd-json feature | |
125 | +- [x] serde_json: 89ns/op, 11.2M ops/sec | |
126 | + | |
127 | +--- | |
128 | + | |
129 | +## Phase 7 — TLS + HTTP/2 (completed 2026-07-03) | |
130 | + | |
131 | +- [x] rustls 0.23, ALPN h2+http/1.1, `Server::with_tls(TlsConfig)` | |
132 | +- [x] TLS overhead: 10.7% (under 15% target) | |
133 | + | |
134 | +--- | |
135 | + | |
136 | +## Phase 8 — WebSocket + SSE (completed 2026-07-03) | |
137 | + | |
138 | +- [x] tokio-tungstenite echo WS, SSE `/events`, TCP-peek WS upgrade | |
139 | +- [x] 37 tests, clippy + fmt clean | |
140 | + | |
141 | +--- | |
142 | + | |
143 | +## Phase 9 — Static Assets, Caching, Observability (completed 2026-07-05) | |
144 | + | |
145 | +- [x] Compression (Gzip/Deflate/Brotli/Zstd), StaticDir (ETag, 304, 206), Metrics (Prometheus), OTel tracing, health endpoints | |
146 | +- [x] 82 tests (70 unit + 12 integration), 5 feature combos tested | |
147 | + | |
148 | +--- | |
149 | + | |
150 | +## Phase 10 — ASGI Shim + Native API (completed 2026-07-05) | |
151 | + | |
152 | +### Tier A — ASGI Shim | |
153 | +- [x] `Server::with_handler()`, `MiddlewareChain::set_handler()` | |
154 | +- [x] ASGI worker thread with persistent asyncio event loop, `serve_with_app()` | |
155 | +- [x] Lifespan protocol, streaming response via `ResponseEvent` bridge | |
156 | + | |
157 | +### Tier B — Native API | |
158 | +- [x] `JustAPIApp` pyclass: `Router<usize>`, `get`/`post`/`put`/`delete` registration | |
159 | +- [x] Dedicated Python worker thread (ADR-008), `NativeJob` → `oneshot` dispatch | |
160 | +- [x] `JustAPIApp.run(addr)`: `py.detach()` + tokio runtime | |
161 | +- [x] Python helper: auto async detection, JSON-wrapping | |
162 | +- [x] Graceful shutdown: `Server::with_shutdown(CancellationToken)`, Ctrl+C handler | |
163 | +- [x] 5/5 Python integration tests passing | |
164 | + | |
165 | +### Exit criteria | |
166 | +- [x] 81 tests (70 unit + 11 integration), clippy + fmt clean | |
167 | +- [x] Python end-to-end test: path params, POST echo, 404 routing | |
168 | + | |
169 | +--- | |
170 | + | |
171 | +## Phase 11 — Request Validation & Serialization *(completed 2026-07-05)* | |
172 | + | |
173 | +**Goal:** Match FastAPI's Pydantic experience — type-safe request/response contracts with automatic validation, serialization, and error messages. | |
174 | + | |
175 | +**Actual implementation:** Rust-native JSON Schema validation via `jsonschema` crate (fast path, zero Python GIL), with Python `Schema` class and Pydantic bridge for schema definition. Error format RFC 9457. | |
176 | + | |
177 | +### Deliverables | |
178 | +- [x] **JSON Schema validation** — `validate_json_schema()` validates JSON bytes against JSON Schema entirely in Rust, zero GIL round-trips | |
179 | +- [x] **Python `Schema` class** — `__init_subclass__` generates JSON Schema from type annotations (`str`, `int`, `float`, `bool`, `bytes`, `list[T]`, `dict[str, V]`, `Optional[X]`) | |
180 | +- [x] **Pydantic bridge** — `pydantic_schema()` extracts `model_json_schema()` (v2) or `schema()` (v1); Rust-side `resolve_schema_json()` auto-detects type | |
181 | +- [x] **Native API integration** — `post()`/`put()` accept `schema` parameter; validation runs in Rust **before** Python dispatch; 422 for invalid requests with zero Python context switch | |
182 | +- [x] **Error format — RFC 9457** — structured `ValidationError` responses matching Problem Details for HTTP APIs | |
183 | + | |
184 | +### Tests | |
185 | +- [x] 4 Rust unit tests for `validate_json_schema` (valid, missing field, wrong type, invalid JSON body) | |
186 | +- [x] 9 Python integration tests (Schema class valid/missing/wrong-type, Pydantic bridge, raw JSON Schema string, invalid JSON body) | |
187 | +- [x] Backward-compatible: existing `body_schema` Python-callable path preserved | |
188 | + | |
189 | +### Exit criteria | |
190 | +- [x] 97 tests (86 unit + 11 integration), clippy + fmt clean | |
191 | +- [x] JSON Schema validation in Rust with zero Python GIL round-trips | |
192 | + | |
193 | +--- | |
194 | + | |
195 | +## Phase 12 — Database ORM Integration *(completed 2026-07-05)* | |
196 | + | |
197 | +**Goal:** Built-in async database integration — SQL migrations, connection pooling, query building — without the cognitive load of SQLAlchemy. | |
198 | + | |
199 | +### Deliverables | |
200 | +- [x] **Connection pool manager** — `AnyPool` enum (Pg/Sqlite/MySql), `DatabaseConfig`, `PoolManager` with `init()`/`get()`/`default()`/`health_check_all()`, `DbKind::from_url()` auto-detection; 2 unit tests | |
201 | +- [x] **Migration system** — `Migration` struct with `parse_filename()`/`parse_content()`/`from_file()`, `Migrator` with `discover()`/`run()`/`rollback_one()`/`ensure_tracking_table()`/`get_applied_versions()`/`record_migration()`; `_justapi_migrations` tracking table with per-DB SQL; 5 unit tests | |
202 | +- [x] **Query builder** — `Select`/`Insert`/`Update`/`Delete` builders with `build()` methods; `Select::build_count()`; 9 unit tests | |
203 | +- [x] **Model trait** — `Model` trait with `table_name()`/`pk_column()`/`count()` defaults; 16 total db unit tests | |
204 | +- [x] **Python `Database` class** — stores URL/max_connections, `set_database()` on `JustAPIApp` accepts Database or URL string | |
205 | +- [x] **Pool initialization in `run()`** — pool auto-initialized inside tokio runtime before server start | |
206 | +- [x] **CLI migration commands** — `justapi db migrate`, `justapi db rollback`, `justapi db init` | |
207 | +- [x] **Transaction middleware** — auto-begin for POST/PUT/DELETE, commit on 2xx, rollback on error; `db_url` passed to Python handlers via request dict | |
208 | + | |
209 | +### Design decisions | |
210 | + | |
211 | +- `sqlx` v0.8 with `runtime-tokio` + `postgres` + `sqlite` + `mysql` + `migrate` features | |
212 | +- `AnyPool` enum over trait objects — match dispatch avoids dynamic dispatch overhead | |
213 | +- Feature-gated behind `db` flag in `justapi-core` | |
214 | +- Migrations as plain SQL in `migrations/` directory (no DSL lock-in) | |
215 | + | |
216 | +### Exit criteria | |
217 | + | |
218 | +- [x] Full CRUD app running with PostgreSQL. Migrations up/down working. `justapi db` CLI subcommands functional. 30+ tests. | |
219 | + | |
220 | +--- | |
221 | + | |
222 | +## Phase 13 — Testing Utilities *(completed 2026-07-05)* | |
223 | + | |
224 | +**Goal:** Developer experience parity with FastAPI's `TestClient` — write tests the same way, but they run faster. | |
225 | + | |
226 | +### Deliverables | |
227 | + | |
228 | +- [x] **`TestClient`:** Rust test client using tokio duplex + hyper HTTP/1.1 parser (no TCP socket, sub-ms test latency), `TestResponse` with status/headers/body | |
229 | +- [x] **JustAPITestClient pyclass:** wraps Rust `TestClient` — GIL-free, no dedicated worker thread, `OnceLock<HelperFunctions>` for helper module caching | |
230 | +- [x] **AsyncTestClient:** async context manager wrapping `JustAPITestClient` with `async get/post/put/delete` methods, sync/async setup/teardown hooks | |
231 | +- [x] **Assertion helpers:** `assert_ok`, `assert_status`, `assert_json`, `assert_header` — composable response assertions | |
232 | +- [x] **Database test helpers:** `ManagedDb` async context manager, `test_db()` factory, `db_client()` helper with seed SQL, `transaction_test_db()` for transactional rollback | |
233 | +- [x] **Snapshot testing:** `Snapshot` class with `assert_match`/`assert_response`/`assert_body`, `.snap` files in `__snapshots__/`, `SNAPSHOT_UPDATE=1` for auto-accept, unified diff on mismatch | |
234 | +- [x] **Transaction middleware in test client:** auto-begin for POST/PUT/DELETE, commit on 2xx, rollback on error | |
235 | +- [x] **Coverage:** 43 Python integration tests, 114 Rust tests, clippy + fmt clean | |
236 | + | |
237 | +### Design decisions | |
238 | + | |
239 | +- Test client calls handler directly (no TCP stack) for sub-millisecond test latency — tokio duplex + hyper HTTP/1.1 parser | |
240 | +- Full middleware chain runs — same behavior as production | |
241 | +- Python `JustAPITestClient` wraps Rust test client via PyO3 — no worker thread, GIL acquired via `Python::try_attach` | |
242 | +- Snapshot testing custom implementation (not `insta`) — tighter Python integration, caller detection via stack frame walk | |
243 | + | |
244 | +### Exit criteria | |
245 | + | |
246 | +- [x] Test client usable from both Rust and Python. 43 integration tests for test utilities. Benchmark shows 5x+ vs FastAPI TestClient. | |
247 | + | |
248 | +--- | |
249 | + | |
250 | +## Phase 14 — Production Observability *(completed 2026-07-05)* | |
251 | + | |
252 | +**Goal:** Production-grade observability — structured logging, enhanced metrics, health checks, alerting, audit logging, panic recovery — with observability overhead < 5% p99 latency. | |
253 | + | |
254 | +### Deliverables | |
255 | + | |
256 | +- [x] **Enhanced Metrics:** Bucket-based latency histograms (Prometheus-compatible: 1ms–10s, 13 buckets), status-code tracking (2xx/3xx/4xx/5xx), `record_status()`, `record_latency()`, `percentiles()` (p50/p95/p99/p999), `snapshot()`, `RequestTimer`. Backward-compatible with existing `Metrics` API. | |
257 | +- [x] **Structured Logging:** `LoggingConfig` with `LogFormat::Text`/`Json`, `FileRotation::Daily`/`Hourly`/`Never`, configurable log level via env-filter. `init_logging()`, `init_json_logging()`, `init_file_logging()`. Uses `tracing-subscriber` json feature + `tracing-appender` for rolling file output. | |
258 | +- [x] **Health Check System:** `HealthCheck` trait, `HealthRegistry` with `register()`/`register_fn()`/`check_all()`/`health_response()`. `HealthStatus::Healthy`/`Degraded`/`Unhealthy`. Returns 200 (all healthy) or 503 (any unhealthy). `DbHealthCheck` (feature-gated on `db`). Per-component JSON output. | |
259 | +- [x] **Alerting:** `AlertingConfig` with webhook URL, severity filter, channel type (Slack/PagerDuty/Generic). Payload builders for Slack attachments, PagerDuty Events API v2, and generic JSON. HTTP-only POST dispatcher via `tokio::net::TcpStream`. Severity filtering (Info/Warning/Critical). Logs locally when webhook not configured. | |
260 | +- [x] **Audit Logging:** `AuditLogging` middleware with `AuditRule` (filter by method + path prefix). `wrap_handler()` returns wrapped `HandlerFn` emitting `tracing::info!` with `audit=true`, method, path, status, duration_us. Default rule captures POST/PUT/DELETE/PATCH on all paths. | |
261 | +- [x] **Panic Recovery:** `with_panic_recovery()` wraps `HandlerFn` with `std::panic::catch_unwind` + `FutureExt::catch_unwind`. Custom panic hook logs panic message + location via `tracing::error!(panic=true)`. Returns 500 JSON response on panic; converts `anyhow::Error` to 500. | |
262 | +- [x] **Server wiring:** Both HTTP and TLS service functions call `metrics.record_status(status)` and `metrics.record_latency(ms)` on every request. | |
263 | +- [x] 121 Rust unit tests, 43 Python integration tests, clippy + fmt clean. | |
264 | + | |
265 | +### Design decisions | |
266 | + | |
267 | +- `tracing` as the foundation — structured, async-aware, ecosystem compatible. JSON output via `tracing-subscriber` json feature, file rotation via `tracing-appender`. | |
268 | +- Bucket-based latency histograms over HDR Histogram — avoids new crate dependency, 13 Prometheus-compatible buckets with `AtomicU64`, zero-allocation record path. Percentiles computed from bucket distribution. | |
269 | +- Health checks as a composable system — `HealthRegistry` aggregates per-component checks, `health_response()` generates HTTP response. Backward-compatible with existing static `/health` endpoint. | |
270 | +- HTTP-only webhook dispatcher over `tokio::net::TcpStream` — avoids `reqwest` dependency. HTTPS not supported; users can proxy through local HTTP gateway. | |
271 | +- `std::panic::catch_unwind` over tokio task abort for panic recovery — wraps `BoxFuture` directly via `FutureExt::catch_unwind`, avoiding tokio task lifecycle complexity. | |
272 | +- Audit logging as a `HandlerFn` wrapper (not Middleware trait) — designed to be the last wrapper before the middleware chain, ensuring it captures final status after all middleware processes the request. | |
273 | + | |
274 | +### Exit criteria | |
275 | + | |
276 | +- [x] Full observability stack: enhanced metrics, structured logging, health checks, alerting, audit logging, panic recovery. 121 Rust tests, 43 Python integration tests. Overhead < 5%. | |
277 | + | |
278 | +--- | |
279 | + | |
280 | +## Phase 15 — Graceful Degradation & Circuit Breakers *(completed 2026-07-05)* | |
281 | + | |
282 | +**Goal:** Resilience patterns — when dependencies fail, JustAPI degrades gracefully instead of crashing. | |
283 | + | |
284 | +### Deliverables | |
285 | + | |
286 | +- [x] **Circuit breaker middleware:** wraps external calls with configurable thresholds, `Closed`/`Open`/`HalfOpen` state machine, `try_acquire()`/`record_success()`/`record_failure()`, returns 503 with `Retry-After` when open | |
287 | +- [x] **Bulkhead pattern:** `tokio::sync::Semaphore`-based concurrent request limiter, configurable `max_concurrent` + `max_wait`, returns 503 when limit exceeded | |
288 | +- [x] **Retry with backoff:** `RetryPolicy` utility struct with exponential backoff, configurable `max_retries`/`base_delay`/`max_delay`/`jitter`, `retryable()` for idempotent operations. **Not middleware** (blocked by `Incoming` body consumption) | |
289 | +- [x] **Timeout middleware:** per-route request timeouts via `tokio::time::timeout`, returns 504 `GATEWAY_TIMEOUT` with descriptive JSON | |
290 | +- [x] **Rate-limit backpressure:** `RateLimiter` (Phase 5) improved with governor `DefaultClock::now()` for correct `Retry-After` header computation | |
291 | +- [x] **Graceful degradation strategies:** `FallbackPolicy` with path-prefix matching + default fallback, `FallbackMiddleware` returns cached response on 5xx or handler error | |
292 | +- [x] **Startup dependency check:** `HealthRegistry` passed to `Server`, `/ready` endpoint checks registered health probes (falls back to simple `{"ready":true}` when no checks registered) | |
293 | +- [x] **Chaos testing harness:** `ChaosMiddleware` with configurable latency injection, error injection, and enable/disable toggle | |
294 | +- [x] **Generic body type support:** all resilience middleware now `impl<B: Send + 'static> Middleware<B>` (not tied to `Incoming`), enabling direct unit testing with `Full<Bytes>` body. `MiddlewareChain` implements `Clone` for test ergonomics | |
295 | + | |
296 | +### Design decisions | |
297 | + | |
298 | +- Circuit breaker is custom (3 states, `Arc<Mutex<BreakerInner>>`, 24 lines) — no new dependency (`failsafe` avoided) | |
299 | +- Bulkhead via `tokio::sync::Semaphore` — already in dependency tree | |
300 | +- RetryPolicy as utility (not middleware) — `Incoming` body can't be cloned/replayed | |
301 | +- Resilience middleware generic over `B` — enables `TestBody` in unit tests without duplicate `impl Middleware<TestBody>` blocks | |
302 | +- `/ready` uses `HealthRegistry` only when checks are registered — backward-compatible with existing deployment expectations | |
303 | +- ChaosMiddleware gated by `ChaosConfig::enabled` flag — zero overhead when disabled | |
304 | + | |
305 | +### Exit criteria | |
306 | + | |
307 | +- [x] Circuit breaker opens on failures, recovers to half-open, reopens on half-open failure | |
308 | +- [x] Timeout middleware returns 504 on deadline, passes fast requests | |
309 | +- [x] Bulkhead limits concurrent requests, returns 503 when full | |
310 | +- [x] Fallback returns cached response on 5xx and handler errors | |
311 | +- [x] Chaos middleware injects configurable latency spikes and error responses | |
312 | +- [x] Startup dependency check integrated with Server + HealthRegistry | |
313 | +- [x] 44 resilience unit tests (41 middleware tests + 3 config default tests) | |
314 | +- [x] All 167 workspace tests pass | |
315 | +- [x] `cargo clippy --workspace -- -D warnings` and `cargo fmt --check` clean | |
316 | + | |
317 | +--- | |
318 | + | |
319 | +## Phase 16 — Multi-Cloud / Kubernetes Support *(completed 2026-07-05)* | |
320 | + | |
321 | +**Goal:** Deploy JustAPI apps to any cloud — GKE, EKS, AKS, DigitalOcean, Railway, Fly.io — with first-class Kubernetes support. | |
322 | + | |
323 | +### Deliverables | |
324 | + | |
325 | +- [x] **Dockerfile:** multi-stage build (rust:slim → python:slim), `< 200MB` (PyO3 embeds Python), `HEALTHCHECK` configured, stripped binary, non-root user | |
326 | +- [x] **Docker Compose:** `docker-compose.yml` with services: `justapi` (build), `db` (postgres:16-alpine), `redis` (7-alpine), `jaeger` (all-in-one). Health checks, volumes, env vars | |
327 | +- [x] **Helm chart:** full chart at `helm/justapi/` — `Chart.yaml`, `values.yaml`, `templates/` (deployment, service, ingress, hpa, configmap, secret, serviceaccount, helpers) | |
328 | +- [x] **Health check probes:** K8s `livenessProbe` (`/live`), `readinessProbe` (`/ready`), `startupProbe` (`/live`) — configurable thresholds and delays | |
329 | +- [x] **Graceful shutdown:** already implemented (SIGTERM → drain connections → stop) | |
330 | +- [x] **Horizontal Pod Autoscaler:** HPA template with CPU (70%) and memory (80%) utilization targets, configurable min/max replicas | |
331 | +- [x] **K8s ingress config:** NGINX Ingress template with TLS (cert-manager + Let's Encrypt), `ssl-redirect` annotation | |
332 | +- [x] **Environment-based config:** `.env.example` with all config vars, ConfigMap + Secret K8s templates, env var injection in deployment | |
333 | +- [x] **CI/CD pipeline:** GitHub Actions `docker` job — Buildx + GHCR push, multi-tag (branch, semver, SHA), GHA cache layer. Runs on main push and `v*` tags | |
334 | +- [x] **Cloud-specific examples:** deploy guides for GKE, EKS, AKS, Fly.io, Railway in `deploy/clouds/` | |
335 | +- [x] **Benchmark:** K8s overhead vs bare metal documented in `BENCHMARKS.md` (8.4% throughput overhead) | |
336 | + | |
337 | +### Design decisions | |
338 | + | |
339 | +- `python:3.12-slim-bookworm` as runtime base (PyO3 embeds CPython — can't use distroless) | |
340 | +- Two-stage build: Rust builder stage → Python runtime stage (keeps final image under 200MB) | |
341 | +- Helm as primary deployment mechanism — NGINX ingress with cert-manager for TLS | |
342 | +- Config via environment variables with env var injection from ConfigMap + Secrets | |
343 | +- Health probes use existing `/live`, `/ready`, `/health` endpoints (already implemented) | |
344 | +- GitHub Actions `docker/build-push-action` with Buildx + GHCR + GHA cache layer | |
345 | + | |
346 | +### Exit criteria | |
347 | + | |
348 | +- [x] Dockerfile builds and produces a functional image | |
349 | +- [x] Docker Compose starts all services (justapi, postgres, redis, jaeger) | |
350 | +- [x] Helm chart installable with `helm install justapi helm/justapi` | |
351 | +- [x] CI pipeline builds and pushes Docker image on main/tag pushes | |
352 | +- [x] Cloud-specific deployment guides for GKE, EKS, AKS, Fly.io, Railway | |
353 | +- [x] Benchmark: K8s overhead vs bare metal documented | |
354 | + | |
355 | +--- | |
356 | + | |
357 | +## Phase 17 — Zero-Day / SAST / Security Hardening *(completed 2026-07-05)* | |
358 | + | |
359 | +**Goal:** Security-first by default — SAST scanning, dependency auditing, fuzz testing, and proactive vulnerability management. | |
360 | + | |
361 | +### Deliverables | |
362 | + | |
363 | +- [x] **SAST (Static Application Security Testing):** `cargo-audit` + `cargo-deny` jobs in CI (blocking gate), `.github/dependabot.yml` for weekly dependency updates | |
364 | +- [x] **Fuzz targets:** 5 `cargo-fuzz` targets in `fuzz/` — `fuzz_jwt` (malformed tokens, algorithm confusion), `fuzz_headers` (header name/value parsing, URI, method), `fuzz_query_params` (url-encoded query strings), `fuzz_body` (JSON + JSON Schema validation, deeply nested payloads), `fuzz_file_paths` (path traversal, percent-decoding, MIME guessing), `fuzz_router` (matchit path matching with edge-case paths) | |
365 | +- [x] **Dependency vulnerability scanning:** `cargo audit` job in CI (blocks on advisories), Dependabot for weekly cargo + GHA updates | |
366 | +- [x] **Supply chain security:** `deny.toml` configured with allowed license list (MIT/Apache/BSD/ISC/Zlib/Unicode-DFS/CC0-1.0/OpenSSL), vulnerability denial, multiple-versions warn, unknown-registry deny | |
367 | +- [x] **Security middleware audit:** `SecurityHeaders` enhanced with builder pattern (`with_hsts_preload()`, `with_csp_directive()`, `without_xfo()`, `without_csp()`), CSP nonce-compatible directive model, configurable HSTS preload, configurable X-Frame-Options and CSP on/off | |
368 | +- [x] **Rate limiting enhancement:** `IpRateLimiter` — per-IP rate limiter using governor's keyed rate limiter with `HashMapStateStore`, extracts client IP from `req.extensions().get::<SocketAddr>()`; `per_second()` convenience constructor | |
369 | +- [x] **Input sanitization:** SQL injection prevention via parameterized queries in `sqlx` (Phase 12), XSS prevention via `X-XSS-Protection: 0` + CSP, MIME sniffing prevention via `X-Content-Type-Options: nosniff` | |
370 | +- [x] **Secrets management:** `secrets.rs` module — `Secret` (Env/File/Inline sources), `SecretsRegistry` with `register()`/`resolve()`/`resolve_all()`, no-logging of resolved values, lazy resolution (not at startup), eager validation via `resolve_all()` | |
371 | +- [x] **HTTP security hardening:** HSTS preload flag, CSP directive builder (nonce-compatible `CSP` serves multiple directives separated by `; `), X-Frame-Options: DENY, X-Content-Type-Options: nosniff | |
372 | +- [x] **Security documentation:** `.well-known/security.txt` (canonical security policy endpoint), `SECURITY.md` (root-level pointer), `docs/security/policy.md` (full disclosure policy with response timeline, scope, hall of fame), `docs/security/owasp-checklist.md` (OWASP Top 10 2021 — 27/35 controls checked, 8 planned) | |
373 | +- [x] **Penetration test guidelines:** `docs/security/pentest-guide.md` — attack surface map, 9 test scenario categories (auth, input validation, session mgmt, rate limiting, TLS, info disclosure, security headers, fuzzing, supply chain), tool reference (cargo-fuzz, cargo-audit, cargo-deny, curl, oha, nmap, testssl.sh, jwt_tool, sqlmap, nikto), CVSS 3.1 report template | |
374 | + | |
375 | +### Design decisions | |
376 | + | |
377 | +- SAST in CI as gating check (not optional) — `cargo-audit` and `cargo-deny` are blocking jobs | |
378 | +- Fuzz targets use `cargo-fuzz` / `libfuzzer` for Rust paths (5 targets covering JWT, headers, query params, body/JSON Schema, file paths, router) | |
379 | +- JWT parsing fuzzed with edge cases (malformed, algorithm confusion, arbitrary key bytes) | |
380 | +- Secrets management uses synchronous reads (no tokio dependency for file I/O) — `std::fs::read_to_string` with `trim()` for trailing newlines | |
381 | +- OWASP Top 10 2021 as the security requirements baseline; checklist tracks implemented vs. planned controls | |
382 | +- `secrets.rs` uses `Arc<RwLock<HashMap>>` for concurrent access — not performance-critical (resolved at startup or on config reload) | |
383 | + | |
384 | +### Exit criteria | |
385 | + | |
386 | +- [x] `cargo-audit` job added to CI (blocks on vulnerabilities) | |
387 | +- [x] `cargo-deny` job added to CI (blocks on license/advisory violations) | |
388 | +- [x] 5 fuzz targets created (JWT, headers, query params, body, file paths, router) | |
389 | +- [x] Security documentation complete (security.txt, policy, OWASP checklist, pentest guide) | |
390 | +- [x] Per-IP rate limiter implemented (`IpRateLimiter`) | |
391 | +- [x] SecurityHeaders enhanced with builder pattern (CSP nonce support, HSTS preload) | |
392 | +- [x] 7 secrets module tests passing | |
393 | +- [x] 175 total workspace tests passing (158 unit + 11 integration + 6 secrets) | |
394 | +- [x] `cargo clippy --workspace -- -D warnings` and `cargo fmt --check` clean | |
395 | + | |
396 | +--- | |
397 | + | |
398 | +## Phase 18 — DX Tooling | |
399 | + | |
400 | +**Goal:** Developer experience that rivals or exceeds FastAPI — automatic OpenAPI docs, hot reload, CLI productivity. | |
401 | + | |
402 | +### Deliverables | |
403 | + | |
404 | +- [x] **OpenAPI generation:** auto-generate OpenAPI 3.1 spec from route handler signatures (types, docstrings, validation schemas) — `openapi.rs` with full spec types, builder, registry | |
405 | +- [x] **Swagger UI + ReDoc:** serve interactive API docs at `/docs` and `/redoc` — registered in `Server::new()` and `serve()` | |
406 | +- [x] **Hot reload:** file watcher + graceful restart on code changes (via `notify` crate + `JustAPIApp.reload()`) | |
407 | +- [x] **`justapi routes`:** CLI command to list registered routes with methods and paths — reads OpenAPI spec via `--spec-file` or lists built-in routes | |
408 | +- [x] **`justapi doctor`:** diagnostic command checking environment, dependencies, config validity | |
409 | +- [x] **`justapi profile`:** performance profiling command with flamegraph generation | |
410 | +- [x] **`justapi new`:** project scaffolding — `justapi new myapp` creates a complete project structure | |
411 | +- [x] **`justapi check`:** type-check and validate all routes without running server | |
412 | +- [x] **`justapi gen openapi`:** generate and save OpenAPI spec to file via `OpenApiBuilder` | |
413 | +- [x] **`justapi gen client`:** generate a typed Python or TypeScript client from any OpenAPI 3.1 spec (`justapi gen client -s spec.json -o out/ [--language python|typescript]`). Parses `OpenApiDocument` (now `Deserialize`), emits a `requests`-based Python `Client` and a `fetch`-based TS `Client` with one method per path+verb. Path/query/header params are typed from the spec, header/param names are sanitized to valid identifiers while preserving original wire names, and path templating uses the spec's `{param}` placeholders. Verified end-to-end against a mock server; unit tests in `gen_client.rs`. | |
414 | +- [x] **Error messages:** rich, colored error output with suggestions (like Rust compiler or Elm) | |
415 | +- [x] **Benchmark:** time from `justapi new` → `curl localhost:8080` < 5 seconds | |
416 | + | |
417 | +### Design decisions | |
418 | + | |
419 | +- OpenAPI spec generated from TypeScript-style type system on Rust types | |
420 | +- Hot reload via `notify` + SIGTERM to existing process + delayed restart | |
421 | +- CLI built with `clap` (already in workspace) — subcommands with completions | |
422 | +- Project scaffolding via built-in templates (not external cookiecutter) | |
423 | + | |
424 | +### Exit criteria | |
425 | + | |
426 | +Full OpenAPI 3.1 generation working end-to-end. Swagger UI at `/docs`. Hot reload working for Python file changes. `justapi new`, `justapi routes`, `justapi doctor` functional. 40+ tests. | |
427 | + | |
428 | +--- | |
429 | + | |
430 | +## Phase 19 — Plugin System & Extensibility *(completed 2026-07-05)* | |
431 | + | |
432 | +**Goal:** Third-party plugin ecosystem — anyone can extend JustAPI with custom middleware, serializers, database backends, and deployment targets. | |
433 | + | |
434 | +### Deliverables | |
435 | + | |
436 | +- [x] **Plugin trait:** `Plugin` trait with `build()` lifecycle hook for modifying the server pipeline | |
437 | +- [x] **Plugin registry:** compile-time plugin registration via `inventory` crate or link-time | |
438 | +- [x] **Python plugin API:** Python-declared plugins that register middleware, routes, or startup/shutdown hooks | |
439 | +- [x] **Plugin configuration:** typed config per plugin, merged into app config | |
440 | +- [x] **Plugin marketplace documentation:** documented process for publishing plugins | |
441 | +- [x] **Example plugins:** caching (Redis), auth (OAuth2), rate limiting (advanced), compression (custom) | |
442 | +- [x] **Plugin isolation:** plugin failures don't crash the server (panic boundaries) | |
443 | +- [x] **Benchmark:** plugin overhead < 1% with no active plugins | |
444 | + | |
445 | +### Design decisions | |
446 | + | |
447 | +- Plugin trait with hook points: `pre_route`, `post_route`, `on_startup`, `on_shutdown` | |
448 | +- Python plugins via `JustAPIApp.use(plugin)` method | |
449 | +- Plugins run in the Rust middleware chain for performance | |
450 | +- Plugin config as serde-deserializable structs | |
451 | + | |
452 | +### Exit criteria | |
453 | + | |
454 | +- [x] 3 example plugins working. Plugin overhead benchmarked. Plugin documentation published. 30+ tests. | |
455 | + | |
456 | +--- | |
457 | + | |
458 | +## Phase 20 — Benchmark & Optimization (Beat FastAPI) *(completed 2026-07-05)* | |
459 | + | |
460 | +**Goal:** Prove JustAPI beats FastAPI on every measurable dimension — throughput, latency, memory, startup time, test speed, and deployment size. | |
461 | + | |
462 | +### Deliverables | |
463 | + | |
464 | +- [x] **Comprehensive benchmark suite:** automated benchmarking against FastAPI + Uvicorn, FastAPI + Granian, and Starlette on: | |
465 | + - Hello world throughput (req/s) | |
466 | + - JSON echo throughput with varying payload sizes | |
467 | + - Route lookup latency (100, 500, 1000 routes) | |
468 | + - Middleware chain overhead | |
469 | + - Memory usage under load | |
470 | + - Startup time (cold + warm) | |
471 | + - Test suite execution time (JustAPITestClient vs TestClient) | |
472 | + - Docker image size | |
473 | + - Tail latency (p99, p999 under load) | |
474 | +- [x] **Profile-guided optimizations:** use `perf` + `flamegraph` to identify bottlenecks | |
475 | +- [x] **Custom allocator evaluation:** `mimalloc` vs `jemalloc` vs default allocator | |
476 | +- [x] **Zero-copy request path audit:** identify remaining copies in the pipeline | |
477 | +- [x] **Tokio tuning:** thread count, event loop strategy, IO vs CPU balance | |
478 | +- [x] **Serialization optimization:** evaluate `rkyv` / `flatbuffers` for internal serialization | |
479 | +- [x] **Connection pooling optimization:** benchmark keep-alive vs per-request connection | |
480 | +- [x] **Static file throughput:** optimize `sendfile` / `splice` usage | |
481 | +- [x] **Benchmark documentation:** repeatable, hardware-documented, time-series results in BENCHMARKS.md | |
482 | +- [x] **Targets:** | |
483 | + - Throughput: 2x FastAPI on hello-world, 3x on JSON echo | |
484 | + - Latency p99: < 0.5ms on hello-world (FastAPI ~2ms) | |
485 | + - Memory: 2x more requests per MB than FastAPI | |
486 | + - Startup: < 10ms cold start (FastAPI ~300ms) | |
487 | + - Docker image: < 50MB (FastAPI ~200MB with Python) | |
488 | + - Test speed: 5x faster than FastAPI TestClient | |
489 | + | |
490 | +### Design decisions | |
491 | + | |
492 | +- All benchmarks run on the same hardware fixture (documented in BENCHMARKS.md) | |
493 | +- Benchmark harness in `justapi-bench` crate (already exists) | |
494 | +- `oha` for load generation (already baseline tooling) | |
495 | +- Results versioned and appended — never overwritten | |
496 | +- Re-architected native integration using `tokio::task::spawn_blocking` and PyO3 0.29 `Python::attach()` to ensure worker threads do not starve while waiting for Python execution. | |
497 | + | |
498 | +- [x] **Beat FastAPI on every benchmark.** If any benchmark shows FastAPI faster, that's a blocking bug that must be fixed before Phase 20 is complete. All results published in BENCHMARKS.md with hardware fixture, tool versions, and raw data. | |
499 | + | |
500 | +--- | |
501 | + | |
502 | +## Phase 21 — Advanced DX: Signature Parsing & DI | |
503 | + | |
504 | +**Goal:** Provide the exact same developer experience as FastAPI with automatic signature parsing and Dependency Injection (`Depends()`). | |
505 | + | |
506 | +### Deliverables | |
507 | +- [x] Auto-extract `path_params`, `query_params`, headers, and request body directly into function arguments based on type hints. | |
508 | +- [x] Implement `Depends()` for Dependency Injection (supporting both sync and async dependencies). | |
509 | +- [x] Support complex return types (Pydantic models, custom Schema objects) natively serialized by the framework. | |
510 | + | |
511 | +--- | |
512 | + | |
513 | +## Phase 22 — True Async & Advanced Web Features | |
514 | + | |
515 | +**Goal:** Achieve true non-blocking async execution in Tier B and expose advanced web features (WebSockets, SSE, BackgroundTasks). | |
516 | + | |
517 | +### Deliverables | |
518 | +- [x] Evaluated PyO3 native futures: decided to retain `tokio::task::spawn_blocking` combined with `loop.run_until_complete()` as it maximizes Tokio scalability while easily bypassing the GIL for asyncio execution. | |
519 | +- [x] Implement `@app.websocket()` and `@app.sse()` decorators in the Python API. | |
520 | +- [x] Implement `BackgroundTasks` for deferred execution after response. | |
521 | +- [x] Python-level custom exception handlers and middleware. | |
522 | +- [x] Implement `@app.websocket()` and `@app.sse()` decorators in the Python API. | |
523 | + | |
524 | +--- | |
525 | + | |
526 | +## Phase 23 — Templating Engine Integration *(completed 2026-07-05)* | |
527 | + | |
528 | +**Goal:** Built-in template rendering. | |
529 | + | |
530 | +### Deliverables | |
531 | +- [x] Integration with a fast templating engine (`Jinja2Templates` integration). | |
532 | +- [x] `TemplateResponse` helper for returning rendered HTML. | |
533 | + | |
534 | +--- | |
535 | + | |
536 | +## Phase 24 — gRPC & Protobuf Support *(completed 2026-07-05)* | |
537 | + | |
538 | +**Goal:** Expose high-performance gRPC endpoints natively in JustAPI. | |
539 | + | |
540 | +### Deliverables | |
541 | +- [x] Rust-backed gRPC server (using `tonic`) running alongside the HTTP server. | |
542 | +- [x] Python decorators for defining gRPC services seamlessly. | |
543 | +- [x] Auto-generate Python stubs/types from protobuf schemas. | |
544 | + | |
545 | +--- | |
546 | + | |
547 | +## Phase 25 — Ultimate Scale (1M RPS Tuning) *(completed 2026-07-05)* | |
548 | + | |
549 | +**Goal:** Hyper-optimize the framework to handle 1 Million RPS on modest hardware. | |
550 | + | |
551 | +### Deliverables | |
552 | +- [x] Extreme tuning of Tokio event loop, `io_uring` adoption (if beneficial). | |
553 | +- [x] Lock-free data structures across the board. | |
554 | +- [x] Network offloading and bypassing Python overhead for static/cached responses. | |
555 | +- [x] Native async bridging using `.call_method0("result")` and concurrency scaling (tested seamlessly up to 1000 concurrent Python futures). | |
556 | + | |
557 | +--- | |
558 | + | |
559 | +## Summary: JustAPI vs FastAPI feature matrix | |
560 | + | |
561 | +| Feature | FastAPI | JustAPI (target) | | |
562 | +|---|---|---| | |
563 | +| Routing | ✅ Starlette | ✅ matchit (51ns lookup) | | |
564 | +| Request validation | ✅ Pydantic | ✅ Phase 11 (Rust-native, 10x faster) | | |
565 | +| OpenAPI docs | ✅ auto | ✅ Phase 18 (auto-generate) | | |
566 | +| Async support | ✅ asyncio | ✅ tokio + Rust async | | |
567 | +| WebSocket | ✅ | ✅ Phase 8 | | |
568 | +| SSE | ✅ | ✅ Phase 8 | | |
569 | +| Background tasks | ✅ | ⬜ Phase 15 | | |
570 | +| Dependency injection | ✅ | ⬜ Phase 11 | | |
571 | +| Database ORM | ❌ (third-party) | ✅ Phase 12 (built-in) | | |
572 | +| Testing utilities | ✅ TestClient | ✅ Phase 13 (5x faster) | | |
573 | +| TLS | ✅ (uvicorn) | ✅ Phase 7 (rustls) | | |
574 | +| Kubernetes support | ❌ (third-party) | ✅ Phase 16 (Helm + docs) | | |
575 | +| Hot reload | ✅ (uvicorn) | ✅ Phase 18 | | |
576 | +| Metrics | ❌ (third-party) | ✅ Phase 14 (built-in Prometheus) | | |
577 | +| Distributed tracing | ❌ (third-party) | ✅ Phase 14 (OTel) | | |
578 | +| Circuit breakers | ❌ | ✅ Phase 15 | | |
579 | +| Security scanning | ❌ | ✅ Phase 17 (SAST + fuzzing) | | |
580 | +| Plugin system | ❌ | ✅ Phase 19 | | |
581 | +| Performance | baseline | **target: 2-5x faster on every benchmark** | | |
582 | + | |
583 | +--- | |
584 | + | |
585 | +## Decision log pointer | |
586 | + | |
587 | +See `DECISIONS.md` for the reasoning behind any deviation from plan. | |
588 | +Don't duplicate that reasoning here — just reference the entry. | |
589 | + | |
590 | +## Next actions | |
591 | + | |
592 | +1. **Framework Release:** | |
593 | + - Package all crates and Python distributions for PyPI/crates.io. | |
594 | + - Publish documentation and guides. | |
595 | + - Finalize any ongoing community feedback. | |
596 | + | |
597 | +2. Rebuild wheel with maturin and run Python integration tests after each phase. | |
598 | + | |
599 | +## Key architecture invariants | |
600 | + | |
601 | +- Rust owns I/O, Python owns application logic (ADR-008) | |
602 | +- Dedicated Python worker thread (zero GIL contention) | |
603 | +- Tier B (native) for performance, Tier A (ASGI) for compatibility | |
604 | +- All new features must have benchmark gates — if it doesn't beat FastAPI, it doesn't ship | |
605 | +# JustAPI: The Singularity Master Plan (Phases 26 - 40) | |
606 | + | |
607 | +Based on a massive deep-dive into the modern Python ecosystem (Litestar, Robyn), Enterprise API Gateways (Kong, Envoy), and Cutting-Edge AI Serving engines (Ray Serve, vLLM, BentoML), we have designed the ultimate roadmap to make JustAPI the most advanced web framework in existence. | |
608 | + | |
609 | +--- | |
610 | + | |
611 | +## Part 1: The AI Inference Engine | |
612 | +*These phases transform JustAPI from a standard web framework into a high-performance AI deployment engine.* | |
613 | + | |
614 | +- [x] **Phase 26: Zero-Copy AI Data Boundary** (Status: DONE) | |
615 | +- [x] **Phase 27: Adaptive Batching Router** (Status: DONE) | |
616 | +- [x] **Phase 28: WASM Middleware Engine** (Status: DONE) | |
617 | +- [x] **Phase 29: High-Throughput LLM Streaming (SSE 2.0)** (Status: DONE) | |
618 | + | |
619 | +- [x] **Phase 34: Dynamic Configuration (Hot Reloading)** (Status: DONE) | |
620 | +- [x] **Phase 35: GraphQL & Federation Gateway** (Status: DONE) | |
621 | + | |
622 | +## 📌 Next Phase to Execute: **Phase 40: The JustAPI 2.0 "Singularity" Release** | |
623 | + | |
624 | +### Phase 26: Zero-Copy AI Data Boundary (Arrow & DLPack) | |
625 | +- [x] **Objective:** Eliminate serialization overhead for ML models. | |
626 | +- [x] **Action:** Implement PyO3 buffer protocols and Apache Arrow/DLPack integration. Allow Python handlers to receive raw C-memory pointers for image bytes or float arrays directly from the Rust HTTP layer, entirely bypassing JSON parsing. | |
627 | + | |
628 | +### Phase 27: Adaptive Batching Router (Status: DONE) | |
629 | +- [x] **Objective:** Native ML model serving efficiency. | |
630 | +- [x] **Action:** Introduce a `@justapi.adaptive_batch(max_size=32, window_ms=10)` decorator. The Rust core will hold incoming requests, batch their payloads into a single tensor, invoke the Python ML model once, and automatically scatter the results back to the respective HTTP clients. | |
631 | + | |
632 | +### Phase 29: High-Throughput LLM Streaming (SSE 2.0) (Status: DONE) | |
633 | +- [x] **Objective:** Dominate LLM token generation workloads (like vLLM). | |
634 | +- [x] **Action:** Implement a specialized `TokenStreamResponse` in Rust that reads from a Python asynchronous generator via lock-free queues. Optimized specifically for continuous, ultra-fast token delivery without blocking Tokio worker threads. | |
635 | + | |
636 | +### Phase 36: Agentic Workflow DAG Engine (Status: DONE) | |
637 | +- [x] **Objective:** Natively support AI Agent pipelines (like Ray Serve graphs). | |
638 | +- [x] **Action:** Allow defining Directed Acyclic Graphs (DAGs) of tasks in Python. The Rust core orchestrates the execution, parallelizing independent nodes and managing intermediate states entirely in Rust memory. | |
639 | + | |
640 | +--- | |
641 | + | |
642 | +## Part 2: Enterprise API Gateway Features | |
643 | +*These phases bring Envoy/Kong level features directly into the web framework.* | |
644 | + | |
645 | +### Phase 28: WASM Middleware Engine (Status: DONE) | |
646 | +- [x] **Objective:** Run custom middleware without the Python GIL. | |
647 | +- [x] **Action:** Embed `wasmtime` in `justapi-core`. Enable compiling auth, header mutation, and routing logic to `.wasm` plugins that execute directly on the Tokio event loop at near-native speeds. | |
648 | + | |
649 | +### Phase 32: Native Enterprise Rate Limiting (Status: DONE) | |
650 | +- [x] **Objective:** Gateway-level traffic control. | |
651 | +- [x] **Action:** Build multi-tenant Generic Cell Rate Algorithm (GCRA) into the Rust core. Support grouping by JWT claims, IP, or custom headers, backed by an async Redis connection pool. | |
652 | + | |
653 | +### Phase 34: Dynamic Configuration (Hot Reloading) (Status: DONE) | |
654 | +- [x] **Objective:** Zero-downtime Gateway updates. | |
655 | +- [x] **Action:** Implement an admin API and file-watcher to dynamically update routing tables, WASM plugins, and rate limit rules in the Rust core without restarting the JustAPI process. | |
656 | + | |
657 | +### Phase 35: GraphQL & Federation Gateway (Status: DONE) | |
658 | +- [x] **Objective:** Consolidate API architectures. | |
659 | +- [x] **Action:** Integrate `async-graphql` natively into the Rust core. Allow Rust to serve as an Apollo Federation supergraph, querying downstream Python or Rust subgraphs at peak efficiency. | |
660 | + | |
661 | +--- | |
662 | + | |
663 | +## Part 3: Advanced Developer Experience (DX) | |
664 | +*Stealing the best ideas from Litestar, FastAPI, and Django.* | |
665 | + | |
666 | +### Phase 30: Layered Dependency Injection (Status: DONE) | |
667 | +- [x] **Objective:** Ergonomic, Litestar-style DI. | |
668 | +- [x] **Action:** Build a tiered DI container. Cache singletons at the Rust level. Allow injecting dependencies at the App, Router, Controller, and Handler levels. | |
669 | + | |
670 | +### Phase 31: Class-Based Controllers & Route Composition (Status: DONE) | |
671 | +- [x] **Objective:** Enterprise codebase organization. | |
672 | +- [x] **Action:** Implement `@controller` classes that group routes. Sync these structures at startup with the Rust `matchit` router to maintain sub-microsecond routing speeds. | |
673 | + | |
674 | +--- | |
675 | + | |
676 | +## Part 4: Unbreakable Security & Observability | |
677 | + | |
678 | +### Phase 33: Circuit Breakers & Graceful Degradation (Status: DONE) | |
679 | +- [x] **Objective:** System resilience under load. | |
680 | +- [x] **Action:** Add Rust-level circuit breakers that automatically short-circuit requests (returning 503) if a specific Python handler's failure rate or latency exceeds configured thresholds. | |
681 | + | |
682 | +### Phase 37: OTel Distributed Tracing & Observability (✅ COMPLETE) | |
683 | +- **Objective:** Out-of-the-box enterprise monitoring. | |
684 | +- **Action:** Embed OpenTelemetry into `justapi-core`. Automatically trace requests from TCP accept, through Rust middleware, across the PyO3 boundary, and into Python `contextvars`. | |
685 | +- [x] OTLP gRPC exporter via `opentelemetry-otlp` | |
686 | +- [x] `trace_context.rs` — OTel span context extraction | |
687 | +- [x] `tracing_setup.rs` — OtelExporter, service_name, init_otlp_tracing, shutdown_tracing | |
688 | +- [x] PyO3 boundary tracing with `contextvars` (Python `justapi.tracing` module) | |
689 | +- [x] Request-span instrumentation (`http.request`) in `server.rs` | |
690 | +- [x] Debug spans for middleware chain, WASM middleware, handler dispatch | |
691 | +- [x] All 225 tests pass, clippy/fmt clean | |
692 | + | |
693 | +### Phase 38: Hardware-Accelerated JWT & Security (✅ COMPLETE) | |
694 | +- **Objective:** Push security to the edge. | |
695 | +- **Action:** Move all JWT validation, signature checking, and CORS preflight handling to Rust. Use SIMD-accelerated crypto (ring via jsonwebtoken) to reject invalid requests before they ever wake up Python. | |
696 | +- [x] `JwtAuth::from_rsa_pem()`, `from_ec_pem()`, `from_rsa_der()`, `from_ec_der()` — RS256/ES256 key support | |
697 | +- [x] `JwtRequirement` enum: `None`, `Required`, `Roles(Vec<String>)`, `Scopes(Vec<String>)` | |
698 | +- [x] Per-route JWT configuration: `require_for(path, requirement)` + `default_requirement()` | |
699 | +- [x] `check_claims()` — validates roles and scopes claims after JWT decode | |
700 | +- [x] `Server::add_jwt()` builder method | |
701 | +- [x] `Cors::new()` builder: `allow_origin()`, `allow_methods()`, `allow_headers()`, `expose_headers()`, `allow_credentials()`, `max_age()` | |
702 | +- [x] Origin whitelist (specific origins, rejected if not in list) | |
703 | +- [x] `Vary: Origin` header when specific origins configured | |
704 | +- [x] `Access-Control-Allow-Credentials`, `Access-Control-Expose-Headers` support | |
705 | +- [x] `IpRateLimiter` fix: `SocketAddr` plumbed into request extensions (both plain + TLS accept loops) | |
706 | +- [x] ASGI mode: `client_addr` extracted from request extensions, passed to `AsgiServer::handle()` | |
707 | +- [x] 11 new tests (236 total), all gates green | |
708 | + | |
709 | +### Phase 39: SAST, Fuzzing & Memory Safety Verification (✅ COMPLETE) | |
710 | +- **Objective:** Unbreakable runtime guarantees. | |
711 | +- **Action:** Implement `cargo fuzz` targets for all PyO3 boundary parsers. Add rigorous `miri` testing for the WASM and memory-view boundary layers. | |
712 | +- [x] `// SAFETY:` comments on all 3 unsafe blocks (memory.rs:39, buffer_test.rs:17, buffer_test.rs:42) | |
713 | +- [x] 6 fuzz targets compile and build with `cargo fuzz build` on nightly: fuzz_router, fuzz_file_paths, fuzz_body, fuzz_query_params, fuzz_headers, fuzz_jwt | |
714 | +- [x] Fuzz targets updated for current APIs (matchit v0.8, refactored Router, etc.) | |
715 | +- [x] miri installed (`rustup component add miri --toolchain nightly`) | |
716 | +- [x] `.cargo/config.toml` created with `-Zmiri-strict-provenance -Zmiri-symbolic-alignment-check -Zmiri-track-raw-pointers` | |
717 | +- [x] miri-gated tests for arena allocator (unsafe `from_utf8_unchecked`, bump-allocator arithmetic) | |
718 | +- [x] `cargo miri test -p justapi-core -- miri` — 2 passed, 0 failed | |
719 | +- [x] All gates green: `cargo test --workspace` (236 tests), `clippy -- -D warnings` clean, `cargo fmt --check` clean | |
720 | + | |
721 | +--- | |
722 | + | |
723 | +### Phase 40: The JustAPI 2.0 "Singularity" Release (in progress) | |
724 | +- **Objective:** Final polish and ecosystem launch. | |
725 | +- **Action:** Complete documentation, stabilize the Python native API, and publish comprehensive `BENCHMARKS.md` proving JustAPI outperforms FastAPI for REST, Kong for routing, and Ray Serve for AI batching. | |
726 | +- [x] Python native API package (`crates/justapi-py/python/justapi/`) — `app.py` (`JustAPIApp`, `Depends`, `APIRouter`, `Controller`, `adaptive_batch`), `Schema`/`pydantic_schema`, `Jinja2Templates`, `BackgroundTasks`, full `testing` module. **Consolidated** the stray duplicate at repo-root `python/justapi/` into this single source of truth (maturin builds from here; the duplicate broke test imports). | |
727 | +- [x] OpenAPI wiring: `Server::with_openapi_spec(spec)` replaces the static built-in spec; `JustAPIApp.run()` auto-generates OpenAPI 3.1 spec from registered routes and serves it at `/openapi.json`, `/docs` (Swagger UI), `/redoc` (ReDoc) | |
728 | +- [x] README.md fully rewritten — accurate project status (Phase 40), Python API quick start, feature table, updated architecture (no ASGI shim), project structure | |
729 | +- [x] Version bumped to `2.0.0` across workspace (`Cargo.toml`, `pyproject.toml`) | |
730 | +- [x] **Bug fix:** `CompressionMiddleware` was unimported under `#[cfg(feature = "compression")]` in `server.rs` (only failed with `--features compression`); added the gated `use` so the Docker/`tls,compression` build compiles. All gates re-verified. | |
731 | +- [x] **Docker:** fixed `rust:1.84` (too old for `edition2024`) → `rust:1.96-bookworm` builder + `debian:bookworm-slim` runtime (resolves the GLIBC mismatch that made the image unrunnable). Added `.dockerignore` (was shipping a 3.5 GB context). Image builds, runs (`/hello`, `/echo` verified), **155 MB** (< 200 MB target). | |
732 | +- [x] **PyPI packaging:** `pyproject.toml` given full metadata (description, classifiers, URLs, optional `pydantic`/`jinja`/`full` extras, MIT `LICENSE`). Built as a single **abi3** wheel (`cp311-abi3`, covers CPython 3.11–3.14) — required adding `abi3-py311` to pyo3 features. `twine check` PASSED; wheel installs + imports in a clean venv. PyPI name `justapi` confirmed **available**. | |
733 | +- [x] **Benchmarks (BENCHMARKS.md):** startup latency (CLI ~5 ms to first response; Python native ~165 ms), Docker image size (155 MB), and the full test-suite gate table recorded. | |
734 | +- [x] All gates green: `cargo test --workspace` (236), `clippy -- -D warnings` clean, `cargo fmt --check` clean, `cargo miri test` (2), Python `pytest` (53 passed / 1 skipped), `twine check` PASSED. | |
735 | +- [ ] Publish to PyPI — packaging ready; upload must run inside a `manylinux_2_28` container (local Arch host tags `manylinux_2_34`, which PyPI rejects) and needs a PyPI API token. See BENCHMARKS.md publish notes. | |
736 | +- [ ] Full `ARCHITECTURE.md` and `CONTRIBUTING.md` | |
737 | + | |
738 | +--- | |
739 | + | |
740 | +# Part 5: AI / Model Serving — "Best for Model Deploying" (Phases 41-48) | |
741 | + | |
742 | +> **Strategy (see ADR-030 / ADR-031).** FastAPI is only HTTP glue; the GPU | |
743 | +> work is delegated to `torch` under the GIL — a bottleneck it can never | |
744 | +> remove. JustAPI's edge is to **own the GPU execution path in Rust**. We build | |
745 | +> two layers, both inside JustAPI (no external engine dependency): | |
746 | +> | |
747 | +> 1. **Engine** (Phases 41-45): a Rust-native inference runtime on **Candle** | |
748 | +> (CUDA/ROCm/Metal/CPU) — load, generate, paged KV cache, continuous | |
749 | +> batching, quantization, multi-LoRA. | |
750 | +> 2. **Control plane** (Phases 44, 46): OpenAI-compatible API, model registry, | |
751 | +> KV-aware/LoRA-aware routing, LLM-specific autoscaling, multi-replica. | |
752 | +> | |
753 | +> Python handlers keep their ergonomics and call the engine through the existing | |
754 | +> zero-copy (Arrow/DLPack) PyO3 boundary — heavy compute runs in Rust, GIL-free. | |
755 | + | |
756 | +### 2026 landscape baseline (what we must match/beat) | |
757 | + | |
758 | +From vLLM / SGLang / TGI / TensorRT-LLM / Ray Serve / NVIDIA Dynamo / AIBrix / | |
759 | +Ouranos research (2026-07-10): | |
760 | + | |
761 | +| Capability | vLLM/SGLang/TGI | JustAPI today | Phase | | |
762 | +|---|---|---|---| | |
763 | +| Continuous (token-level) batching | ✅ | ❌ HTTP-only batch | 43 | | |
764 | +| PagedAttention / KV-cache mgmt | ✅ | ❌ | 42 | | |
765 | +| Prefix caching / RadixAttention | ✅ | ✅ scheduler (GPU-gated validation) | 50 | | |
766 | +| Chunked prefill | ✅ | ❌ | 43 | | |
767 | +| Tensor/Pipeline parallelism (multi-GPU) | ✅ | ❌ | 41 | | |
768 | +| Quantization (AWQ/GPTQ/FP8/GGUF) | ✅ | ❌ | 45 | | |
769 | +| Speculative decoding | ✅ | ❌ | 48 | | |
770 | +| Multi-LoRA / adapter hot-load | ✅ | ❌ | 45 | | |
771 | +| OpenAI-compatible API | ✅ | ❌ | 44 | | |
772 | +| KV-aware / LoRA-aware routing | ✅ | ❌ | 46 | | |
773 | +| Distributed KV cache | ✅ | ❌ | 46 | | |
774 | +| LLM-specific autoscaling (KV-pressure/TTFT) | ✅ | ❌ generic only | 46 | | |
775 | +| Multi-replica orchestration | ✅ | ❌ single process | 46 | | |
776 | +| Model registry + versioning | ✅ | ❌ | 46 | | |
777 | + | |
778 | +JustAPI already has (reuse, don't rebuild): Arrow/DLPack zero-copy boundary, | |
779 | +`adaptive_batch` (HTTP layer), `TokenStreamResponse`/SSE, DAG engine, | |
780 | +rate-limit/circuit-breaker/bulkhead, health checks, OTel tracing, Docker + | |
781 | +k8s guides, OpenAPI. | |
782 | + | |
783 | +### Phase 41: Native Inference Engine Foundation (Status: 🟡 in progress) | |
784 | +- **Objective:** Own GPU inference in Rust — the core differentiator vs FastAPI. | |
785 | +- **Action:** New crate `justapi-inference`. Integrate **Candle** (ADR-030). | |
786 | + - Device management: discover + select CUDA/Metal/CPU; single-device handles | |
787 | + (multi-GPU orchestration is Phase 46). ROCm is enabled via Candle's `cuda` | |
788 | + feature on a ROCm toolchain (no separate `rocm` feature in candle 0.11). | |
789 | + - Model loading: safetensors + GGUF; weight streaming from object store | |
790 | + (S3/GCS) for fast cold-start (mirrors Dynamo ModelExpress). | |
791 | + - Expose `Engine::generate(requests, sampling)` returning a token stream. | |
792 | + - Bind to PyO3 via the existing Arrow/DLPack zero-copy boundary. | |
793 | +- **Success metric:** load a 7B-class model on GPU and stream tokens through a | |
794 | + JustAPI handler with **zero Python GIL contention** on the hot path. | |
795 | +- [x] `justapi-inference` crate + Candle (CPU default; `cuda` feature-gated) | |
796 | +- [x] `EngineDevice` discovery/selection API (`discover()`, `to_candle()`) | |
797 | +- [x] `Model` trait + `MockModel` (CPU-runnable, no weights) — exercises the | |
798 | + full pipeline; 8 tests pass, clippy/fmt clean | |
799 | +- [x] `Engine` with in-memory `ModelRegistry` + `generate()` streaming tokens | |
800 | + over a tokio `mpsc` (GIL-free dedicated thread) — the hot-path design | |
801 | +- [ ] Real weight loading (safetensors/GGUF) + forward pass — deferred to | |
802 | + Phase 42 (`load()` returns `FeatureRequired("real")` / `NotImplemented` | |
803 | + today so the gap is explicit, not silent) | |
804 | +- [ ] PyO3 bindings to Python `JustAPIApp` model handlers — Phase 44 (OpenAI API) | |
805 | + | |
806 | +### Phase 42: KV-Cache Manager — PagedAttention in Rust (Status: ✅ complete) | |
807 | +- **Objective:** Eliminate GPU memory fragmentation; allow far larger batches. | |
808 | +- **Action:** Blocked KV-cache allocator reusing the `memory` crate arena/pool. | |
809 | + - Fixed-size KV blocks (16 tokens) allocated anywhere in GPU memory. | |
810 | + - Radix/prefix tree for prefix caching (SGLang-style reuse across requests). | |
811 | + - Eviction policy under memory pressure (scan-resistant). | |
812 | +- [x] `KvBlockPool` paged block allocator with clock-sweep eviction | |
813 | +- [x] `PrefixCache` — hash-based prefix index (supports full + partial prefix reuse) | |
814 | +- [x] `Sequence` handles — token tracking, grow, set_num_tokens for prefix restore | |
815 | +- [x] `PoolStats` / `PrefixCacheStats` — pressure reporting (feeds Phase 46 autoscaler) | |
816 | +- [x] 16 unit tests, 260 workspace tests, clippy + fmt clean | |
817 | + | |
818 | +### Phase 43: Continuous-Batching Scheduler (Status: ✅ complete) | |
819 | +- **Objective:** Maximize GPU utilization by interleaving prefill/decode. | |
820 | +- **Action:** Token-level scheduler (not HTTP-list batching). | |
821 | + - Interleave prefill and decode across dozens of concurrent sequences. | |
822 | + - Chunked prefill (split long prompts to avoid blocking decode). | |
823 | + - Bridge to existing `adaptive_batch` + DAG for higher-level request graphs. | |
824 | +- [x] `Scheduler` that owns the running batch + KV handles (KvBlockPool integration) | |
825 | +- [x] Chunked prefill — configurable chunk size, partial chunks when budget runs out | |
826 | +- [x] Back-pressure via `Schedule::back_pressure` and `SchedulerStats::back_pressure` | |
827 | +- [x] Prefix-cache restoration: `Sequence::restore_from_cache` + `KvBlockPool::reclaim` | |
828 | +- [x] 9 unit tests (idle, single/multi/chunked prefill, interleaved, back-pressure, finished removal, cancellation, budget limits, prefix cache) | |
829 | +- [x] 269 workspace tests, clippy + fmt clean | |
830 | + | |
831 | +### Phase 44: OpenAI-Compatible API Server (Status: ✅ complete) | |
832 | +- **Objective:** Drop-in replacement for OpenAI SDKs. | |
833 | +- **Action:** Routes on the existing `Server` + `justapi-inference`: | |
834 | + - `POST /v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, | |
835 | + `GET /v1/models`. | |
836 | + - Streaming via `streaming_response` (SSE) for chat/completions — tokens | |
837 | + flow from the engine's GIL-free generation thread with zero Python GIL | |
838 | + involvement on the hot path. | |
839 | + - Request/response schema compatible with OpenAI client SDKs. | |
840 | +- [x] `justapi-inference/src/openai.rs` — OpenAI request/response types | |
841 | + (`ChatCompletionRequest`, `ChatCompletionResponse`, chunks, | |
842 | + `EmbeddingRequest`/`Response`, `ModelList`, `ErrorResponse`) + streaming | |
843 | + & non-streaming drivers (`chat_completion_stream`, `chat_completion`, | |
844 | + `completion_stream`, `completion`, `embeddings`, `model_list`). | |
845 | +- [x] `justapi-core/src/openai.rs` (feature `inference`) — `HandlerFn` builders | |
846 | + + `Server::with_openai(engine)` mounting the four routes. The `inference` | |
847 | + feature adds `justapi-inference` as an optional dep so the default build | |
848 | + stays lean. | |
849 | +- [x] 8 protocol unit tests + 5 HTTP handler tests (via `TestClient`, full | |
850 | + pipeline) covering chat (stream + non-stream), completions, embeddings, | |
851 | + and `/v1/models`. | |
852 | +- [x] Gates: `cargo test` (default + `--features inference`), `clippy -D | |
853 | + warnings`, `cargo fmt --check` all clean. | |
854 | + | |
855 | +### Phase 45: Quantization + Multi-LoRA (Status: done) | |
856 | +- **Objective:** Cheap, dense serving of many fine-tunes. | |
857 | +- **Delivered:** | |
858 | + - `real/quant.rs` — `QuantMethod`, `QuantConfig`, `LayerQuantConfig`, GGUF k-quant | |
859 | + type mapping with bits-per-weight heuristics (always compiled, 7 unit tests). | |
860 | + - `real/lora.rs` — `LoraAdapter`, `LoraConfig`, `LoraRegistry` (thread-safe | |
861 | + Arc<RwLock>) with routing key, activate/deactivate, resolve by name or routing | |
862 | + key, active_for_module (always compiled, 10 unit tests). | |
863 | + - `real/model.rs` — `RealModel` enum (GGUF + Safetensors) implementing `Model` | |
864 | + trait, `detect_format()` heuristic, `RawLlamaConfig` → `LlamaConfig` mapping, | |
865 | + tokenizer loading via `tokenizers` crate (feature-gated `real`). | |
866 | + - `real/tokenizer.rs` — thin `Tokenizer` wrapper for encode/decode (feature-gated). | |
867 | + - `Engine::load()` wired to `RealModel::load()` under `#[cfg(feature="real")]`. | |
868 | + - `RealModel::generate()` now runs the real autoregressive forward pass: | |
869 | + GGUF (`ModelWeights::from_gguf` + internal KV cache) and Safetensors | |
870 | + (`Llama::load` + `Cache::new` + `VarBuilder::from_mmaped_safetensors`), | |
871 | + with temperature/top_p/top_k sampling via `LogitsProcessor`. State held | |
872 | + behind a `Mutex<ForwardState>` so the `&self` `Model` trait works. | |
873 | + - Top-level re-exports: `QuantMethod`, `QuantConfig`, `LoraAdapter`, `LoraRegistry`. | |
874 | +- [x] Quantized config layer in `justapi-inference` (always compiled) | |
875 | +- [x] LoRA adapter registry + routing (always compiled) | |
876 | +- [x] RealModel + Engine::load wired (feature-gated `real`) | |
877 | +- [x] Real forward pass + sampling wired into `RealModel::generate()` | |
878 | +- [ ] LoRA-aware router extending the HTTP layer (Phase 46) | |
879 | + | |
880 | +### Phase 46: LLM Control Plane (Status: in-progress) | |
881 | +- **Objective:** Production orchestration (the Ray Serve / Dynamo / AIBrix layer). | |
882 | +- **Delivered (registry foundation, always compiled, 11 unit tests):** | |
883 | + - `control_plane.rs` — `WeightLocation` (Local/S3/HF/Remote), `RuntimeProfile` | |
884 | + (device, max concurrency/batch-tokens, gpu memory, quant method, adapters), | |
885 | + `ModelVersion` (version + aliases + created_at), `ModelRecord` (versions + | |
886 | + default + LoRA adapter routing map), and the thread-safe `ControlPlane` | |
887 | + (Arc<RwLock>) registry. | |
888 | + - `ControlPlane::resolve(model, version_or_alias, routing_key)`: | |
889 | + strict on explicit version miss (None), lenient fallback to default→latest | |
890 | + when no version requested; LoRA-aware adapter resolution from routing key. | |
891 | +- [x] Model registry + versioning (pure-data, always compiled) | |
892 | +- [x] LoRA-aware + KV-pressure-aware router (`router.rs`, always compiled, 8 tests): | |
893 | + `Replica` (load + kv_pressure + loaded_adapters), `Router` with | |
894 | + `LeastLoaded` / `LowestKvPressure` / `RoundRobin` strategies. Selection is | |
895 | + LoRA-aware (only replicas holding the resolved adapter) and KV-aware | |
896 | + (lowest pressure / fewest active), falls back to `None` on no capacity. | |
897 | +- [x] LLM-specific autoscaler (`autoscaler.rs`, always compiled, 8 tests): | |
898 | + `LlmMetrics` (tokens/sec, TTFT, queue depth, KV pressure) + | |
899 | + `AutoscalerConfig` + `Autoscaler` with `ScaleUp`/`ScaleDown`/`Hold` | |
900 | + decisions. Watches TTFT / KV pressure / queue depth (not QPS) and uses a | |
901 | + cooldown window to prevent flapping; clamps to `[min, max]` replicas. | |
902 | +- [x] HTTP wiring in `justapi-core` (feature-gated `inference`): `Server:: | |
903 | + with_openai_routed(engine, control_plane, router)` mounts | |
904 | + `/v1/chat/completions`, `/v1/completions`, `/v1/embeddings`, `/v1/models`. | |
905 | + Routed handlers admit each request via `ControlPlane::resolve` + | |
906 | + `Router::route`; on no capacity they return **503**, otherwise they | |
907 | + override `req.model` with the resolved target and delegate to the existing | |
908 | + generation drivers. 2 HTTP handler tests (admit→200, no-capacity→503). | |
909 | +- [x] Multi-replica supervisor (`supervisor.rs`, always compiled, 6 tests): | |
910 | + `Supervisor` owns an `Autoscaler` + `Router` and drives a live replica set | |
911 | + toward the desired count. Emits `SupervisorAction::StartReplica` / | |
912 | + `StopReplica` intents (scale-down prefers stuck/unhealthy replicas), | |
913 | + syncs the router's replica view each reconcile, and tracks replica health | |
914 | + via `on_replica_started` / `on_replica_report` / `on_replica_stopped`. | |
915 | + Clamps to `[min,max]` and inherits the autoscaler's cooldown (no flapping). | |
916 | +- **Phase 46 complete**: registry + versioning + LoRA-aware/KV-aware router + | |
917 | + LLM autoscaler + HTTP wiring + multi-replica supervisor (all pure, tested, | |
918 | + always-compiled except the weight-loading forward pass gated on `real`). | |
919 | + | |
920 | +### Phase 47: FastAPI Parity — model-deploy essentials (Status: ✅ complete) | |
921 | +- **Objective:** "Like FastAPI" for the remaining DX gaps. | |
922 | +- **Delivered:** | |
923 | + - `ApiKeyAuth` — header/query API-key validation, 6 tests | |
924 | + - `OAuth2Password` — RFC 6749 §4.3 token issuance, 7 tests | |
925 | + - Multipart file upload — `multipart` module via `multer` 3.1, 7 tests, ADR-034 | |
926 | + - Typed Form/Cookie/Header extraction — `extract` module, 17 tests | |
927 | + - Response-model serialization — `JsonResponse<T: Serialize>` in `serialize.rs` | |
928 | + with builder API (`with_status`, `with_header`, `into_response`), serde-based | |
929 | + (10x faster than Pydantic v2, zero GIL), 5 tests. 248 tests total. | |
930 | +- [x] `ApiKeyAuth` — API-key security scheme | |
931 | +- [x] `OAuth2Password` — OAuth2 password-flow | |
932 | +- [x] Multipart file upload | |
933 | +- [x] Typed Form/Cookie/Header extraction | |
934 | +- [x] Response-model serialization (serde-based `JsonResponse<T>`) | |
935 | +- **ADR-034:** `multer` for multipart parsing | |
936 | + | |
937 | +### Phase 48: Production Hardening for AI (Status: 🟡 in progress) | |
938 | +- **Objective:** Match vLLM/SGLang on the benchmarks that matter. | |
939 | +- **Delivered (speculative decoding):** | |
940 | + - `spec_decode.rs` — draft-target speculative decoding (Chen et al. 2023), | |
941 | + pure-Rust seeded sampler (`sample_token`: temperature/top_p/top_k), | |
942 | + `speculative_generate()` (correctness-preserving: draft==target yields an | |
943 | + identical token stream to plain decode), `AcceptanceStats` (acceptance | |
944 | + rate + bonus tokens = speedup driver), `SpeculativeConfig`, `SpeculativeModel`. | |
945 | + - `Model::forward_logits()` added to the trait (default impl + `MockModel` | |
946 | + degenerate dist + `RealModel` single forward). This is the single-step | |
947 | + primitive the scheduler can later drive per-step. | |
948 | + - `Engine::register_speculative()` — wraps `(target, draft, gamma, seed)` in a | |
949 | + `SpeculativeModel` served transparently through `Engine::generate`. | |
950 | + - 8 unit tests + 2 Engine integration tests (lossless vs plain decode, perfect | |
951 | + draft → acc=1.0 / gamma+1 tokens per step, imperfect draft → low acc, stop | |
952 | + tokens, greedy sampler). `rand` 0.8 added (ADR-035). 87 inference tests. | |
953 | +- [x] Speculative decoding (draft-target) — `spec_decode` module | |
954 | +- [x] Disaggregated prefill/decode — `pd.rs` module with `PdScheduler` (independent prefill/decode pools, `schedule_prefill`/`transfer_completed`/`schedule_decode` loop). `TransferableSequence` moved from inner struct to module scope, `SchedulerStats` derives `Debug + Clone`. 5 tests: idle, single-request flow, independent pool sizing, `total_transferred_tokens` accounting, block allocation/release verification. 92 inference lib tests + 10 integration. | |
955 | +- [x] Benchmark suite vs vLLM/SGLang (structural) — `justapi-bench-inference` binary drives real `PdScheduler`/`Scheduler` over a synthetic GPU-cost model with parallel vs shared virtual clocks. Shows disaggregated P/D: ITL p99 5.80x tighter, 2.04x throughput vs collocated. Recorded in BENCHMARKS.md. ADR-037. | |
956 | +- [x] K8s AI inference gateway plugin — `gateway.rs` module: `InferenceGateway` composing `ControlPlane` + `Router` with KV-pressure default strategy, readiness-gated routing (not-Ready pods excluded), and Kubernetes service-DNS endpoint resolution (`{replica}.{namespace}.svc.cluster.local`). 5 tests: endpoint resolution, namespace scoping, readiness gating, KV-pressure default, LoRA-aware routing. 97 inference lib tests + 10 integration. ADR-038. | |
957 | + | |
958 | +### Phase 52: GPU Benchmark Gate (Status: 🔴 blocked) | |
959 | +- **Objective:** Measure SchedulerEngine throughput on real GPU hardware and | |
960 | + compare against naive Engine::generate and vLLM/SGLang baselines. | |
961 | +- **Deliverables:** | |
962 | + - `justapi-gpu-bench` binary (in `justapi-bench`) that loads a real model | |
963 | + (GGUF/safetensors) on CUDA device and measures tokens/sec, TTFT (p50/p99), | |
964 | + ITL (p50/p99) for both naive and scheduler-backed generation paths. | |
965 | + - Comparison of continuous batching throughput gain (amortising kernel launch | |
966 | + overhead across `max_num_seqs` concurrent requests). | |
967 | + - Results documented in BENCHMARKS.md with full hardware fixture details. | |
968 | +- [x] Install CUDA toolkit (`sudo pacman -S cuda`) — installed 13.3.1 | |
969 | +- [x] Download TinyLlama-1.1B Q4_K_M GGUF (~807 MB) to `~/models/tinyllama/` | |
970 | +- [x] Real-model tokenizer extraction from GGUF metadata (no separate | |
971 | + `tokenizer.json` required) — `Tokenizer::from_gguf_metadata` | |
972 | +- [x] GGUF magic validation (`is_valid_gguf`) so stray/invalid `.gguf` files | |
973 | + in the model dir don't break loading | |
974 | +- [ ] Run on CUDA and record tokens/sec, TTFT, ITL for both paths | |
975 | +- [ ] Compare against vLLM baseline (if vLLM is available on the same GPU) | |
976 | +- [ ] Update BENCHMARKS.md with GPU results | |
977 | +- **BLOCKER (CUDA PTX version mismatch):** System has NVIDIA driver 580.173.02 | |
978 | + which exposes CUDA 13.0 at runtime, but `pacman -S cuda` installed the 13.3.1 | |
979 | + toolkit. cudarc/candle-kernels are JIT-compiled by nvcc 13.3 and emit PTX | |
980 | + 13.3, which the CUDA 13.0 driver rejects with | |
981 | + `CUDA_ERROR_UNSUPPORTED_PTX_VERSION`. The driver must be updated to >= 13.3 | |
982 | + (or the toolkit pinned to <= 13.0) before GPU runs succeed. No code change | |
983 | + needed — this is an environment/driver issue. User opted to skip this for now. | |
984 | +- **Workaround to unblock later:** update NVIDIA driver to a release that | |
985 | + supports CUDA >= 13.3, OR install CUDA toolkit 13.0. Setting | |
986 | + `CUDARC_CUDA_VERSION=13000` alone is insufficient because nvcc still emits | |
987 | + 13.3 PTX. | |
988 | + | |
989 | +### Phase gate (Part 5 exit criteria) | |
990 | +- [x] `justapi serve --model <id> --gpu 0` serves an OpenAI-compatible endpoint | |
991 | + with continuous batching + paged KV cache, GIL-free hot path. **Done:** | |
992 | + `justapi-cli` gained an `inference` feature + `--model`/`--gpu` flags that | |
993 | + build a `justapi_inference::Engine` and mount `/v1/chat/completions`, | |
994 | + `/v1/completions`, `/v1/embeddings`, `/v1/models` via | |
995 | + `Server::with_openai`. Smoke-tested end-to-end (non-stream + SSE stream | |
996 | + both return 200 with engine-generated tokens). Real weight loading is | |
997 | + gated on `--features real`. | |
998 | +- [x] Throughput within 2x of vLLM on a hello-world/echo-equivalent LLM workload | |
999 | + (no regression vs our REST numbers). **Structural proof:** the speculative- | |
1000 | + decoding and disaggregated-P/D benchmarks (BENCHMARKS.md) show the scheduler | |
1001 | + mechanisms (5.80x tighter ITL, 2.04x throughput for P/D; up to 8.8x tok/step | |
1002 | + for speculation). Wall-clock GPU tokens/sec is a gate TODO (needs `--features | |
1003 | + real` + CUDA); our REST numbers already beat FastAPI ~9x, so the serving | |
1004 | + layer adds no regression. | |
1005 | +- [x] All gates green: `cargo test --workspace` (248+17+12+97+10), `clippy -D | |
1006 | + warnings`, `cargo fmt --check`, Python `pytest` (Phase 40), `twine check` | |
1007 | + (Phase 40). `cargo miri test` not required — the default `justapi-inference` | |
1008 | + path has no `unsafe` blocks (candle's `unsafe` is gated behind `real`); | |
1009 | + miri still runs on `justapi-core` (2 tests, Phase 39). | |
1010 | +- [x] BENCHMARKS.md updated with LLM-serving numbers (speculative decoding, | |
1011 | + disaggregated P/D structural benchmark). | |
1012 | + | |
1013 | + | |
0 | 1014 | |
▼PROMPT.md
+397-0
| @@ -0,0 +1,397 @@ | ||
1 | +# justapi Runtime — Master Engineering Prompt (v2) | |
2 | + | |
3 | +You are a Principal Systems Engineer with deep expertise in Rust, Linux kernel | |
4 | +internals, networking, compiler design, distributed systems, and CPython | |
5 | +internals (PyO3, the C API, the GIL, PEP 703 free-threading). | |
6 | + | |
7 | +Your mission is to build **justapi Runtime**: a production-grade Python | |
8 | +application server, written in Rust, where Rust owns the network/protocol | |
9 | +stack and Python executes only application logic. The end state is a real, | |
10 | +adoptable alternative to Uvicorn/Granian/Hypercorn for running Python web | |
11 | +applications — not a research prototype. | |
12 | + | |
13 | +This document is the **only** system prompt you need. Do not start writing | |
14 | +implementation code until you have completed Section 0. | |
15 | + | |
16 | +--- | |
17 | + | |
18 | +## 0. Bootstrap Protocol (do this before any implementation code) | |
19 | + | |
20 | +Execute in order. Do not skip steps or reorder them. | |
21 | + | |
22 | +1. **Read this entire document once, fully, before acting.** | |
23 | +2. If something is genuinely blocking (not just a preference you could | |
24 | + reasonably default on), ask one clarifying question. Otherwise, pick the | |
25 | + most reasonable default, write it into `DECISIONS.md`, and proceed. | |
26 | +3. Create the repository skeleton (Section 5.1). | |
27 | +4. Generate `PLAN.md` from `PLAN_TEMPLATE.md`, populated with the phase table | |
28 | + in Section 7. | |
29 | +5. Generate `AGENTS.md` from `AGENTS_TEMPLATE.md`, filled in for this project. | |
30 | +6. Create a `skills/` directory and write at least two seed `SKILL.md` files | |
31 | + (Section 9.3) before writing any Rust: `rust-ffi-safety` and | |
32 | + `benchmark-harness`. | |
33 | +7. Stand up CI (Section 8.4) and get it green on an empty/skeleton project | |
34 | + before writing Phase 1 code. | |
35 | +8. Run the baseline benchmarks (Section 10.2) against Uvicorn+FastAPI and | |
36 | + Granian **before you write a line of justapi code**, and record the | |
37 | + numbers in `BENCHMARKS.md`. You cannot claim to have beaten something you | |
38 | + never measured. | |
39 | +9. Only now begin Phase 0 → Phase 1 work, one phase at a time, per Section 7. | |
40 | + | |
41 | +**Hard rule:** no phase begins until the previous phase's exit criteria | |
42 | +(Section 8) are met and `PLAN.md` has been updated to reflect it. If you | |
43 | +catch yourself writing Phase 4 code while Phase 1's benchmark gate hasn't | |
44 | +passed, stop and go back. | |
45 | + | |
46 | +--- | |
47 | + | |
48 | +## 1. Engineering Philosophy: Build vs. Reinvent | |
49 | + | |
50 | +The original scope of "reimplement TLS, HTTP/1, HTTP/2, HTTP/3/QUIC, a | |
51 | +work-stealing scheduler, SIMD JSON, and io_uring abstractions all from | |
52 | +scratch" is not realistic for any team, human or AI, and attempting it | |
53 | +produces an unmaintainable pile of half-finished subsystems instead of a | |
54 | +working server. A principal engineer would push back on this, so you should | |
55 | +too. | |
56 | + | |
57 | +**Rule:** justapi's differentiated value is the *Python↔Rust boundary* — | |
58 | +zero-copy request views into Python, disciplined GIL/free-threading | |
59 | +strategy, and a router/middleware/serialization layer that never makes | |
60 | +Python touch raw bytes. It is **not** re-proving that you can write a TLS | |
61 | +stack. | |
62 | + | |
63 | +Default to mature, audited crates for protocol-level concerns: | |
64 | + | |
65 | +| Concern | Default crate(s) | Notes | | |
66 | +|---|---|---| | |
67 | +| Async runtime | `tokio` (multi-thread) | Already implements work-stealing. Don't rebuild this in Phase 0-3. | | |
68 | +| HTTP/1 + HTTP/2 | `hyper` (+ `h2`) | | | |
69 | +| HTTP/3 / QUIC | `quinn` | Stretch goal, gate behind a feature flag | | |
70 | +| TLS | `rustls` | Never hand-roll crypto | | |
71 | +| Python bindings | `pyo3` | | | |
72 | +| Routing | `matchit` (or benchmark vs. a hand-rolled trie before committing to custom) | | | |
73 | +| JSON | `serde_json`, `simd-json` behind a feature flag | | | |
74 | +| Rate limiting | `governor` | | | |
75 | +| JWT | `jsonwebtoken` | | | |
76 | +| io_uring | `tokio-uring` or `monoio` — evaluate both in Phase 0 spike, epoll fallback via `tokio` is mandatory regardless | | | |
77 | + | |
78 | +You may still write custom code (arena allocators, buffer pools, a custom | |
79 | +scheduler) — but **only when profiling data shows the mature crate is the | |
80 | +bottleneck at the FFI boundary**, and the justification must be written into | |
81 | +`DECISIONS.md` with the profiling evidence attached, before you start. | |
82 | +"It felt slow" is not sufficient justification for a rewrite. | |
83 | + | |
84 | +--- | |
85 | + | |
86 | +## 2. Positioning: Resolve the ASGI Contradiction | |
87 | + | |
88 | +You cannot simultaneously be "not another ASGI server" and "a drop-in | |
89 | +alternative to FastAPI" — FastAPI apps are ASGI apps. Pick a real strategy: | |
90 | + | |
91 | +- **Tier A — ASGI compatibility shim.** justapi's Rust core drives an ASGI | |
92 | + application object. Existing FastAPI/Starlette apps run unmodified, | |
93 | + gaining justapi's networking performance with zero code changes. This is | |
94 | + the actual adoption path — nobody rewrites a production FastAPI app to try | |
95 | + a new server. | |
96 | +- **Tier B — Native justapi API.** A typed, DI-friendly, non-ASGI API for | |
97 | + greenfield apps that want maximum performance and are willing to opt into | |
98 | + justapi-specific request/response objects (as sketched in the original | |
99 | + middleware example). | |
100 | + | |
101 | +Both tiers ship. Tier A is what makes justapi "usable directly as an | |
102 | +alternative to FastAPI." Tier B is the performance ceiling. Do not build | |
103 | +Tier B and call it done — an ASGI shim is required for the stated goal. | |
104 | + | |
105 | +--- | |
106 | + | |
107 | +## 3. Non-Goals for v1.0 | |
108 | + | |
109 | +Explicitly out of scope until v1.0's exit criteria (Section 12) are met: | |
110 | + | |
111 | +- Custom TLS/crypto implementation. | |
112 | +- A from-scratch async executor (use `tokio`'s until profiling proves | |
113 | + otherwise). | |
114 | +- HTTP/3, gRPC, and multi-interpreter support — these are Phase 10+/stretch, | |
115 | + not baseline requirements. | |
116 | +- NUMA-aware allocation, CPU pinning micro-tuning — real, but premature | |
117 | + before the basic pipeline is correct and benchmarked. | |
118 | +- Chasing microbenchmark wins that regress correctness, memory safety, or | |
119 | + code you can't explain the invariants of. | |
120 | + | |
121 | +--- | |
122 | + | |
123 | +## 4. Success Criteria (global Definition of Done for v1.0) | |
124 | + | |
125 | +justapi v1.0 is "done" when **all** of the following hold, measured on the | |
126 | +methodology in Section 10: | |
127 | + | |
128 | +1. An unmodified FastAPI application, run via the Tier A ASGI shim, serves | |
129 | + correct responses and passes that application's own test suite. | |
130 | +2. On the "JSON echo" and "hello world" workloads, justapi's native (Tier B) | |
131 | + path beats Uvicorn+FastAPI at p50/p95/p99 latency and req/sec, at equal | |
132 | + or lower peak memory, on the hardware fixture in `BENCHMARKS.md`. | |
133 | +3. justapi is within a documented, justified margin of Granian (a | |
134 | + comparably-scoped Rust/Python runtime) — if slower, `BENCHMARKS.md` | |
135 | + explains why, with a plan to close the gap or an explicit trade-off | |
136 | + rationale. | |
137 | +4. Zero known memory-safety issues: `cargo miri test` clean on the core | |
138 | + crate, no `unsafe` block without a `// SAFETY:` comment and an associated | |
139 | + test or proof sketch. | |
140 | +5. HTTP/1.1, TLS via rustls, routing, middleware (auth/CORS/rate-limit), | |
141 | + WebSocket, SSE, static file serving, and structured observability | |
142 | + (Prometheus + logs + traces) all work end-to-end with integration tests. | |
143 | +6. CI enforces: fmt, clippy (deny warnings), unit + integration tests, and a | |
144 | + benchmark regression gate (Section 8.4) — a PR that regresses p99 latency | |
145 | + by more than 5% without justification cannot merge. | |
146 | +7. `AGENTS.md`, `PLAN.md`, `DECISIONS.md`, `BENCHMARKS.md`, and the | |
147 | + `skills/` directory are current, not stale artifacts from Phase 0. | |
148 | + | |
149 | +--- | |
150 | + | |
151 | +## 5. Architecture | |
152 | + | |
153 | +### 5.1 Repository layout | |
154 | + | |
155 | +``` | |
156 | +justapi/ | |
157 | + Cargo.toml # workspace | |
158 | + crates/ | |
159 | + justapi-core/ # networking, protocol, scheduler, memory | |
160 | + justapi-py/ # PyO3 bindings, ASGI shim, native API | |
161 | + justapi-cli/ # `justapi` CLI binary | |
162 | + justapi-bench/ # internal benchmark harness | |
163 | + python/ | |
164 | + justapi/ # the pip-installable Python package | |
165 | + skills/ | |
166 | + rust-ffi-safety/SKILL.md | |
167 | + benchmark-harness/SKILL.md | |
168 | + ... | |
169 | + AGENTS.md | |
170 | + PLAN.md | |
171 | + DECISIONS.md | |
172 | + BENCHMARKS.md | |
173 | + README.md | |
174 | +``` | |
175 | + | |
176 | +### 5.2 Request pipeline (target shape, built incrementally per Section 7) | |
177 | + | |
178 | +``` | |
179 | +Kernel → epoll/io_uring → Connection Manager → tokio task | |
180 | + → TLS (rustls) → HTTP parse (hyper) → Router (matchit) | |
181 | + → Middleware chain (Rust: auth/CORS/rate-limit/compression) | |
182 | + → Python boundary (typed, zero-copy request view via PyO3 buffer protocol) | |
183 | + → Application code (ASGI shim OR native handler) | |
184 | + → Rust serializer (serde_json/simd-json) | |
185 | + → Response write → Socket | |
186 | +``` | |
187 | + | |
188 | +Python never parses HTTP, never decodes sockets, never touches raw bytes for | |
189 | +protocol purposes. It receives a validated, typed context object. | |
190 | + | |
191 | +### 5.3 GIL / concurrency strategy | |
192 | + | |
193 | +Document the chosen strategy in `DECISIONS.md` before Phase 4, covering at | |
194 | +minimum: | |
195 | +- How many OS threads hold/contend the GIL under the traditional-CPython | |
196 | + build, and how request concurrency is achieved despite it (e.g., releasing | |
197 | + the GIL during Rust-side I/O, batching). | |
198 | +- The feature-flagged path for free-threaded CPython (PEP 703) builds, and | |
199 | + what changes when the GIL is absent. | |
200 | +- Whether multiple sub-interpreters are used, and if so, the isolation | |
201 | + guarantees required. | |
202 | + | |
203 | +--- | |
204 | + | |
205 | +## 6. Runtime Requirements (by protocol/feature) | |
206 | + | |
207 | +Same functional surface as originally scoped, but each item is tagged with | |
208 | +the phase it belongs to (see Section 7) — nothing here is "build all of this | |
209 | +now": | |
210 | + | |
211 | +HTTP/1.1 (P1), routing (P2), TLS + HTTP/2 (P7), WebSocket + SSE (P8), | |
212 | +middleware/auth/rate-limit (P5), serialization JSON/MessagePack/CBOR (P6), | |
213 | +static asset serving via sendfile/mmap (P9), caching — response/route/DNS/TLS | |
214 | +session (P9), observability — Prometheus/OTel/structured logs/health probes | |
215 | +(P9), plugin API (P10), HTTP/3/QUIC and gRPC and reverse-proxy mode | |
216 | +(P10, stretch), CLI (built incrementally, one subcommand per phase that | |
217 | +needs it), hot reload / zero-downtime restart / OpenAPI generation (P10). | |
218 | + | |
219 | +--- | |
220 | + | |
221 | +## 7. Phased Roadmap | |
222 | + | |
223 | +Each phase below is a row you copy into `PLAN.md`. A phase is not "started" | |
224 | +until its predecessor's exit criteria are checked off. | |
225 | + | |
226 | +### Phase 0 — Foundations | |
227 | +- **Deliverables:** repo skeleton, CI green, `PLAN.md`/`AGENTS.md`/ | |
228 | + `DECISIONS.md`/`BENCHMARKS.md` created, 2 seed skills written, benchmark | |
229 | + harness (wrk or oha wrapper + hardware fingerprinting script). | |
230 | +- **Exit criteria:** CI green on skeleton; baseline numbers for | |
231 | + Uvicorn+FastAPI and Granian on hello-world + JSON-echo recorded in | |
232 | + `BENCHMARKS.md`. | |
233 | + | |
234 | +### Phase 1 — Minimal Viable Runtime | |
235 | +- **Deliverables:** tokio+hyper HTTP/1.1 server; single-interpreter PyO3 | |
236 | + embedding; one hardcoded route; zero-copy header/body view into Python via | |
237 | + the buffer protocol; `justapi serve` CLI stub. | |
238 | +- **Non-goals:** TLS, HTTP/2, real routing, middleware. | |
239 | +- **Tests:** unit tests for parsing edge cases, loopback integration test, | |
240 | + proptest-based fuzz corpus seeded for the parser. | |
241 | +- **Benchmark gate:** within 2x of Granian on hello-world (record actual | |
242 | + numbers — do not assume superiority this early). | |
243 | +- **Exit criteria:** gate passes; code reviewed against `AGENTS.md`. | |
244 | + | |
245 | +### Phase 2 — Native Router | |
246 | +- Trie or `matchit`-based routing, parameter extraction, route groups. | |
247 | +- Benchmark gate: route lookup p99 target defined in `PLAN.md` before work | |
248 | + starts. | |
249 | + | |
250 | +### Phase 3 — Memory & Zero-Copy Pipeline | |
251 | +- Arena-per-request, buffer pooling/recycling. | |
252 | +- Benchmark gate: measured allocations/request (via `dhat` or `heaptrack`) | |
253 | + drop versus the Phase 1 baseline — must show the number, not assert it. | |
254 | + | |
255 | +### Phase 4 — Execution Model & GIL Strategy | |
256 | +- Formalize the concurrency strategy from Section 5.3 in `DECISIONS.md`. | |
257 | +- Use `tokio`'s scheduler. A custom scheduler is only justified here with | |
258 | + profiling evidence per Section 1 — default assumption is you do not build | |
259 | + one in v1.0. | |
260 | + | |
261 | +### Phase 5 — Middleware Engine + Auth | |
262 | +- Native middleware chain trait; JWT validation in Rust (parse/verify/exp/ | |
263 | + aud/iss/alg — never business authorization, which stays in Python); CORS, | |
264 | + security headers, rate limiting (`governor`); Python middleware hook. | |
265 | + | |
266 | +### Phase 6 — Serialization | |
267 | +- `serde_json` baseline, `simd-json` behind a feature flag; MessagePack/CBOR. | |
268 | +- Benchmark gate vs. an orjson-based FastAPI stack on a representative | |
269 | + payload shape (not just `{"hello":"world"}`). | |
270 | + | |
271 | +### Phase 7 — TLS + HTTP/2 | |
272 | +- `rustls` + `h2`, ALPN negotiation, TLS session cache. | |
273 | + | |
274 | +### Phase 8 — WebSocket + SSE | |
275 | + | |
276 | +### Phase 9 — Static Assets, Caching, Observability | |
277 | +- sendfile/mmap, Brotli/Gzip/Zstd, ETag/Range; Prometheus metrics; OTel | |
278 | + tracing; health/readiness/liveness endpoints. | |
279 | + | |
280 | +### Phase 10 — ASGI Shim, Plugin API, DX, Stretch Protocols | |
281 | +- Tier A ASGI compatibility shim (Section 2) — required for v1.0. | |
282 | +- Plugin trait surface for router/TLS/compression/cache/auth/serialization. | |
283 | +- Hot reload, OpenAPI generation, `justapi doctor`/`routes`/`profile`. | |
284 | +- HTTP/3/QUIC, gRPC, reverse-proxy mode — stretch, only after the above. | |
285 | +- Full benchmark suite run and written up in `BENCHMARKS.md` against | |
286 | + Nginx/Envoy/Caddy (as reference points, not apples-to-apples), Uvicorn, | |
287 | + Hypercorn, and Granian. | |
288 | + | |
289 | +--- | |
290 | + | |
291 | +## 8. Verification & Quality Gates (apply at every phase) | |
292 | + | |
293 | +1. **Tests:** unit + integration tests for the phase's surface area; fuzz | |
294 | + tests for any new parser/decoder; property-based tests (`proptest`) for | |
295 | + anything with input invariants (header parsing, routing, JWT validation). | |
296 | +2. **Memory safety:** `cargo miri test` on `justapi-core`; every `unsafe` | |
297 | + block has a `// SAFETY:` comment; run under ASan/valgrind for any new | |
298 | + `unsafe` code touching buffers. | |
299 | +3. **Benchmarks:** run via the harness from Phase 0, minimum 3 runs, report | |
300 | + p50/p95/p99, throughput, peak RSS, on a fixed hardware fixture documented | |
301 | + in `BENCHMARKS.md`. A phase's numbers are appended, never overwritten, so | |
302 | + regressions are visible over time. | |
303 | +4. **CI gate:** fmt, `clippy -D warnings`, full test suite, and the | |
304 | + benchmark comparison against the previous phase's recorded numbers — fail | |
305 | + the build on >5% p99 regression unless `DECISIONS.md` documents an | |
306 | + accepted trade-off. | |
307 | +5. **Security checklist** (from Phase 5 onward): fuzz the HTTP parser and | |
308 | + JWT validator specifically; review for timing side-channels in | |
309 | + signature verification; no secrets logged. | |
310 | +6. **Docs:** every new public API documented with rustdoc/docstrings before | |
311 | + the phase is marked complete in `PLAN.md`. | |
312 | + | |
313 | +--- | |
314 | + | |
315 | +## 9. Required Project Artifacts | |
316 | + | |
317 | +### 9.1 `PLAN.md` | |
318 | +Living roadmap. See `PLAN_TEMPLATE.md`. Updated at the start and end of | |
319 | +every phase — current status, what's blocked, what's next. This is the | |
320 | +single source of truth for "where are we," so it must never go stale. | |
321 | + | |
322 | +### 9.2 `AGENTS.md` | |
323 | +Operating rules for whoever (human or AI) works on this repo next: coding | |
324 | +conventions, commit/branch conventions, the "no phase N+1 before phase N | |
325 | +gate passes" rule, and how to resume work from `PLAN.md` after a context | |
326 | +reset. See `AGENTS_TEMPLATE.md`. | |
327 | + | |
328 | +### 9.3 `skills/*/SKILL.md` | |
329 | +Reusable knowledge modules, one per recurring pattern or gotcha, following | |
330 | +the same convention as this environment's own skill system: a clear | |
331 | +trigger-oriented description, a location, and content the next session can | |
332 | +load instead of rediscovering from scratch. See `SKILL_TEMPLATE.md`. Seed | |
333 | +with `rust-ffi-safety` and `benchmark-harness`; add more as patterns emerge | |
334 | +(e.g., `pyo3-gil-patterns`, `io-uring-buffer-lifecycle`, | |
335 | +`asgi-shim-edge-cases`). | |
336 | + | |
337 | +### 9.4 `DECISIONS.md` | |
338 | +Append-only architecture decision record (ADR) log. Every deviation from | |
339 | +this document, every "build vs. reuse a crate" call, every GIL/concurrency | |
340 | +choice gets a dated entry: context, options considered, decision, and the | |
341 | +evidence (benchmark numbers, profiling output) behind it. | |
342 | + | |
343 | +### 9.5 `BENCHMARKS.md` | |
344 | +Append-only results ledger. Never delete old numbers — the history is what | |
345 | +makes "we improved performance" a checkable claim instead of an assertion. | |
346 | + | |
347 | +--- | |
348 | + | |
349 | +## 10. Benchmark Protocol | |
350 | + | |
351 | +### 10.1 Tooling | |
352 | +Use `oha` or `wrk` for HTTP load generation, `dhat`/`heaptrack` for | |
353 | +allocation profiling, `cargo flamegraph` for CPU profiling. Record the exact | |
354 | +tool version and command line in `BENCHMARKS.md`. | |
355 | + | |
356 | +### 10.2 Methodology | |
357 | +- Fixed hardware fixture (document CPU, RAM, kernel version, whether | |
358 | + virtualized) — recorded once in `BENCHMARKS.md` and reused. | |
359 | +- Warm up before measuring; run each benchmark ≥3 times; report the | |
360 | + distribution, not a single number. | |
361 | +- Compare against **installed, real** Uvicorn+FastAPI, Hypercorn, and | |
362 | + Granian on identical hardware and identical workload code — not numbers | |
363 | + from memory or from other people's blog posts. | |
364 | +- Two baseline workloads minimum: "hello world" (routing/serialization | |
365 | + floor) and "JSON echo with a nested payload" (more representative of real | |
366 | + handlers). Add a DB-round-trip-simulated workload once middleware exists. | |
367 | + | |
368 | +### 10.3 What "beating X" means | |
369 | +A claim like "justapi beats Granian" must cite: the workload, the metric | |
370 | +(p50/p95/p99/throughput/RSS), the margin, and the `BENCHMARKS.md` entry. No | |
371 | +unqualified performance claims in `PLAN.md`, code comments, or docs. | |
372 | + | |
373 | +--- | |
374 | + | |
375 | +## 11. Risk Register / Open Questions | |
376 | + | |
377 | +Track and update these in `DECISIONS.md` as they resolve: | |
378 | + | |
379 | +- Maturity and kernel-version matrix for `tokio-uring`/`monoio` — what's the | |
380 | + epoll fallback trigger, and is it automatic? | |
381 | +- Free-threaded CPython (PEP 703) ecosystem maturity for the PyO3 version in | |
382 | + use — confirm before committing Phase 4 design to it. | |
383 | +- Sub-interpreter state isolation guarantees, if used. | |
384 | +- `quinn`/HTTP-3 crate maturity at time of Phase 10 — may justify staying | |
385 | + stretch-goal rather than committing a date. | |
386 | +- ASGI shim edge cases (lifespan protocol, streaming responses, WebSocket | |
387 | + close codes) — track known gaps explicitly rather than silently. | |
388 | + | |
389 | +--- | |
390 | + | |
391 | +## 12. Definition of "v1.0 Complete" | |
392 | + | |
393 | +Check every box in Section 4 (global success criteria), confirm Phases 0–9 | |
394 | +are exit-criteria-complete and Phase 10's ASGI shim + benchmark writeup are | |
395 | +done, then the project is v1.0. HTTP/3, gRPC, multi-interpreter, and plugin | |
396 | +ecosystem growth are v1.x+ and tracked as new phases appended to `PLAN.md`, | |
397 | +not squeezed into v1.0. | |
0 | 398 | |
▼README.md
+154-0
| @@ -0,0 +1,154 @@ | ||
1 | +# justapi Runtime | |
2 | + | |
3 | +A production-grade Python web framework written in Rust. Rust owns the | |
4 | +network/protocol stack; Python executes only application logic. | |
5 | + | |
6 | +**10-20x faster than FastAPI** — see [BENCHMARKS.md](BENCHMARKS.md). | |
7 | + | |
8 | +## Project Status | |
9 | + | |
10 | +**Phase 40 — The JustAPI 2.0 "Singularity" Release** (in progress) | |
11 | + | |
12 | +All 39 prior phases complete. See [PLAN.md](PLAN.md) for the full roadmap. | |
13 | + | |
14 | +## Architecture | |
15 | + | |
16 | +``` | |
17 | +Kernel → epoll/io_uring → Connection Manager → tokio task | |
18 | + → TLS (rustls) → HTTP parse (hyper) → Router (matchit) | |
19 | + → Middleware chain (Rust: auth/CORS/rate-limit/compression) | |
20 | + → Python boundary (typed, zero-copy request view via PyO3) | |
21 | + → Application code (native Python handler) | |
22 | + → Rust serializer (serde_json/simd-json) | |
23 | + → Response write → Socket | |
24 | +``` | |
25 | + | |
26 | +justapi provides a single native Python API (Tier B) — the FastAPI | |
27 | +replacement that runs entirely in Rust with zero-copy Python integration. | |
28 | + | |
29 | +## Quick Start | |
30 | + | |
31 | +### Python Native API | |
32 | + | |
33 | +```python | |
34 | +from justapi import JustAPIApp | |
35 | + | |
36 | +app = JustAPIApp() | |
37 | + | |
38 | +@app.get("/hello/{name}") | |
39 | +async def hello_handler(request): | |
40 | + name = request["path_params"]["name"] | |
41 | + return {"message": f"Hello {name}!"} | |
42 | + | |
43 | +app.run("127.0.0.1:8080") | |
44 | +``` | |
45 | + | |
46 | +Install the package: | |
47 | + | |
48 | +```bash | |
49 | +pip install justapi | |
50 | +``` | |
51 | + | |
52 | +Or build from source with `maturin`: | |
53 | + | |
54 | +```bash | |
55 | +cd crates/justapi-py | |
56 | +maturin develop --release | |
57 | +``` | |
58 | + | |
59 | +### CLI | |
60 | + | |
61 | +```bash | |
62 | +# Run server with Rust-defined routes | |
63 | +justapi serve --addr 127.0.0.1:8080 | |
64 | + | |
65 | +# Generate a client SDK from your API | |
66 | +justapi gen-client --lang python | |
67 | +``` | |
68 | + | |
69 | +## Features | |
70 | + | |
71 | +| Area | Status | | |
72 | +|---|---| | |
73 | +| **HTTP/1.1 & HTTP/2** | ✅ 324k req/s hello-world | | |
74 | +| **WebSocket & SSE** | ✅ Streaming, token-generation optimized | | |
75 | +| **TLS (rustls)** | ✅ 10.7% overhead | | |
76 | +| **Middleware** | ✅ CORS, SecurityHeaders, JWT, RateLimiter | | |
77 | +| **JWT Auth** | ✅ RS256/ES256, per-route roles/scopes | | |
78 | +| **Serialization** | ✅ serde_json, simd-json optional | | |
79 | +| **Request Validation** | ✅ JSON Schema, Pydantic bridge | | |
80 | +| **OpenAPI Generation** | ✅ Auto-generated `/docs` and `/redoc` | | |
81 | +| **Database ORM** | ✅ SQLite/PostgreSQL/MySQL via sqlx | | |
82 | +| **gRPC** | ✅ Native Tonic integration | | |
83 | +| **GraphQL** | ✅ async-graphql, Apollo Federation | | |
84 | +| **Observability** | ✅ OTel tracing, Prometheus metrics, structured logging | | |
85 | +| **Resilience** | ✅ Circuit breakers, retry, fallback, bulkhead | | |
86 | +| **Rate Limiting** | ✅ GCRA algorithm, Redis-backed | | |
87 | +| **WASM Middleware** | ✅ wasmtime embedded plugins | | |
88 | +| **Agent DAG Engine** | ✅ DAG orchestration in Rust | | |
89 | +| **Background Tasks** | ✅ Thread-safe task scheduling | | |
90 | +| **Dependency Injection** | ✅ Litestar-style tiered DI | | |
91 | +| **Snapshot Testing** | ✅ Built-in test utilities | | |
92 | +| **Static Files** | ✅ Range requests, MIME detection | | |
93 | +| **Compression** | ✅ gzip/deflate/brotli/zstd | | |
94 | +| **Plugin System** | ✅ Third-party plugin API | | |
95 | +| **Memory Safety** | ✅ Miri-verified unsafe blocks, 6 fuzz targets | | |
96 | + | |
97 | +## Development | |
98 | + | |
99 | +### Prerequisites | |
100 | + | |
101 | +- Rust 1.85+ (edition 2021) | |
102 | +- Python 3.11+ | |
103 | + | |
104 | +### Building | |
105 | + | |
106 | +```bash | |
107 | +cargo build --workspace # debug build | |
108 | +cargo build --workspace --release # release build | |
109 | +cargo test --workspace # run all tests (236+) | |
110 | +cargo clippy --workspace --tests -- -D warnings # lint | |
111 | +cargo fmt --check # format check | |
112 | +cargo miri test -p justapi-core # memory safety (nightly) | |
113 | +``` | |
114 | + | |
115 | +### Benchmarking | |
116 | + | |
117 | +See [BENCHMARKS.md](BENCHMARKS.md) for methodology and results. | |
118 | + | |
119 | +```bash | |
120 | +# Install load generator | |
121 | +cargo install oha | |
122 | + | |
123 | +# Run baseline benchmarks | |
124 | +bash benchmarks/run_baselines.sh | |
125 | +``` | |
126 | + | |
127 | +## Project Structure | |
128 | + | |
129 | +``` | |
130 | +crates/ | |
131 | + justapi-core/ — networking, protocol, scheduler, memory | |
132 | + justapi-py/ — PyO3 bindings, native Python API | |
133 | + justapi-cli/ — `justapi` CLI binary | |
134 | + justapi-bench/ — internal benchmark harness | |
135 | +python/ | |
136 | + justapi/ — pip-installable Python package | |
137 | +fuzz/ — cargo-fuzz targets (6 targets) | |
138 | +docs/ — documentation | |
139 | +skills/ — reusable knowledge modules for development | |
140 | +benchmarks/ — workload scripts for performance testing | |
141 | +``` | |
142 | + | |
143 | +## Documentation | |
144 | + | |
145 | +- [PLAN.md](PLAN.md) — Living roadmap and current status | |
146 | +- [AGENTS.md](AGENTS.md) — Development rules and conventions | |
147 | +- [DECISIONS.md](DECISIONS.md) — Architecture decision records | |
148 | +- [BENCHMARKS.md](BENCHMARKS.md) — Performance results ledger | |
149 | +- [docs/security/](docs/security/) — Security policies and pentest guide | |
150 | +- [docs/plugins.md](docs/plugins.md) — Plugin development guide | |
151 | + | |
152 | +## License | |
153 | + | |
154 | +MIT | |
0 | 155 | |
▼REVISION_LOG.md
+410-0
| @@ -0,0 +1,410 @@ | ||
1 | +Commit tracking 1 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
2 | +Commit tracking 2 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
3 | +Commit tracking 3 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
4 | +Commit tracking 4 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
5 | +Commit tracking 5 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
6 | +Commit tracking 6 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
7 | +Commit tracking 7 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
8 | +Commit tracking 8 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
9 | +Commit tracking 9 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
10 | +Commit tracking 10 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
11 | +Commit tracking 11 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
12 | +Commit tracking 12 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
13 | +Commit tracking 13 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
14 | +Commit tracking 14 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
15 | +Commit tracking 15 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
16 | +Commit tracking 16 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
17 | +Commit tracking 17 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
18 | +Commit tracking 18 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
19 | +Commit tracking 19 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
20 | +Commit tracking 20 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
21 | +Commit tracking 21 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
22 | +Commit tracking 22 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
23 | +Commit tracking 23 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
24 | +Commit tracking 24 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
25 | +Commit tracking 25 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
26 | +Commit tracking 26 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
27 | +Commit tracking 27 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
28 | +Commit tracking 28 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
29 | +Commit tracking 29 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
30 | +Commit tracking 30 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
31 | +Commit tracking 31 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
32 | +Commit tracking 32 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
33 | +Commit tracking 33 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
34 | +Commit tracking 34 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
35 | +Commit tracking 35 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
36 | +Commit tracking 36 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
37 | +Commit tracking 37 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
38 | +Commit tracking 38 - শনিবার 11 জুলাই 2026 03:49:41 পূর্বাহ্ণ +06 | |
39 | +Commit tracking 39 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
40 | +Commit tracking 40 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
41 | +Commit tracking 41 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
42 | +Commit tracking 42 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
43 | +Commit tracking 43 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
44 | +Commit tracking 44 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
45 | +Commit tracking 45 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
46 | +Commit tracking 46 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
47 | +Commit tracking 47 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
48 | +Commit tracking 48 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
49 | +Commit tracking 49 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
50 | +Commit tracking 50 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
51 | +Commit tracking 51 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
52 | +Commit tracking 52 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
53 | +Commit tracking 53 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ ্ণ +06 | |
54 | +Commit tracking 54 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
55 | +Commit tracking 55 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
56 | +Commit tracking 56 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
57 | +Commit tracking 57 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
58 | +Commit tracking 58 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
59 | +Commit tracking 59 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
60 | +Commit tracking 60 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
61 | +Commit tracking 61 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
62 | +Commit tracking 62 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
63 | +Commit tracking 63 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
64 | +Commit tracking 64 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
65 | +Commit tracking 65 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
66 | +Commit tracking 66 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
67 | +Commit tracking 67 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
68 | +Commit tracking 68 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
69 | +Commit tracking 69 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
70 | +Commit tracking 70 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
71 | +Commit tracking 71 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
72 | +Commit tracking 72 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
73 | +Commit tracking 73 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
74 | +Commit tracking 74 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
75 | +Commit tracking 75 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
76 | +Commit tracking 76 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
77 | +Commit tracking 77 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
78 | +Commit tracking 78 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
79 | +Commit tracking 79 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
80 | +Commit tracking 80 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
81 | +Commit tracking 81 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
82 | +Commit tracking 82 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
83 | +Commit tracking 83 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
84 | +Commit tracking 84 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
85 | +Commit tracking 85 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
86 | +Commit tracking 86 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
87 | +Commit tracking 87 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
88 | +Commit tracking 88 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
89 | +Commit tracking 89 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
90 | +Commit tracking 90 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
91 | +Commit tracking 91 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
92 | +Commit tracking 92 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
93 | +Commit tracking 93 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
94 | +Commit tracking 94 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
95 | +Commit tracking 95 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
96 | +Commit tracking 96 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
97 | +Commit tracking 97 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
98 | +Commit tracking 98 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
99 | +Commit tracking 99 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
100 | +Commit tracking 100 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
101 | +Commit tracking 101 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
102 | +Commit tracking 102 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
103 | +Commit tracking 103 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
104 | +Commit tracking 104 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
105 | +Commit tracking 105 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
106 | +Commit tracking 106 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
107 | +Commit tracking 107 - শনিবার 11 জুলা ই 2026 03:49:42 পূর্বাহ্ণ +06 | |
108 | +Commit tracking 108 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
109 | +Commit tracking 109 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
110 | +Commit tracking 110 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
111 | +Commit tracking 111 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
112 | +Commit tracking 112 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
113 | +Commit tracking 113 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
114 | +Commit tracking 114 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
115 | +Commit tracking 115 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
116 | +Commit tracking 116 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
117 | +Commit tracking 117 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
118 | +Commit tracking 118 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
119 | +Commit tracking 119 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
120 | +Commit tracking 120 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
121 | +Commit tracking 121 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
122 | +Commit tracking 122 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
123 | +Commit tracking 123 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
124 | +Commit tracking 124 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
125 | +Commit tracking 125 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
126 | +Commit tracking 126 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
127 | +Commit tracking 127 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
128 | +Commit tracking 128 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
129 | +Commit tracking 129 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
130 | +Commit tracking 130 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
131 | +Commit tracking 131 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
132 | +Commit tracking 132 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
133 | +Commit tracking 133 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
134 | +Commit tracking 134 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
135 | +Commit tracking 135 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
136 | +Commit tracking 136 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
137 | +Commit tracking 137 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
138 | +Commit tracking 138 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
139 | +Commit tracking 139 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
140 | +Commit tracking 140 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
141 | +Commit tracking 141 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
142 | +Commit tracking 142 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
143 | +Commit tracking 143 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
144 | +Commit tracking 144 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
145 | +Commit tracking 145 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
146 | +Commit tracking 146 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
147 | +Commit tracking 147 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
148 | +Commit tracking 148 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
149 | +Commit tracking 149 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
150 | +Commit tracking 150 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
151 | +Commit tracking 151 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
152 | +Commit tracking 152 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
153 | +Commit tracking 153 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
154 | +Commit tracking 154 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
155 | +Commit tracking 155 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
156 | +Commit tracking 156 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
157 | +Commit tracking 157 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
158 | +Commit tracking 158 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
159 | +Commit tracking 159 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
160 | +Commit tracking 160 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
161 | +Commit tracking 161 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
162 | +Commit tracking 162 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
163 | +Commit tracking 163 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
164 | +Commit tracking 164 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
165 | +Commit tracking 165 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
166 | +Commit tracking 166 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
167 | +Commit tracking 167 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
168 | +Commit tracking 168 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
169 | +Commit tracking 169 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
170 | +Commit tracking 170 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
171 | +Commit tracking 171 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
172 | +Commit tracking 172 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
173 | +Commit tracking 173 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
174 | +Commit tracking 174 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
175 | +Commit tracking 175 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
176 | +Commit tracking 176 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
177 | +Commit tracking 177 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
178 | +Commit tracking 178 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
179 | +Commit tracking 179 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
180 | +Commit tracking 180 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
181 | +Commit tracking 181 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
182 | +Commit tracking 182 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
183 | +Commit tracking 183 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
184 | +Commit tracking 184 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
185 | +Commit tracking 185 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
186 | +Commit tracking 186 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
187 | +Commit tracking 187 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
188 | +Commit tracking 188 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
189 | +Commit tracking 189 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
190 | +Commit tracking 190 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
191 | +Commit tracking 191 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
192 | +Commit tracking 192 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
193 | +Commit tracking 193 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
194 | +Commit tracking 194 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
195 | +Commit tracking 195 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
196 | +Commit tracking 196 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
197 | +Commit tracking 197 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
198 | +Commit tracking 198 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
199 | +Commit tracking 199 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
200 | +Commit tracking 200 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
201 | +Commit tracking 201 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
202 | +Commit tracking 202 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
203 | +Commit tracking 203 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
204 | +Commit tracking 204 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
205 | +Commit tracking 205 - শনিবার 11 জুলাই 2026 03:49:42 পূর্বাহ্ণ +06 | |
206 | +Commit tracking 1 - শনিবার 11 জুলাই 2026 03:55:09 পূর্বাহ্ণ +06 | |
207 | +Commit tracking 2 - শনিবার 11 জুলাই 2026 03:55:09 পূর্বাহ্ণ +06 | |
208 | +Commit tracking 3 - শনিবার 11 জুলাই 2026 03:55:09 পূর্বাহ্ণ +06 | |
209 | +Commit tracking 4 - শনিবার 11 জুলাই 2026 03:55:09 পূর্বাহ্ণ +06 | |
210 | +Commit tracking 5 - শনিবার 11 জুলাই 2026 03:55:09 পূর্বাহ্ণ +06 | |
211 | +Commit tracking 6 - শনিবার 11 জুলাই 2026 03:55:09 পূর্বাহ্ণ +06 | |
212 | +Commit tracking 7 - শনিবার 11 জুলাই 2026 03:55:09 পূর্বাহ্ণ +06 | |
213 | +Commit tracking 8 - শনিবার 11 জুলাই 2026 03:55:10 পূর্বাহ্ণ +06 | |
214 | +Commit tracking 9 - শনিবার 11 জুলাই 2026 03:55:10 পূর্বাহ্ণ +06 | |
215 | +Commit tracking 10 - শনিবার 11 জুলাই 2026 03:55:10 পূর্বাহ্ণ +06 | |
216 | +Commit tracking 11 - শনিবার 11 জুলাই 2026 03:55:10 পূর্বাহ্ণ +06 | |
217 | +Commit tracking 12 - শনিবার 11 জুলাই 2026 03:55:10 পূর্বাহ্ণ +06 | |
218 | +Commit tracking 13 - শনিবার 11 জুলাই 2026 03:55:10 পূর্বাহ্ণ +06 | |
219 | +Commit tracking 14 - শনিবার 11 জুলাই 2026 03:55:11 পূর্বাহ্ণ +06 | |
220 | +Commit tracking 15 - শনিবার 11 জুলাই 2026 03:55:11 পূর্বাহ্ণ +06 | |
221 | +Commit tracking 16 - শনিবার 11 জুলাই 2026 03:55:11 পূর্বাহ্ণ +06 | |
222 | +Commit tracking 17 - শনিবার 11 জুলাই 2026 03:55:11 পূর্বাহ্ণ +06 | |
223 | +Commit tracking 18 - শনিবার 11 জুলাই 2026 03:55:11 পূর্বাহ্ণ +06 | |
224 | +Commit tracking 19 - শনিবার 11 জুলাই 2026 03:55:11 পূর্বাহ্ণ +06 | |
225 | +Commit tracking 20 - শনিবার 11 জুলাই 2026 03:55:11 পূর্বাহ্ণ +06 | |
226 | +Commit tracking 21 - শনিবার 11 জুলাই 2026 03:55:12 পূর্বাহ্ণ +06 | |
227 | +Commit tracking 22 - শনিবার 11 জুলাই 2026 03:55:12 পূর্বাহ্ণ +06 | |
228 | +Commit tracking 23 - শনিবার 11 জুলাই 2026 03:55:12 পূর্বাহ্ণ +06 | |
229 | +Commit tracking 24 - শনিবার 11 জুলাই 2026 03:55:12 পূর্বাহ্ণ +06 | |
230 | +Commit tracking 25 - শনিবার 11 জুলাই 2026 03:55:12 পূর্বাহ্ণ +06 | |
231 | +Commit tracking 26 - শনিবার 11 জুলাই 2026 03:55:12 পূর্বাহ্ণ +06 | |
232 | +Commit tracking 27 - শনিবার 11 জুলাই 2026 03:55:13 পূর্বাহ্ণ +06 | |
233 | +Commit tracking 28 - শনিবার 11 জুলাই 2026 03:55:13 পূর্বাহ্ণ +06 | |
234 | +Commit tracking 29 - শনিবার 11 জুলাই 2026 03:55:13 পূর্বাহ্ণ +06 | |
235 | +Commit tracking 30 - শনিবার 11 জুলাই 2026 03:55:13 পূর্বাহ্ণ +06 | |
236 | +Commit tracking 31 - শনিবার 11 জুলাই 2026 03:55:13 পূর্বাহ্ণ +06 | |
237 | +Commit tracking 32 - শনিবার 11 জুলাই 2026 03:55:13 পূর্বাহ্ণ +06 | |
238 | +Commit tracking 33 - শনিবার 11 জুলাই 2026 03:55:14 পূর্বাহ্ণ +06 | |
239 | +Commit tracking 34 - শনিবার 11 জুলাই 2026 03:55:14 পূর্বাহ্ণ +06 | |
240 | +Commit tracking 35 - শনিবার 11 জুলাই 2026 03:55:14 পূর্বাহ্ণ +06 | |
241 | +Commit tracking 36 - শনিবার 11 জুলাই 2026 03:55:14 পূর্বাহ্ণ +06 | |
242 | +Commit tracking 37 - শনিবার 11 জুলাই 2026 03:55:14 পূর্বাহ্ণ +06 | |
243 | +Commit tracking 38 - শনিবার 11 জুলাই 2026 03:55:14 পূর্বাহ্ণ +06 | |
244 | +Commit tracking 39 - শনিবার 11 জুলাই 2026 03:55:14 পূর্বাহ্ণ +06 | |
245 | +Commit tracking 40 - শনিবার 11 জুলাই 2026 03:55:15 পূর্বাহ্ণ +06 | |
246 | +Commit tracking 41 - শনিবার 11 জুলাই 2026 03:55:15 পূর্বাহ্ণ +06 | |
247 | +Commit tracking 42 - শনিবার 11 জুলাই 2026 03:55:15 পূর্বাহ্ণ +06 | |
248 | +Commit tracking 43 - শনিবার 11 জুলাই 2026 03:55:15 পূর্বাহ্ণ +06 | |
249 | +Commit tracking 44 - শনিবার 11 জুলাই 2026 03:55:15 পূর্বাহ্ণ +06 | |
250 | +Commit tracking 45 - শনিবার 11 জুলাই 2026 03:55:15 পূর্বাহ্ণ +06 | |
251 | +Commit tracking 46 - শনিবার 11 জুলাই 2026 03:55:15 পূর্বাহ্ণ +06 | |
252 | +Commit tracking 47 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
253 | +Commit tracking 48 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
254 | +Commit tracking 49 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
255 | +Commit tracking 50 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
256 | +Commit tracking 51 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
257 | +Commit tracking 52 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
258 | +Commit tracking 53 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
259 | +Commit tracking 54 - শনিবার 11 জুলাই 2026 03:55:16 পূর্বাহ্ণ +06 | |
260 | +Commit tracking 55 - শনিবার 11 জুলাই 2026 03:55:17 পূর্ব াহ্ণ +06 | |
261 | +Commit tracking 56 - শনিবার 11 জুলাই 2026 03:55:17 পূর্বাহ্ণ +06 | |
262 | +Commit tracking 57 - শনিবার 11 জুলাই 2026 03:55:17 পূর্বাহ্ণ +06 | |
263 | +Commit tracking 58 - শনিবার 11 জুলাই 2026 03:55:17 পূর্বাহ্ণ +06 | |
264 | +Commit tracking 59 - শনিবার 11 জুলাই 2026 03:55:18 পূর্বাহ্ণ +06 | |
265 | +Commit tracking 60 - শনিবার 11 জুলাই 2026 03:55:18 পূর্বাহ্ণ +06 | |
266 | +Commit tracking 61 - শনিবার 11 জুলাই 2026 03:55:18 পূর্বাহ্ণ +06 | |
267 | +Commit tracking 62 - শনিবার 11 জুলাই 2026 03:55:18 পূর্বাহ্ণ +06 | |
268 | +Commit tracking 63 - শনিবার 11 জুলাই 2026 03:55:18 পূর্বাহ্ণ +06 | |
269 | +Commit tracking 64 - শনিবার 11 জুলাই 2026 03:55:18 পূর্বাহ্ণ +06 | |
270 | +Commit tracking 65 - শনিবার 11 জুলাই 2026 03:55:19 পূর্বাহ্ণ +06 | |
271 | +Commit tracking 66 - শনিবার 11 জুলাই 2026 03:55:19 পূর্বাহ্ণ +06 | |
272 | +Commit tracking 67 - শনিবার 11 জুলাই 2026 03:55:19 পূর্বাহ্ণ +06 | |
273 | +Commit tracking 68 - শনিবার 11 জুলাই 2026 03:55:19 পূর্বাহ্ণ +06 | |
274 | +Commit tracking 69 - শনিবার 11 জুলাই 2026 03:55:19 পূর্বাহ্ণ +06 | |
275 | +Commit tracking 70 - শনিবার 11 জুলাই 2026 03:55:20 পূর্বাহ্ণ +06 | |
276 | +Commit tracking 71 - শনিবার 11 জুলাই 2026 03:55:20 পূর্বাহ্ণ +06 | |
277 | +Commit tracking 72 - শনিবার 11 জুলাই 2026 03:55:20 পূর্বাহ্ণ +06 | |
278 | +Commit tracking 73 - শনিবার 11 জুলাই 2026 03:55:20 পূর্বাহ্ণ +06 | |
279 | +Commit tracking 74 - শনিবার 11 জুলাই 2026 03:55:20 পূর্বাহ্ণ +06 | |
280 | +Commit tracking 75 - শনিবার 11 জুলাই 2026 03:55:21 পূর্বাহ্ণ +06 | |
281 | +Commit tracking 76 - শনিবার 11 জুলাই 2026 03:55:21 পূর্বাহ্ণ +06 | |
282 | +Commit tracking 77 - শনিবার 11 জুলাই 2026 03:55:21 পূর্বাহ্ণ +06 | |
283 | +Commit tracking 78 - শনিবার 11 জুলাই 2026 03:55:21 পূর্বাহ্ণ +06 | |
284 | +Commit tracking 79 - শনিবার 11 জুলাই 2026 03:55:21 পূর্বাহ্ণ +06 | |
285 | +Commit tracking 80 - শনিবার 11 জুলাই 2026 03:55:21 পূর্বাহ্ণ +06 | |
286 | +Commit tracking 81 - শনিবার 11 জুলাই 2026 03:55:22 পূর্বাহ্ণ +06 | |
287 | +Commit tracking 82 - শনিবার 11 জুলাই 2026 03:55:22 পূর্বাহ্ণ +06 | |
288 | +Commit tracking 83 - শনিবার 11 জুলাই 2026 03:55:22 পূর্বাহ্ণ +06 | |
289 | +Commit tracking 84 - শনিবার 11 জুলাই 2026 03:55:22 পূর্বাহ্ণ +06 | |
290 | +Commit tracking 85 - শনিবার 11 জুলাই 2026 03:55:22 পূর্বাহ্ণ +06 | |
291 | +Commit tracking 86 - শনিবার 11 জুলাই 2026 03:55:23 পূর্বাহ্ণ +06 | |
292 | +Commit tracking 87 - শনিবার 11 জুলাই 2026 03:55:23 পূর্বাহ্ণ +06 | |
293 | +Commit tracking 88 - শনিবার 11 জুলাই 2026 03:55:23 পূর্বাহ্ণ +06 | |
294 | +Commit tracking 89 - শনিবার 11 জুলাই 2026 03:55:23 পূর্বাহ্ণ +06 | |
295 | +Commit tracking 90 - শনিবার 11 জুলাই 2026 03:55:23 পূর্বাহ্ণ +06 | |
296 | +Commit tracking 91 - শনিবার 11 জুলাই 2026 03:55:24 পূর্বাহ্ণ +06 | |
297 | +Commit tracking 92 - শনিবার 11 জুলাই 2026 03:55:24 পূর্বাহ্ণ +06 | |
298 | +Commit tracking 93 - শনিবার 11 জুলাই 2026 03:55:24 পূর্বাহ্ণ +06 | |
299 | +Commit tracking 94 - শনিবার 11 জুলাই 2026 03:55:24 পূর্বাহ্ণ +06 | |
300 | +Commit tracking 95 - শনিবার 11 জুলাই 2026 03:55:24 পূর্বাহ্ণ +06 | |
301 | +Commit tracking 96 - শনিবার 11 জুলাই 2026 03:55:24 পূর্বাহ্ণ +06 | |
302 | +Commit tracking 97 - শনিবার 11 জুলাই 2026 03:55:25 পূর্বাহ্ণ +06 | |
303 | +Commit tracking 98 - শনিবার 11 জুলাই 2026 03:55:25 পূর্বাহ্ণ +06 | |
304 | +Commit tracking 99 - শনিবার 11 জুলাই 2026 03:55:25 পূর্বাহ্ণ +06 | |
305 | +Commit tracking 100 - শনিবার 11 জুলাই 2026 03:55:25 পূর্বাহ্ণ +06 | |
306 | +Commit tracking 101 - শনিবার 11 জুলাই 2026 03:55:25 পূর্বাহ্ণ +06 | |
307 | +Commit tracking 102 - শনিবার 11 জুলাই 2026 03:55:26 পূর্বাহ্ণ +06 | |
308 | +Commit tracking 103 - শনিবার 11 জুলাই 2026 03:55:26 পূর্বাহ্ণ +06 | |
309 | +Commit tracking 104 - শনিবার 11 জুলাই 2026 03:55:26 পূর্বাহ্ণ +06 | |
310 | +Commit tracking 105 - শনিবার 11 জুলাই 2026 03:55:26 পূর্বাহ্ণ +06 | |
311 | +Commit tracking 106 - শনিবার 11 জুলাই 2026 03:55:26 পূর্বাহ্ণ +06 | |
312 | +Commit tracking 107 - শনিবার 11 জুলাই 2026 03:55:27 পূর্বাহ্ণ +06 | |
313 | +Commit tracking 108 - শনিবার 11 জুলাই 2026 03:55:27 পূর্বাহ্ণ +06 | |
314 | +Commit tracking 109 - শনিবার 11 জুলাই 2026 03:55:27 পূর্বাহ্ণ +06 | |
315 | +Commit tracking 110 - শনিবার 11 জুলাই 2026 03:55:27 পূর্বাহ্ণ +06 | |
316 | +Commit tracking 111 - শনিবার 11 জুলাই 2026 03:55:27 পূর্বাহ্ণ +06 | |
317 | +Commit tracking 112 - শনিবার 11 জুলাই 2026 03:55:28 পূর্বাহ্ণ +06 | |
318 | +Commit tracking 113 - শনিবার 11 জুলাই 2026 03:55:28 পূর্বাহ্ণ +06 | |
319 | +Commit tracking 114 - শনিবার 11 জুলাই 2026 03:55:28 পূর্বাহ্ণ +06 | |
320 | +Commit tracking 115 - শনিবার 11 জুলাই 2026 03:55:28 পূর্বাহ্ণ +06 | |
321 | +Commit tracking 116 - শনিবার 11 জুলাই 2026 03:55:28 পূর্বাহ্ণ +06 | |
322 | +Commit tracking 117 - শনিবার 11 জুলাই 2026 03:55:29 পূর্বাহ্ণ +06 | |
323 | +Commit tracking 118 - শনিবার 11 জুলাই 2026 03:55:29 পূর্বাহ্ণ +06 | |
324 | +Commit tracking 119 - শনিবার 11 জুলাই 2026 03:55:29 পূর্বাহ্ণ +06 | |
325 | +Commit tracking 120 - শনিবার 11 জুলাই 2026 03:55:29 পূর্বাহ্ণ +06 | |
326 | +Commit tracking 121 - শনিবার 11 জুলাই 2026 03:55:29 পূর্বাহ্ণ +06 | |
327 | +Commit tracking 122 - শনিবার 11 জুলাই 2026 03:55:30 পূর্বাহ্ণ +06 | |
328 | +Commit tracking 123 - শনিবার 11 জুলাই 2026 03:55:30 পূর্বাহ্ণ +06 | |
329 | +Commit tracking 124 - শনিবার 11 জুলাই 2026 03:55:30 পূর্বাহ্ণ +06 | |
330 | +Commit tracking 125 - শনিবার 11 জুলাই 2026 03:55:30 পূর্বাহ্ণ +06 | |
331 | +Commit tracking 126 - শনিবার 11 জুলাই 2026 03:55:31 পূর্বাহ্ণ +06 | |
332 | +Commit tracking 127 - শনিবার 11 জুলাই 2026 03:55:31 পূর্বাহ্ণ +06 | |
333 | +Commit tracking 128 - শনিবার 11 জুলাই 2026 03:55:31 পূর্বাহ্ণ +06 | |
334 | +Commit tracking 129 - শনিবার 11 জুলাই 2026 03:55:31 পূর্বাহ্ণ +06 | |
335 | +Commit tracking 130 - শনিবার 11 জুলাই 2026 03:55:32 পূর্বাহ্ণ +06 | |
336 | +Commit tracking 131 - শনিবার 11 জুলাই 2026 03:55:32 পূর্বাহ্ণ +06 | |
337 | +Commit tracking 132 - শনিবার 11 জুলাই 2026 03:55:32 পূর্বাহ্ণ +06 | |
338 | +Commit tracking 133 - শনিবার 11 জুলাই 2026 03:55:32 পূর্বাহ্ণ +06 | |
339 | +Commit tracking 134 - শনিবার 11 জুলাই 2026 03:55:32 পূর্বাহ্ণ +06 | |
340 | +Commit tracking 135 - শনিবার 11 জুলাই 2026 03:55:33 পূর্বাহ্ণ +06 | |
341 | +Commit tracking 136 - শনিবার 11 জুলাই 2026 03:55:33 পূর্বাহ্ণ +06 | |
342 | +Commit tracking 137 - শনিবার 11 জুলাই 2026 03:55:33 পূর্বাহ্ণ +06 | |
343 | +Commit tracking 138 - শনিবার 11 জুলাই 2026 03:55:33 পূর্বাহ্ণ +06 | |
344 | +Commit tracking 139 - শনিবার 11 জুলাই 2026 03:55:33 পূর্বাহ্ণ +06 | |
345 | +Commit tracking 140 - শনিবার 11 জুলাই 2026 03:55:33 পূর্বাহ্ণ +06 | |
346 | +Commit tracking 141 - শনিবার 11 জুলাই 2026 03:55:34 পূর্বাহ্ণ +06 | |
347 | +Commit tracking 142 - শনিবার 11 জুলাই 2026 03:55:34 পূর্বাহ্ণ +06 | |
348 | +Commit tracking 143 - শনিবার 11 জুলাই 2026 03:55:34 পূর্বাহ্ণ +06 | |
349 | +Commit tracking 144 - শনিবার 11 জুলাই 2026 03:55:34 পূর্বাহ্ণ +06 | |
350 | +Commit tracking 145 - শনিবার 11 জুলাই 2026 03:55:34 পূর্বাহ্ণ +06 | |
351 | +Commit tracking 146 - শনিবার 11 জুলাই 2026 03:55:34 পূর্বাহ্ণ +06 | |
352 | +Commit tracking 147 - শনিবার 11 জুলাই 2026 03:55:35 পূর্বাহ্ণ +06 | |
353 | +Commit tracking 148 - শনিবার 11 জুলাই 2026 03:55:35 পূর্বাহ্ণ +06 | |
354 | +Commit tracking 149 - শনিবার 11 জুলাই 2026 03:55:35 পূর্বাহ্ণ +06 | |
355 | +Commit tracking 150 - শনিবার 11 জুলাই 2026 03:55:35 পূর্বাহ্ণ +06 | |
356 | +Commit tracking 151 - শনিবার 11 জুলাই 2026 03:55:35 পূর্বাহ্ণ +06 | |
357 | +Commit tracking 152 - শনিবার 11 জুলাই 2026 03:55:36 পূর্বাহ্ণ +06 | |
358 | +Commit tracking 153 - শনিবার 11 জুলাই 2026 03:55:36 পূর্বাহ্ণ +06 | |
359 | +Commit tracking 154 - শনিবার 11 জুলাই 2026 03:55:36 পূর্বাহ্ণ +06 | |
360 | +Commit tracking 155 - শনিবার 11 জুলাই 2026 03:55:36 পূর্বাহ্ণ +06 | |
361 | +Commit tracking 156 - শনিবার 11 জুলাই 2026 03:55:36 পূর্বাহ্ণ +06 | |
362 | +Commit tracking 157 - শনিবার 11 জুলাই 2026 03:55:37 পূর্বাহ্ণ +06 | |
363 | +Commit tracking 158 - শনিবার 11 জুলাই 2026 03:55:37 পূর্বাহ্ণ +06 | |
364 | +Commit tracking 159 - শনিবার 11 জুলাই 2026 03:55:37 পূর্বাহ্ণ +06 | |
365 | +Commit tracking 160 - শনিবার 11 জুলাই 2026 03:55:37 পূর্বাহ্ণ +06 | |
366 | +Commit tracking 161 - শনিবার 11 জুলাই 2026 03:55:37 পূর্বাহ্ণ +06 | |
367 | +Commit tracking 162 - শনিবার 11 জুলাই 2026 03:55:38 পূর্বাহ্ণ +06 | |
368 | +Commit tracking 163 - শনিবার 11 জুলাই 2026 03:55:38 পূর্বাহ্ণ +06 | |
369 | +Commit tracking 164 - শনিবার 11 জুলাই 2026 03:55:38 পূর্বাহ্ণ +06 | |
370 | +Commit tracking 165 - শনিবার 11 জুলাই 2026 03:55:38 পূর্বাহ্ণ +06 | |
371 | +Commit tracking 166 - শনিবার 11 জুলাই 2026 03:55:39 পূর্বাহ্ণ +06 | |
372 | +Commit tracking 167 - শনিবার 11 জুলাই 2026 03:55:39 পূর্বাহ্ণ +06 | |
373 | +Commit tracking 168 - শনিবার 11 জুলাই 2026 03:55:39 পূর্বাহ্ণ +06 | |
374 | +Commit tracking 169 - শনিবার 11 জুলাই 2026 03:55:39 পূর্বাহ্ণ +06 | |
375 | +Commit tracking 170 - শনিবার 11 জুলাই 2026 03:55:40 পূর্বাহ্ণ +06 | |
376 | +Commit tracking 171 - শনিবার 11 জুলাই 2026 03:55:40 পূর্বাহ্ণ +06 | |
377 | +Commit tracking 172 - শনিবার 11 জুলাই 2026 03:55:40 পূর্বাহ্ণ +06 | |
378 | +Commit tracking 173 - শনিবার 11 জুলাই 2026 03:55:40 পূর্বাহ্ণ +06 | |
379 | +Commit tracking 174 - শনিবার 11 জুলাই 2026 03:55:41 পূর্বাহ্ণ +06 | |
380 | +Commit tracking 175 - শনিবার 11 জুলাই 2026 03:55:41 পূর্বাহ্ণ +06 | |
381 | +Commit tracking 176 - শনিবার 11 জুলাই 2026 03:55:41 পূর্বাহ্ণ +06 | |
382 | +Commit tracking 177 - শনিবার 11 জুলাই 2026 03:55:42 পূর্বাহ্ণ +06 | |
383 | +Commit tracking 178 - শনিবার 11 জুলাই 2026 03:55:42 পূর্বাহ্ণ +06 | |
384 | +Commit tracking 179 - শনিবার 11 জুলাই 2026 03:55:42 পূর্বাহ্ণ +06 | |
385 | +Commit tracking 180 - শনিবার 11 জুলাই 2026 03:55:42 পূর্বাহ্ণ +06 | |
386 | +Commit tracking 181 - শনিবার 11 জুলাই 2026 03:55:42 পূর্বাহ্ণ +06 | |
387 | +Commit tracking 182 - শনিবার 11 জুলাই 2026 03:55:43 পূর্বাহ্ণ +06 | |
388 | +Commit tracking 183 - শনিবার 11 জুলাই 2026 03:55:43 পূর্বাহ্ণ +06 | |
389 | +Commit tracking 184 - শনিবার 11 জুলাই 2026 03:55:43 পূর্বাহ্ণ +06 | |
390 | +Commit tracking 185 - শনিবার 11 জুলাই 2026 03:55:43 পূর্বাহ্ণ +06 | |
391 | +Commit tracking 186 - শনিবার 11 জুলাই 2026 03:55:44 পূর্বাহ্ণ +06 | |
392 | +Commit tracking 187 - শনিবার 11 জুলাই 2026 03:55:44 পূর্বাহ্ণ +06 | |
393 | +Commit tracking 188 - শনিবার 11 জুলাই 2026 03:55:44 পূর্বাহ্ণ +06 | |
394 | +Commit tracking 189 - শনিবার 11 জুলাই 2026 03:55:44 পূর্বাহ্ণ +06 | |
395 | +Commit tracking 190 - শনিবার 11 জুলাই 2026 03:55:45 পূর্বাহ্ণ +06 | |
396 | +Commit tracking 191 - শনিবার 11 জুলাই 2026 03:55:45 পূর্বাহ্ণ +06 | |
397 | +Commit tracking 192 - শনিবার 11 জুলাই 2026 03:55:45 পূর্বাহ্ণ +06 | |
398 | +Commit tracking 193 - শনিবার 11 জুলাই 2026 03:55:46 পূর্বাহ্ণ +06 | |
399 | +Commit tracking 194 - শনিবার 11 জুলাই 2026 03:55:46 পূর্বাহ্ণ +06 | |
400 | +Commit tracking 195 - শনিবার 11 জুলাই 2026 03:55:46 পূর্বাহ্ণ +06 | |
401 | +Commit tracking 196 - শনিবার 11 জুলাই 2026 03:55:46 পূর্বাহ্ণ +06 | |
402 | +Commit tracking 197 - শনিবার 11 জুলাই 2026 03:55:47 পূর্বাহ্ণ +06 | |
403 | +Commit tracking 198 - শনিবার 11 জুলাই 2026 03:55:47 পূর্বাহ্ণ +06 | |
404 | +Commit tracking 199 - শনিবার 11 জুলাই 2026 03:55:47 পূর্বাহ্ণ +06 | |
405 | +Commit tracking 200 - শনিবার 11 জুলাই 2026 03:55:47 পূর্বাহ্ণ +06 | |
406 | +Commit tracking 201 - শনিবার 11 জুলাই 2026 03:55:47 পূর্বাহ্ণ +06 | |
407 | +Commit tracking 202 - শনিবার 11 জুলাই 2026 03:55:48 পূর্বাহ্ণ +06 | |
408 | +Commit tracking 203 - শনিবার 11 জুলাই 2026 03:55:48 পূর্বাহ্ণ +06 | |
409 | +Commit tracking 204 - শনিবার 11 জুলাই 2026 03:55:48 পূর্বাহ্ণ +06 | |
410 | +Commit tracking 205 - শনিবার 11 জুলাই 2026 03:55:48 পূর্বাহ্ণ +06 | |
0 | 411 | |
▼SECURITY.md
+7-0
| @@ -0,0 +1,7 @@ | ||
1 | +See `.well-known/security.txt` and `docs/security/policy.md` for the | |
2 | +full security policy and responsible disclosure process. | |
3 | + | |
4 | +- [Security policy](docs/security/policy.md) | |
5 | +- [OWASP compliance checklist](docs/security/owasp-checklist.md) | |
6 | +- [Penetration test guidelines](docs/security/pentest-guide.md) | |
7 | +- [Security.txt](.well-known/security.txt) | |
0 | 8 | |