Skip to main content

logicaffeine_system/http/
engine.rs

1//! Engine selection + the pre-serialized static-response table — the
2//! cross-platform half of the HTTP fast path.
3//!
4//! LOGOS serves HTTP everywhere; the ENGINE is a knob. Two engines exist:
5//!
6//! - **tokio** — the portable baseline in [`super::server`]. Always compiled,
7//!   works on every native platform, and is the only engine for the
8//!   interpreter tier (whose `!Send` handler drives `Listener`/`Conn` inline).
9//! - **uring** — the Linux thread-per-core io_uring engine in
10//!   `engine_uring` (behind the `io-uring` feature). The compiled
11//!   (AOT) tier's fast path: its handlers are plain `Fn`s, exactly the
12//!   engine's contract.
13//!
14//! [`choose_engine`] is the ONE selection law, applied by the serve entries in
15//! `server.rs`: `LOGOS_HTTP_ENGINE=uring|tokio` overrides; the default is
16//! uring wherever a probe ring actually initializes (Linux, `io-uring` feature
17//! compiled in, kernel support present), else tokio. Selection is ALWAYS
18//! announced with one stderr line — loud, never silent — so a deploy can never
19//! quietly run the slow path.
20
21use std::collections::HashMap;
22
23use super::wire::{serialize_response_conn, HttpRequest, HttpResponse};
24
25/// Which engine a serve entry runs on.
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum EngineKind {
28    /// The Linux thread-per-core io_uring engine.
29    Uring,
30    /// The portable tokio accept loop.
31    Tokio,
32}
33
34/// The outcome of [`choose_engine`]: the engine plus the human-readable reason
35/// that goes on stderr.
36#[derive(Debug, Clone)]
37pub struct EngineChoice {
38    pub kind: EngineKind,
39    pub reason: String,
40}
41
42/// Read the selection knobs from the environment and apply
43/// [`choose_engine_from`]. `LOGOS_HTTP_FORCE_FALLBACK=1` makes the uring probe
44/// fail artificially (the fallback-honesty test hook — it exercises the same
45/// path a ring-init failure takes on a real kernel without io_uring).
46pub fn choose_engine() -> EngineChoice {
47    let requested = std::env::var("LOGOS_HTTP_ENGINE").ok();
48    let force_fallback = std::env::var("LOGOS_HTTP_FORCE_FALLBACK")
49        .map(|v| v.trim() == "1")
50        .unwrap_or(false);
51    let requested_lifecycle =
52        lifecycle_knob_requested(|k| std::env::var(k).ok().filter(|v| !v.trim().is_empty()));
53    match requested_lifecycle {
54        // A lifecycle guarantee was ASKED for by name. The uring engine calls its
55        // handler inline on the shard thread, so it cannot enforce a wall-clock
56        // request budget, answer a probe path, or shed by admission — and silently
57        // serving without the guarantee someone configured is the one outcome worse
58        // than being slower. Fall back, and say which name did it.
59        Some(knob) => EngineChoice {
60            kind: EngineKind::Tokio,
61            reason: format!(
62                "{knob} configures a lifecycle guarantee the io_uring engine cannot enforce \
63                 (its handler runs inline on the shard thread); serving on tokio so the \
64                 guarantee holds"
65            ),
66        },
67        None => choose_engine_from(requested.as_deref(), force_fallback, probe_uring),
68    }
69}
70
71/// The name of the first lifecycle knob the environment sets, if any — the pure
72/// half of the rule above, so it can be tested without the process environment.
73///
74/// Only knobs the uring engine cannot honor are listed. Body and read-deadline
75/// caps ARE enforced there (they are properties of parsing, not of scheduling),
76/// so configuring those does not cost you the fast engine.
77pub fn lifecycle_knob_requested(get: impl Fn(&str) -> Option<String>) -> Option<&'static str> {
78    const UNSUPPORTED: [&str; 5] = [
79        "LOGOS_REQUEST_TIMEOUT_MS",
80        "LOGOS_MAX_INFLIGHT",
81        "LOGOS_READY_PATH",
82        "LOGOS_HEALTH_PATH",
83        "LOGOS_CORRELATION_ID",
84    ];
85    UNSUPPORTED.into_iter().find(|k| get(k).is_some())
86}
87
88/// The pure selection law. `probe` initializes a throwaway ring (or explains
89/// why it cannot); it runs only when uring is actually a candidate.
90pub fn choose_engine_from(
91    requested: Option<&str>,
92    force_fallback: bool,
93    probe: impl FnOnce() -> Result<(), String>,
94) -> EngineChoice {
95    let requested = requested.map(|r| r.trim().to_ascii_lowercase());
96    match requested.as_deref() {
97        Some("tokio") => EngineChoice {
98            kind: EngineKind::Tokio,
99            reason: "LOGOS_HTTP_ENGINE=tokio".to_string(),
100        },
101        Some("uring") => try_uring("LOGOS_HTTP_ENGINE=uring", force_fallback, probe),
102        Some(other) => EngineChoice {
103            kind: EngineKind::Tokio,
104            reason: format!(
105                "LOGOS_HTTP_ENGINE={other:?} is not recognized (expected `uring` or `tokio`)"
106            ),
107        },
108        None => try_uring("default", force_fallback, probe),
109    }
110}
111
112fn try_uring(
113    origin: &str,
114    force_fallback: bool,
115    probe: impl FnOnce() -> Result<(), String>,
116) -> EngineChoice {
117    if force_fallback {
118        return EngineChoice {
119            kind: EngineKind::Tokio,
120            reason: format!("{origin}, but LOGOS_HTTP_FORCE_FALLBACK=1 forced the tokio fallback"),
121        };
122    }
123    match probe() {
124        Ok(()) => EngineChoice {
125            kind: EngineKind::Uring,
126            reason: format!("{origin}, io_uring probe ring initialized"),
127        },
128        Err(why) => EngineChoice {
129            kind: EngineKind::Tokio,
130            reason: format!("{origin}, but {why}"),
131        },
132    }
133}
134
135/// The real probe: can a ring be set up on this box, with this build?
136#[cfg(all(target_os = "linux", feature = "io-uring"))]
137fn probe_uring() -> Result<(), String> {
138    super::engine_uring::probe().map_err(|e| format!("io_uring probe failed: {e}"))
139}
140
141/// The engine is not compiled in (non-Linux target, or the `io-uring` feature
142/// is off) — tokio serves, everywhere, with the reason on stderr.
143#[cfg(not(all(target_os = "linux", feature = "io-uring")))]
144fn probe_uring() -> Result<(), String> {
145    Err(
146        "the io_uring engine is not compiled in (requires Linux and the `io-uring` feature)"
147            .to_string(),
148    )
149}
150
151/// The one loud line: which engine this serve entry runs on, and why.
152pub fn announce(entry: &str, choice: &EngineChoice) {
153    let kind = match choice.kind {
154        EngineKind::Uring => "uring",
155        EngineKind::Tokio => "tokio",
156    };
157    eprintln!("logos-http: {entry}: engine={kind} ({})", choice.reason);
158}
159
160/// Pre-serialized static responses — the memcpy fast path.
161///
162/// A table from request path to the FULLY rendered wire bytes of a response,
163/// in both connection dispositions. A serve entry carrying a table answers a
164/// matching request by copying bytes straight into the socket, before (and
165/// instead of) dispatching the handler.
166///
167/// Both variants are rendered by the same [`serialize_response_conn`] the
168/// handler path uses, so table bytes are byte-identical to what the handler
169/// path would produce for the same [`HttpResponse`] — the invariant the
170/// engine differential asserts.
171///
172/// Matching is deliberately conservative: only `GET` requests with an empty
173/// query string hit the table (anything else — `POST`, a `?query`, an unknown
174/// path — falls through to the handler).
175#[derive(Debug, Default, Clone)]
176pub struct StaticResponses {
177    entries: HashMap<String, StaticEntry>,
178}
179
180#[derive(Debug, Clone)]
181struct StaticEntry {
182    keep_alive: Vec<u8>,
183    close: Vec<u8>,
184}
185
186impl StaticResponses {
187    pub fn new() -> Self {
188        Self::default()
189    }
190
191    /// Render `resp` into both wire variants and file them under `path`.
192    pub fn insert(&mut self, path: &str, resp: &HttpResponse) {
193        self.entries.insert(
194            path.to_string(),
195            StaticEntry {
196                keep_alive: serialize_response_conn(resp, true),
197                close: serialize_response_conn(resp, false),
198            },
199        );
200    }
201
202    /// The pre-rendered wire frame for `req` under the given disposition, if
203    /// the table answers it.
204    pub fn wire_frame(&self, req: &HttpRequest, keep_alive: bool) -> Option<&[u8]> {
205        if req.method != "GET" || !req.query.is_empty() {
206            return None;
207        }
208        self.entries.get(&req.path).map(|e| {
209            if keep_alive {
210                e.keep_alive.as_slice()
211            } else {
212                e.close.as_slice()
213            }
214        })
215    }
216
217    pub fn is_empty(&self) -> bool {
218        self.entries.is_empty()
219    }
220
221    pub fn len(&self) -> usize {
222        self.entries.len()
223    }
224}
225
226/// The shard-count law for the uring engine: `LOGOS_HTTP_SHARDS` when it
227/// parses to a positive count (clamped to 1024), else the box's available
228/// parallelism, else 1. Pure so it is testable without touching the process
229/// environment.
230pub fn shard_count_from(env_shards: Option<&str>, available_parallelism: usize) -> usize {
231    env_shards
232        .and_then(|v| v.trim().parse::<usize>().ok())
233        .filter(|&n| n >= 1)
234        .map(|n| n.min(1024))
235        .unwrap_or_else(|| available_parallelism.max(1))
236}
237
238#[cfg(test)]
239#[path = "../../../logicaffeine_tests/unit/logicaffeine_system/http/engine/tests.rs"]
240mod tests;