Skip to main content

aimdb_core/session/
server.rs

1//! The reactive **server** engine of the session substrate.
2//!
3//! - [`run_session`] drives one accepted [`Connection`]: a biased `select_biased!`
4//!   loop interleaving inbound requests (RPC + subscribe + write) with outbound
5//!   subscription events.
6//! - [`serve`] is the accept loop over a [`Listener`], honoring
7//!   [`SessionLimits::max_connections`].
8//!
9//! Spawn-free (every per-connection/-subscription task lives in a
10//! [`FuturesUnordered`] the runner drives) and runtime-neutral (purely reactive,
11//! so no timer and no `tokio`/`embassy-*`).
12//!
13//! The loops use an **extract-then-act** shape: `select_biased!` only computes a
14//! small [`Step`], then the loop acts on it once the arm futures (and their
15//! borrows of `conn`/`subs`) drop — `futures`' macro, unlike `tokio::select!`,
16//! keeps non-selected futures alive across the handler.
17
18use alloc::boxed::Box;
19use alloc::string::{String, ToString};
20use alloc::sync::Arc;
21use alloc::vec::Vec;
22
23use async_channel::Sender;
24use futures_channel::oneshot;
25use futures_util::stream::{FuturesUnordered, StreamExt};
26use futures_util::{select_biased, FutureExt};
27use hashbrown::HashMap;
28
29use super::{
30    BoxFut, BoxStream, Connection, Dispatch, EnvelopeCodec, Inbound, Listener, Outbound, Payload,
31    RpcError, SessionLimits, SubUpdate,
32};
33
34/// Per-session engine knobs.
35#[derive(Debug, Clone, Default)]
36pub struct SessionConfig {
37    /// Connection cap (consulted by [`serve`]) and per-connection subscription
38    /// cap (by [`run_session`]).
39    pub limits: SessionLimits,
40    /// If `true`, read one frame before authenticating and pass it to
41    /// [`Dispatch::authenticate`] as an in-band Hello. If `false` (default),
42    /// authenticate from [`PeerInfo`](super::PeerInfo) alone.
43    pub reads_hello: bool,
44    /// If `true`, emit an explicit [`Outbound::Subscribed`] ack before the first
45    /// event. If `false` (default) the ack is implicit (events carry the
46    /// subscription id back).
47    pub acks_subscribe: bool,
48}
49
50/// Bound for the per-connection event funnel: pending outbound updates a
51/// connection may buffer before pumps start dropping.
52const EVENT_BUFFER: usize = 256;
53
54/// One subscription update on its way back to the connection's send half.
55struct SubEvent {
56    sub: String,
57    seq: u64,
58    /// Concrete record topic, when the subscription tags its updates
59    /// (see [`SubUpdate::topic`]).
60    topic: Option<Arc<str>>,
61    data: Payload,
62}
63
64/// What [`run_session`]'s `select_biased!` decided this iteration — extracted so
65/// the work runs after the arm futures' borrows of `conn`/`subs` release.
66enum Step {
67    /// A logical frame arrived from the peer (decode + dispatch).
68    Frame(Vec<u8>),
69    /// Peer closed or the transport errored — end the session.
70    Closed,
71    /// A subscription update to encode and forward to the peer.
72    Event(SubEvent),
73    /// A subscription pump finished on its own (stream exhausted) — reap it and
74    /// prune its `cancels` entry by the carried sub id.
75    SubDrained(String),
76}
77
78/// Drive one accepted [`Connection`] until it closes.
79///
80/// Authenticates once, then interleaves (biased toward inbound reads, so a chatty
81/// subscription cannot starve the RPC path) incoming requests, outgoing
82/// subscription events, and reaping of finished subscription pumps. Dropping the
83/// engine cancels every live subscription.
84pub async fn run_session<C, D>(
85    mut conn: Box<dyn Connection>,
86    codec: &C,
87    dispatch: &D,
88    config: &SessionConfig,
89) where
90    C: EnvelopeCodec + ?Sized,
91    D: Dispatch + ?Sized,
92{
93    // Resolve the session context (Hello-frame or peer-only — see `reads_hello`).
94    let first = if config.reads_hello {
95        match conn.recv().await {
96            Ok(Some(frame)) => Some(frame),
97            // Peer closed or errored before sending the Hello — nothing to serve.
98            _ => return,
99        }
100    } else {
101        None
102    };
103    let ctx = match dispatch.authenticate(conn.peer(), first.as_deref()).await {
104        Ok(ctx) => ctx,
105        Err(_e) => {
106            log_warn!("session authenticate rejected: {:?}", _e);
107            return;
108        }
109    };
110
111    // Open the per-connection session once; the loop threads `&mut` into it.
112    let mut session = dispatch.open(&ctx);
113
114    // Event funnel: every per-subscription pump sends updates here; the main loop
115    // is the sole writer to the connection. Bounded, so a slow client cannot grow
116    // it without limit — pumps drop on overflow (events carry a monotonic `seq`).
117    let (event_tx, event_rx) = async_channel::bounded::<SubEvent>(EVENT_BUFFER);
118    // Per-connection subscription pumps; the engine future is their sole owner.
119    // Each resolves to its own sub id, so the loop can prune the matching
120    // `cancels` entry (a no-op if Unsubscribe already removed it).
121    let mut subs: FuturesUnordered<BoxFut<'static, String>> = FuturesUnordered::new();
122    // sub-id → cancel handle; dropping/firing the oneshot cancels the pump.
123    let mut cancels: HashMap<String, oneshot::Sender<()>> = HashMap::new();
124    // Reused encode scratch buffer.
125    let mut out = Vec::new();
126
127    loop {
128        // Biased toward inbound reads. The select only decides the next step; the
129        // work happens after, in the `match`, once the arm borrows release.
130        let step = {
131            // Per-iteration futures, fused for the select. `event_rx.recv()` is
132            // `!Unpin`, so pin it; `subs` (a `FusedStream`) parks on the empty set
133            // while the always-active `recv` arm keeps the select alive.
134            let mut recv = conn.recv().fuse();
135            let mut event = core::pin::pin!(event_rx.recv().fuse());
136            select_biased! {
137                // ---- inbound: one logical frame from the peer --------------
138                r = recv => match r {
139                    Ok(Some(frame)) => Step::Frame(frame),
140                    Ok(None) | Err(_) => Step::Closed, // peer closed / transport error
141                },
142                // ---- outbound: a subscription update to forward ------------
143                ev = event => match ev {
144                    Ok(ev) => Step::Event(ev),
145                    // Funnel closed — only if every sender dropped, which can't
146                    // happen while the loop holds `event_tx`, so this is
147                    // unreachable; end the session defensively if it ever does.
148                    Err(_) => Step::Closed,
149                },
150                // ---- drain finished subscription pumps ---------------------
151                sub_id = subs.select_next_some() => Step::SubDrained(sub_id),
152            }
153        };
154
155        match step {
156            Step::Closed => break,
157            // A pump finished on its own (stream exhausted), not via Unsubscribe;
158            // drop its cancel handle so the ended subscription neither leaks nor
159            // keeps counting against `max_subs_per_connection`.
160            Step::SubDrained(sub_id) => {
161                cancels.remove(&sub_id);
162            }
163
164            Step::Event(ev) => {
165                out.clear();
166                let encoded = codec
167                    .encode(
168                        Outbound::Event {
169                            sub: &ev.sub,
170                            seq: ev.seq,
171                            topic: ev.topic.as_deref(),
172                            data: ev.data,
173                        },
174                        &mut out,
175                    )
176                    .is_ok();
177                if encoded && conn.send(&out).await.is_err() {
178                    break;
179                }
180            }
181
182            Step::Frame(frame) => {
183                let msg = match codec.decode(&frame) {
184                    Ok(msg) => msg,
185                    Err(_e) => continue, // skip a malformed frame, keep the session
186                };
187                match msg {
188                    Inbound::Request { id, method, params } => {
189                        let result = session.call(&method, params).await;
190                        out.clear();
191                        if codec
192                            .encode(Outbound::Reply { id, result }, &mut out)
193                            .is_err()
194                        {
195                            continue;
196                        }
197                        if conn.send(&out).await.is_err() {
198                            break;
199                        }
200                    }
201                    Inbound::Subscribe { id, topic } => {
202                        // The opening request id is the subscription's routing key;
203                        // events carry it back as `Outbound::Event.sub`.
204                        let sub_id = id.to_string();
205                        if cancels.len() >= config.limits.max_subs_per_connection {
206                            send_reply_err(&mut conn, codec, &mut out, id, RpcError::Denied).await;
207                            continue;
208                        }
209                        match session.subscribe(&topic).await {
210                            Ok(stream) => {
211                                // Optional explicit ack (see `acks_subscribe`).
212                                if config.acks_subscribe {
213                                    out.clear();
214                                    if codec
215                                        .encode(Outbound::Subscribed { sub: &sub_id }, &mut out)
216                                        .is_ok()
217                                        && conn.send(&out).await.is_err()
218                                    {
219                                        break;
220                                    }
221                                }
222                                // Optional late-join snapshots (one per covered
223                                // record), before the first event. They share
224                                // the subscription's `seq` space — numbered
225                                // `1..=N` here, events continue at `N + 1` — so
226                                // every snapshot is accounted for end-to-end:
227                                // one dropped by a slow client sink (or skipped
228                                // by the encode failure below) leaves a hole the
229                                // client reports as `skipped`, and a loss at the
230                                // tail of the burst surfaces on the first event.
231                                let snaps = session.snapshots(&topic);
232                                let total = snaps.len();
233                                let mut seq: u64 = 0;
234                                let mut send_failed = false;
235                                for (i, (snap_topic, data)) in snaps.into_iter().enumerate() {
236                                    seq += 1;
237                                    out.clear();
238                                    let encoded = codec.encode(
239                                        Outbound::Snapshot {
240                                            sub: &sub_id,
241                                            seq,
242                                            // The client reserves a sink slot
243                                            // for this one, so the burst's
244                                            // loss total always lands.
245                                            last: i + 1 == total,
246                                            topic: &snap_topic,
247                                            data,
248                                        },
249                                        &mut out,
250                                    );
251                                    if let Err(_e) = encoded {
252                                        // A record whose serialized bytes aren't
253                                        // valid for this envelope — broken for
254                                        // every read of it, not just here, so say
255                                        // so rather than dropping it in silence.
256                                        // `seq` already advanced, so the client
257                                        // still counts it as loss; if this was the
258                                        // burst's last snapshot the `last` flag
259                                        // goes with it and no `snapshot_end`
260                                        // reaches the subscriber.
261                                        log_warn!(
262                                            "snapshot encode failed, skipping: sub={} topic={} err={:?}",
263                                            sub_id,
264                                            snap_topic,
265                                            _e
266                                        );
267                                        continue;
268                                    }
269                                    if conn.send(&out).await.is_err() {
270                                        send_failed = true;
271                                        break;
272                                    }
273                                }
274                                if send_failed {
275                                    break;
276                                }
277                                let (cancel_tx, cancel_rx) = oneshot::channel();
278                                cancels.insert(sub_id.clone(), cancel_tx);
279                                subs.push(Box::pin(pump_subscription(
280                                    sub_id,
281                                    stream,
282                                    event_tx.clone(),
283                                    cancel_rx,
284                                    seq,
285                                )));
286                            }
287                            Err(e) => {
288                                send_reply_err(&mut conn, codec, &mut out, id, e).await;
289                            }
290                        }
291                    }
292                    Inbound::Unsubscribe { sub } => {
293                        // Dropping the sender resolves the pump's cancel future.
294                        cancels.remove(&sub);
295                    }
296                    Inbound::Write { topic, payload } => {
297                        // Fire-and-forget; single-writer-per-key stays intact.
298                        let _ = session.write(&topic, payload).await;
299                    }
300                    Inbound::Ping => {
301                        out.clear();
302                        if codec.encode(Outbound::Pong, &mut out).is_ok()
303                            && conn.send(&out).await.is_err()
304                        {
305                            break;
306                        }
307                    }
308                }
309            }
310        }
311    }
312
313    // Dropping `subs` here cancels every live subscription pump.
314    drop(subs);
315}
316
317/// Encode + send a `Reply` carrying an [`RpcError`]; best-effort (a send/encode
318/// failure just ends this attempt — the caller's loop handles a dead connection
319/// on its next `send`).
320async fn send_reply_err<C: EnvelopeCodec + ?Sized>(
321    conn: &mut Box<dyn Connection>,
322    codec: &C,
323    out: &mut Vec<u8>,
324    id: u64,
325    err: RpcError,
326) {
327    out.clear();
328    if codec
329        .encode(
330            Outbound::Reply {
331                id,
332                result: Err(err),
333            },
334            out,
335        )
336        .is_ok()
337    {
338        let _ = conn.send(out).await;
339    }
340}
341
342/// Pump one `Session::subscribe` stream into the connection's event funnel,
343/// tagging each update with a monotonic `seq`. Ends when the stream finishes or
344/// the cancel handle is dropped/fired (Unsubscribe or connection teardown).
345///
346/// `start_seq` is the last sequence number the late-join snapshot burst used
347/// (`0` when there were none), so events continue the *same* counter and a
348/// snapshot lost at the tail of the burst still shows up as a gap on the first
349/// event.
350///
351/// Returns its `sub_id` so [`run_session`] can prune the `cancels` entry for a
352/// pump that ended on its own; the Unsubscribe path already removed it, so that
353/// later prune is a no-op.
354async fn pump_subscription(
355    sub_id: String,
356    mut stream: BoxStream<'static, SubUpdate>,
357    tx: Sender<SubEvent>,
358    cancel: oneshot::Receiver<()>,
359    start_seq: u64,
360) -> String {
361    // Fuse the cancel receiver: a bare `oneshot::Receiver` reports
362    // `is_terminated()` once its sender drops, and `select_biased!` skips
363    // terminated arms — so the cancel would never fire. `Fuse` keeps the arm
364    // polled until it actually resolves.
365    let mut cancel = cancel.fuse();
366    let mut seq: u64 = start_seq;
367    loop {
368        // Independent arms, so a direct `select_biased!` is fine here.
369        let update = select_biased! {
370            // Resolves on explicit Unsubscribe (send) or on sender drop.
371            _ = cancel => break,
372            // `BoxStream` is not `FusedStream`, so fuse the per-iteration `next`.
373            next = stream.next().fuse() => match next {
374                Some(update) => update,
375                None => break, // stream exhausted
376            },
377        };
378        seq += update.skipped + 1;
379        // Non-blocking: drop on a full funnel (slow-client protection); only a
380        // disconnected funnel ends the pump.
381        match tx.try_send(SubEvent {
382            sub: sub_id.clone(),
383            seq,
384            topic: update.topic,
385            data: update.data,
386        }) {
387            Ok(()) => {}
388            Err(e) if e.is_full() => {} // drop on overflow
389            Err(_) => break,            // funnel disconnected — connection gone
390        }
391    }
392    sub_id
393}
394
395/// Accept connections from `listener` and serve each with [`run_session`],
396/// bounded by [`SessionLimits::max_connections`]. The accept loop and all
397/// per-connection futures share one [`FuturesUnordered`] — spawn-free.
398pub async fn serve<L, C, D>(mut listener: L, codec: Arc<C>, dispatch: Arc<D>, config: SessionConfig)
399where
400    L: Listener,
401    C: EnvelopeCodec + 'static,
402    // `?Sized` so a caller can serve an `Arc<dyn Dispatch>`.
403    D: Dispatch + 'static + ?Sized,
404{
405    let mut conns: FuturesUnordered<BoxFut<'static, ()>> = FuturesUnordered::new();
406
407    loop {
408        // Extract the accept result first, then act (keeps the `listener` and
409        // `conns` borrows apart).
410        let accept = {
411            let mut accept = listener.accept().fuse();
412            select_biased! {
413                a = accept => a,
414                // Parks on the empty set, so the accept arm keeps the select alive.
415                () = conns.select_next_some() => continue,
416            }
417        };
418
419        match accept {
420            Ok(conn) => {
421                // Soft cap; `len()` counts finished-but-not-yet-reaped futures, so
422                // under an accept flood (the biased `accept` arm starves the reap
423                // arm) it may read high transiently — acceptable for a soft cap.
424                if conns.len() >= config.limits.max_connections {
425                    log_warn!(
426                        "max_connections={} reached, refusing client",
427                        config.limits.max_connections
428                    );
429                    drop(conn);
430                    continue;
431                }
432                let codec = codec.clone();
433                let dispatch = dispatch.clone();
434                let cfg = config.clone();
435                conns.push(Box::pin(async move {
436                    run_session(conn, codec.as_ref(), dispatch.as_ref(), &cfg).await;
437                }));
438            }
439            Err(_e) => {
440                log_error!("accept failed: {:?}", _e);
441                // Keep serving existing connections despite a transient accept error.
442            }
443        }
444    }
445}