Skip to main content

aimdb_core/session/
pump.rs

1//! Data-plane pump helpers.
2//!
3//! Two free functions that own the boilerplate a data-plane connector used to
4//! hand-roll. The author writes only the pure I/O adapter — a
5//! [`Connector`](crate::transport::Connector) (outbound) and a [`Source`]
6//! (inbound) — and composes the helpers in `build()`
7//! (illustrative — `sink()`/`subscription()` are the author's own constructors):
8//!
9//! ```rust,ignore
10//! let mut f = pump_sink(db, "redis", self.sink().await?);          // outbound
11//! f.extend(pump_source(db, "redis", self.subscription().await?));  // inbound
12//! Ok(f)
13//! ```
14//!
15//! Both are `no_std + alloc`-native (boxed futures, no `tokio`).
16
17use alloc::boxed::Box;
18use alloc::sync::Arc;
19use alloc::vec;
20use alloc::vec::Vec;
21
22use super::Source;
23use crate::builder::{AimDb, BoxFuture};
24use crate::router::RouterBuilder;
25use crate::transport::{Connector, ConnectorConfig};
26
27/// Outbound pump: one publisher future per outbound route on `scheme`.
28///
29/// Extracts the consume-and-publish loop a data-plane connector used to write by
30/// hand. For each route from [`collect_outbound_routes`](AimDb::collect_outbound_routes),
31/// the returned future subscribes to the route's fused
32/// [`SerializedSource`](crate::connector::SerializedSource) — whose readers
33/// yield destination + serialized payload directly (no `Box<dyn Any>` per
34/// message) — and publishes through `sink`. Per-route
35/// configuration (`qos`/`retain`/…) is built once from the route's URL query
36/// via [`ConnectorConfig::from_query`].
37///
38/// The publisher future terminates when its subscription yields an error (e.g. the
39/// record buffer closed), matching the legacy hand-rolled loop.
40pub fn pump_sink(db: &AimDb, scheme: &str, sink: Arc<dyn Connector>) -> Vec<BoxFuture> {
41    let routes = db.collect_outbound_routes(scheme);
42    let mut futures: Vec<BoxFuture> = Vec::with_capacity(routes.len());
43
44    for crate::OutboundRoute {
45        topic: default_topic,
46        source,
47        config,
48    } in routes
49    {
50        let sink = sink.clone();
51        let runtime_ctx = db.runtime_ctx();
52        let cfg = ConnectorConfig::from_query(&config);
53        let scratch_capacity = source.serializer_scratch_capacity().unwrap_or(0);
54
55        futures.push(Box::pin(async move {
56            // Subscribe inside the pump future (not at collect time), so the
57            // ring-buffer cursor starts when the publisher actually runs.
58            let mut reader = source.subscribe();
59            // One bounded allocation per route, reused for every successful
60            // into-slice serialization. Owned-only sources request zero bytes.
61            let mut scratch = vec![0_u8; scratch_capacity];
62
63            log_info!(
64                "pump_sink: publisher started for destination: {}",
65                default_topic
66            );
67
68            loop {
69                let msg = match reader
70                    .recv_into(&runtime_ctx, scratch.as_mut_slice())
71                    .await
72                {
73                    Ok(m) => m,
74                    // SPMC-ring overflow: messages were missed, but the reader
75                    // recovers (cursor resets to the oldest live value). Skip the
76                    // gap and keep pumping — a transient lag must not permanently
77                    // kill the publisher.
78                    Err(crate::DbError::BufferLagged { .. }) => {
79                        log_warn!("pump_sink: consumer lagged for '{}'", default_topic);
80                        continue;
81                    }
82                    // Buffer closed / fatal — the record is gone; end the publisher.
83                    Err(_e) => {
84                        log_info!(
85                            "pump_sink: publisher stopping for '{}': {:?}",
86                            default_topic,
87                            _e
88                        );
89                        break;
90                    }
91                };
92                let crate::connector::SerializedValueInto { dest, payload } = msg;
93                // Destination: dynamic (resolved by the source) or default (from URL).
94                let dest = dest.as_deref().unwrap_or(&default_topic);
95
96                let payload = match &payload {
97                    crate::connector::SerializedPayload::Scratch { len } => {
98                        let Some(payload) = scratch.get(..*len) else {
99                            log_error!(
100                                "pump_sink: serializer returned invalid length {} for {}-byte scratch buffer",
101                                len,
102                                scratch.len()
103                            );
104                            continue;
105                        };
106                        payload
107                    }
108                    crate::connector::SerializedPayload::Owned(payload) => payload.as_slice(),
109                };
110
111                // Publish through the connector's pure I/O adapter.
112                if let Err(_e) = sink.publish(dest, &cfg, payload).await {
113                    log_error!("pump_sink: failed to publish to '{}': {:?}", dest, _e);
114                } else {
115                    log_debug!("pump_sink: published to: {}", dest);
116                }
117            }
118
119            log_info!(
120                "pump_sink: publisher stopped for destination: {}",
121                default_topic
122            );
123        }));
124    }
125
126    futures
127}
128
129/// Inbound pump: a single multiplexed reader future for `scheme`.
130///
131/// Drives one [`Source`] (never one task per topic), fanning each
132/// `(topic, payload)` out to the matching producers via a [`Router`] built from
133/// [`collect_inbound_routes`](AimDb::collect_inbound_routes).
134///
135/// Backpressure: [`Router::route`] drops + logs on a full producer buffer rather
136/// than blocking, so one slow record never stalls the shared source. Route errors
137/// are non-fatal.
138///
139/// [`Router`]: crate::router::Router
140/// [`Router::route`]: crate::router::Router::route
141pub fn pump_source(db: &AimDb, scheme: &str, mut src: impl Source + 'static) -> Vec<BoxFuture> {
142    let routes = db.collect_inbound_routes(scheme);
143    let router = Arc::new(RouterBuilder::from_routes(routes).build());
144    let ctx = db.runtime_ctx();
145
146    vec![Box::pin(async move {
147        log_info!(
148            "pump_source: reader started ({} topics)",
149            router.resource_ids().len()
150        );
151
152        while let Some((topic, payload)) = src.next().await {
153            // `route` deserializes and fans out to producers (synchronously —
154            // the fused ingest path never awaits); it drops + logs on a full
155            // producer buffer and never returns a fatal error.
156            if let Err(_e) = router.route(&topic, &payload, &ctx) {
157                log_error!(
158                    "pump_source: failed to route message on '{}': {}",
159                    topic,
160                    _e
161                );
162            }
163        }
164
165        log_info!("pump_source: reader stopped");
166    })]
167}