Skip to main content

liminal_server/server/
embedded.rs

1//! The embedding handle: a running liminal server with no listener, whose one
2//! connection-granting surface is the in-process loopback
3//! (design `docs/design/IN-PROCESS-TRANSPORT.md` §2, §4, §9 ruling 4).
4//!
5//! **What an [`EmbeddedServer`] secures is the RECORD PATH** (hardened
6//! face-substrate draft r2 §5). A connection minted here is admitted by the
7//! same door a socket connection is admitted by: the same
8//! `try_reserve_admission` against the same slot pool, a real durable
9//! connection incarnation from the same authority, the same registry record,
10//! the same `Connect`/`ConnectAck` handshake with the same constant-time token
11//! compare, the same frame preflight, the same participant gate, and the same
12//! `apply_frame` seam. No append reaches the record except through that door,
13//! and the door does not know which mount knocked. An embedded caller with the
14//! wrong token is refused on its own loopback, and an embedded caller arriving
15//! at capacity is refused exactly as a socket connect is.
16//!
17//! **What an [`EmbeddedServer`] does NOT secure is the mount.** A co-resident
18//! caller is TRUSTED CODE: it reaches the host process's heap, its descriptors,
19//! and its store handle without ever calling this type, so the record vouches
20//! for a co-resident mount only as far as the host process itself is trusted.
21//! That is inherent to the mount, not a defect of it. Every append admitted
22//! here carries the mount fact the admitting door stamped
23//! ([`MountKind::Loopback`](crate::server::mount::MountKind::Loopback), §10)
24//! precisely because the mount is what a consumer must weigh; this type is not
25//! a sandbox and must never be read as one.
26//!
27//! **The surface is deliberately one door wide.** This handle exposes no
28//! supervisor, no services, no store, no handler, no registry, and no scheduler
29//! — the module privacy that keeps those unreachable is the structural half of
30//! the no-side-door guarantee, and a convenience accessor here would undo it as
31//! surely as a public spawn seam would. `connect_loopback` is the whole grant.
32
33use std::fmt;
34use std::sync::Arc;
35
36use crate::ServerError;
37use crate::config::types::LimitsConfig;
38use crate::server::connection::{
39    ConnectionServices, ConnectionSupervisor, LoopbackClientEnd, LoopbackDuplex,
40};
41
42/// Bytes each direction of an embedded connection's duplex may hold.
43///
44/// 256 KiB, chosen to sit in the same order as the kernel socket buffers the
45/// loopback replaces: a default `SO_SNDBUF`/`SO_RCVBUF` pair on Linux and macOS
46/// is tens to a couple of hundred kilobytes, so an embedded writer meets
47/// backpressure at roughly the point a socket writer meets it and the mount
48/// does not quietly buy itself a deeper queue than every other mount has. It is
49/// a BOUND, not a reservation — each ring is a `VecDeque` that grows toward
50/// this ceiling only under load, so an idle embedded connection costs two empty
51/// queues.
52///
53/// The value is per ring rather than shared, so a backed-up inbound direction
54/// cannot starve the server's replies out of the outbound one.
55const LOOPBACK_RING_CAPACITY_BYTES: usize = 256 * 1024;
56
57/// A running liminal server with no listener, granting in-process connections.
58///
59/// Built from the same ingredients the production stack is built from —
60/// services, an optional connection auth token, and the operational limits —
61/// and torn down on drop, so an embedded server's lifetime is its handle's.
62pub struct EmbeddedServer {
63    supervisor: ConnectionSupervisor,
64}
65
66impl fmt::Debug for EmbeddedServer {
67    /// Prints nothing about the server.
68    ///
69    /// A `Debug` that rendered the supervisor would be an accessor by another
70    /// name: it would put the runtime's contents, the admission counter, and
71    /// the registry into any log line that formatted this handle.
72    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
73        formatter
74            .debug_struct("EmbeddedServer")
75            .finish_non_exhaustive()
76    }
77}
78
79impl EmbeddedServer {
80    /// Starts an embedded server over `services`, open-access and at the
81    /// default limits.
82    ///
83    /// # Errors
84    /// Returns [`ServerError`] when incarnation startup or scheduler startup
85    /// fails.
86    pub fn with_services(services: Arc<dyn ConnectionServices>) -> Result<Self, ServerError> {
87        Self::with_services_auth_and_limits(services, None, LimitsConfig::default())
88    }
89
90    /// Starts an embedded server over `services`, gated by `auth_token` when
91    /// one is configured, under `limits`.
92    ///
93    /// The three arguments are exactly the three
94    /// [`ConnectionSupervisor::with_services_auth_and_limits`] takes, because
95    /// an embedded server is a production stack with the listener removed and
96    /// nothing else: `None` for the token is the open-access server an absent
97    /// `[auth]` section produces, and `limits` carries the same
98    /// `max_connections` bound that both connection admission and the durable
99    /// incarnation stream enforce.
100    ///
101    /// # Errors
102    /// Returns [`ServerError`] when incarnation startup or scheduler startup
103    /// fails.
104    pub fn with_services_auth_and_limits(
105        services: Arc<dyn ConnectionServices>,
106        auth_token: Option<Vec<u8>>,
107        limits: LimitsConfig,
108    ) -> Result<Self, ServerError> {
109        Ok(Self {
110            supervisor: ConnectionSupervisor::with_services_auth_and_limits(
111                services, auth_token, limits,
112            )?,
113        })
114    }
115
116    /// Admits one in-process connection and returns the caller's end of it.
117    ///
118    /// This is the whole grant. It replaces exactly the listener's `accept()` +
119    /// `spawn_connection` pair and nothing else about admission: the returned
120    /// end is a byte stream that has not yet handshaken, so the caller still
121    /// sends `Connect` and still receives `ConnectAck` or `ConnectError` from
122    /// the same `connect_response` a socket client reaches.
123    ///
124    /// Dropping the returned end tears the connection down by the same
125    /// end-of-file a socket hangup produces, releasing its admission slot.
126    ///
127    /// # Errors
128    /// Returns [`ServerError::ConnectionLimitReached`] when the server is at
129    /// its `max_connections` bound — the identical typed refusal a socket
130    /// connect receives at capacity, surfaced as an error rather than a panic —
131    /// and other [`ServerError`] values when incarnation allocation or process
132    /// spawn fails. On every refusal the duplex is dropped whole, so no half of
133    /// a rejected connection survives.
134    pub fn connect_loopback(&self) -> Result<LoopbackClientEnd, ServerError> {
135        let (client, server) = LoopbackDuplex::bounded(LOOPBACK_RING_CAPACITY_BYTES);
136        // The handle is deliberately discarded: it is a pid plus an incarnation,
137        // and handing it back would be a second surface onto the connection the
138        // supervisor now owns. The registry record is what keeps the connection
139        // addressable, and the client end is what keeps it alive.
140        self.supervisor.spawn_loopback_connection(server)?;
141        Ok(client)
142    }
143}
144
145impl Drop for EmbeddedServer {
146    /// Stops the connection scheduler, mirroring the in-tree socket fixtures'
147    /// teardown (`SdkSocketFixture::stop`): every host record is removed while
148    /// the readiness owner is still live, then the scheduler is shut down.
149    ///
150    /// Teardown is `Drop` alone rather than a `shutdown` method plus `Drop`
151    /// because Rust drop points are already deterministic — a caller that wants
152    /// the server stopped at a particular moment drops it at that moment — and
153    /// a second spelling of the same act is a second surface to keep honest.
154    fn drop(&mut self) {
155        self.supervisor.shutdown();
156    }
157}