Skip to main content

aimdb_core/buffer/
traits.rs

1//! Runtime-agnostic buffer traits
2//!
3//! Defines `Buffer<T>` (static trait) and `DynBuffer<T>` (trait object) for
4//! buffer implementations. Adapters (tokio, embassy) provide concrete types.
5//!
6//! See `aimdb-tokio-adapter` and `aimdb-embassy-adapter` for implementations.
7
8use core::task::{Context, Poll};
9
10use alloc::boxed::Box;
11#[cfg(feature = "remote")]
12use alloc::vec::Vec;
13
14use super::BufferCfg;
15use crate::DbError;
16
17/// Static buffer trait for concrete implementations
18///
19/// Provides push/subscribe operations for typed buffers. Readers are owned
20/// and can outlive the subscription call (required for spawned tasks).
21///
22/// Trait bounds ensure thread-safety and `'static` lifetime for async runtimes.
23///
24/// See `aimdb_tokio_adapter::TokioRingBuffer` for implementation example.
25pub trait Buffer<T: Clone + Send>: Send + Sync + 'static {
26    /// Reader type for consuming values
27    ///
28    /// Each `subscribe()` call returns an independent owned reader.
29    type Reader: BufferReader<T> + 'static;
30
31    /// Creates a new buffer with the given configuration
32    ///
33    /// # Panics
34    /// May panic if configuration is invalid (call `cfg.validate()` first)
35    fn new(cfg: &BufferCfg) -> Self
36    where
37        Self: Sized;
38
39    /// Push a value into the buffer (non-blocking)
40    ///
41    /// Behavior depends on buffer type:
42    /// - **SPMC Ring**: Overwrites oldest value if full
43    /// - **SingleLatest**: Overwrites previous value
44    /// - **Mailbox**: Overwrites pending value if not consumed
45    fn push(&self, value: T);
46
47    /// Create a new independent reader for a consumer
48    ///
49    /// Each reader maintains its own position and can consume at its own pace.
50    /// The returned reader is owned and can outlive this reference.
51    ///
52    /// # Fresh-subscriber semantics (SingleLatest)
53    ///
54    /// For a [`SingleLatest`](BufferCfg::SingleLatest) buffer, a fresh
55    /// subscriber observes the buffer's current value once, as if it had been
56    /// pushed right after subscription; if nothing has been produced yet, it
57    /// waits. A late subscriber to state wants the current state, not just
58    /// future changes. This rule holds identically across every runtime adapter
59    /// and is enforced by
60    /// [`test_support::assert_single_latest_contract`](super::test_support::assert_single_latest_contract).
61    ///
62    /// `SpmcRing` and `Mailbox` readers have no such replay: a fresh subscriber
63    /// only sees values produced after it subscribes.
64    fn subscribe(&self) -> Self::Reader;
65}
66
67/// Dynamic buffer trait for trait objects (object-safe)
68///
69/// Type-erased interface for buffers that can be stored as trait objects.
70/// Automatically implemented for all `Buffer<T>` types via blanket impl.
71///
72/// Used when storing heterogeneous buffer types (e.g., in `TypedRecord`).
73pub trait DynBuffer<T: Clone + Send>: Send + Sync {
74    /// Push a value into the buffer (non-blocking)
75    fn push(&self, value: T);
76
77    /// Create a boxed reader for consuming values
78    ///
79    /// Returns a type-erased reader. Each reader maintains its own position.
80    fn subscribe_boxed(&self) -> Box<dyn BufferReader<T> + Send>;
81
82    /// Returns self as Any for downcasting to concrete buffer types
83    fn as_any(&self) -> &dyn core::any::Any;
84
85    /// Non-destructive read of the buffer's current value.
86    ///
87    /// Returns `Some(T)` if the buffer holds a current value that can be read
88    /// without affecting any consumer's position. Returns `None` if the buffer
89    /// type has no canonical "current value" concept (e.g., SPMC Ring) or if
90    /// no value has been produced yet.
91    ///
92    /// This is the buffer-native point-in-time read used by AimX `record.get`
93    ///. Implementations must not advance any reader position.
94    ///
95    /// The default returns `None`, which is the correct behaviour for buffers
96    /// without a canonical latest value.
97    fn peek(&self) -> Option<T> {
98        None
99    }
100
101    /// Get buffer metrics snapshot (metrics feature only)
102    ///
103    /// Returns `Some(snapshot)` if the buffer implementation supports metrics,
104    /// `None` otherwise. Default implementation returns `None`.
105    #[cfg(feature = "observability")]
106    fn metrics_snapshot(&self) -> Option<BufferMetricsSnapshot> {
107        None
108    }
109
110    /// Reset buffer metrics counters (metrics feature only)
111    ///
112    /// Default implementation is a no-op so buffers without metrics support are
113    /// safe to call. Implementations that track counters should override this
114    /// to zero them.
115    #[cfg(feature = "observability")]
116    fn reset_metrics(&self) {}
117}
118
119/// Non-blocking push error — carries the rejected value back to the caller.
120///
121/// Returned by [`Producer::try_produce`](crate::typed_api::Producer::try_produce).
122/// Both variants own the value so the caller can retry, escalate, or drop it.
123#[derive(Debug)]
124pub enum TryProduceError<T> {
125    /// Buffer is at capacity and configured not to overwrite. Transient.
126    Full(T),
127    /// Buffer / record has been torn down (e.g. shutdown). Terminal.
128    Closed(T),
129}
130
131// `Display` describes the failure mode only; the rejected value is the payload,
132// not part of the message, so this carries no `T: Display` bound.
133impl<T> core::fmt::Display for TryProduceError<T> {
134    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
135        match self {
136            Self::Full(_) => f.write_str("buffer is at capacity and configured not to overwrite"),
137            Self::Closed(_) => f.write_str("buffer or record has been torn down"),
138        }
139    }
140}
141
142// `Error: Debug + Display`; the derived `Debug` needs `T: Debug`, which every
143// `Producer<T>` already requires. Gated on `std` to match the crate's other
144// local error types (`PublishError`, `SerializeError`).
145#[cfg(feature = "std")]
146impl<T: core::fmt::Debug> std::error::Error for TryProduceError<T> {}
147
148/// Write-side handle for a single record.
149///
150/// `Producer<T>` holds an `Arc<dyn WriteHandle<T>>` so it can be parameterised
151/// over `T` alone — no runtime adapter `R` and no per-call record-key string
152/// lookup on the produce hot path. The implementor (`RecordWriter<T>`)
153/// pre-binds the underlying buffer, the latest-snapshot slot, and the metadata
154/// tracker at build time.
155///
156/// Crate-private on purpose. `Producer<T>::new` is the only construction path;
157/// external test code that needs a fake Producer should go through a future
158/// `Producer::for_testing(...)` helper rather than implementing `WriteHandle`
159/// directly.
160pub(crate) trait WriteHandle<T: Clone + Send + 'static>: Send + Sync {
161    /// Push a value into the buffer, update the latest-snapshot cache, and
162    /// (when a buffer is present) mark the metadata `last_update` timestamp.
163    /// Infallible — all three operations are synchronous and lock-free or
164    /// spin-locked.
165    fn push(&self, value: T);
166
167    /// Default: delegate to `push` and return `Ok(())`. Overwriting buffers
168    /// cannot fail, so the value is always accepted. Bounded / non-overwriting
169    /// buffers override this to return `Full(value)` or `Closed(value)`.
170    fn try_push(&self, value: T) -> Result<(), TryProduceError<T>> {
171        self.push(value);
172        Ok(())
173    }
174}
175
176/// Reader trait for consuming values from a buffer
177///
178/// This is the object-safe **service-provider interface** that runtime adapters
179/// implement. It is poll-based — and therefore object-safe and zero-allocation —
180/// rather than `async`: an `async fn` on an erased trait forces a
181/// `Pin<Box<dyn Future>>` heap allocation on every call.
182/// Consumers do not call this directly; they use the [`Reader<T>`](super::Reader)
183/// handle returned by `Consumer::subscribe`, whose `recv()` is `async` and wraps
184/// [`poll_recv`](BufferReader::poll_recv) via `core::future::poll_fn` with no
185/// allocation.
186///
187/// Each reader is independent with its own state.
188///
189/// # Error Handling
190/// - `Ok(value)` - Successfully received a value
191/// - `Err(BufferLagged)` - Missed messages (SPMC ring only, can continue)
192/// - `Err(BufferClosed)` - Buffer closed (graceful shutdown)
193///
194/// # Contract (poll/try_recv interleaving and cancellation)
195///
196/// Every implementation in this workspace (tokio/embassy/wasm adapters)
197/// conforms — this section makes the contract explicit for future
198/// implementors.
199///
200/// 1. `try_recv` MAY be called between `Pending` `poll_recv` calls;
201///    implementations MUST NOT lose values or wakeups across the
202///    interleaving.
203/// 2. A value claimed internally just before caller cancellation MUST be
204///    delivered on the next `poll_recv`/`try_recv` call — this is why, e.g.,
205///    the tokio broadcast/watch readers keep a persistent `ReusableBoxFuture`
206///    across calls rather than dropping and recreating it per call.
207/// 3. `poll_recv` MUST register the *latest* waker on every `Pending`
208///    return; spurious wakes are permitted (a registered waker firing with
209///    no corresponding new value is not a bug — callers must re-poll and
210///    tolerate a no-op).
211/// 4. Lag MUST surface as `Err(DbError::BufferLagged)` plus a metrics
212///    `add_dropped` call on **both** `poll_recv` and `try_recv`.
213pub trait BufferReader<T: Clone + Send>: Send {
214    /// Poll for the next value.
215    ///
216    /// Returns `Poll::Ready(Ok(value))` when a value is available,
217    /// `Poll::Ready(Err(..))` on lag/closure, or `Poll::Pending` after
218    /// registering `cx.waker()` to be woken when the next value arrives.
219    ///
220    /// # Behavior by Buffer Type
221    /// - **SPMC Ring**: Returns next value, or `Lagged(n)` if fell behind
222    /// - **SingleLatest**: Waits for value change, returns most recent
223    /// - **Mailbox**: Waits for slot value, takes and clears it
224    fn poll_recv(&mut self, cx: &mut Context<'_>) -> Poll<Result<T, DbError>>;
225
226    /// Non-blocking receive — returns immediately.
227    ///
228    /// Returns `Err(DbError::BufferEmpty)` if no pending values.
229    ///
230    /// # Behavior by Buffer Type
231    /// - **SPMC Ring**: Returns next buffered value, or `BufferEmpty` if caught up
232    /// - **SingleLatest**: Returns value if changed since last read, or `BufferEmpty`
233    /// - **Mailbox**: Takes and returns slot value, or `BufferEmpty` if empty
234    fn try_recv(&mut self) -> Result<T, DbError>;
235}
236
237/// Reader trait for consuming JSON-serialized values from a buffer.
238///
239/// Type-erased reader that subscribes to a typed buffer and emits either
240/// `serde_json::Value` or already-serialized JSON bytes. Used by the remote
241/// access protocol for subscriptions.
242///
243/// This trait enables subscribing to a buffer without knowing the concrete type `T`
244/// at compile time, by serializing values to JSON on each poll.
245///
246/// Object-safe and poll-based for the same reason as [`BufferReader`].
247/// Consumers use the [`JsonReader`](super::JsonReader) handle, whose
248/// receive methods are `async` wrappers over the corresponding poll methods
249/// and do not box a per-message future.
250///
251/// # Requirements
252/// - Record must be configured with `.with_remote_access()`
253/// - Only available with the `remote` feature (requires serde_json)
254#[cfg(feature = "remote")]
255pub trait JsonBufferReader: Send {
256    /// Poll for the next value, serialized to JSON.
257    ///
258    /// Returns `Poll::Ready(Ok(json))` when a value is available and
259    /// serializes successfully, `Poll::Ready(Err(..))` on lag/closure/serialize
260    /// failure, or `Poll::Pending` after registering `cx.waker()`.
261    ///
262    /// # Returns
263    /// - `Ok(JsonValue)` - Successfully received and serialized value
264    /// - `Err(BufferLagged)` - Missed messages (can continue reading)
265    /// - `Err(BufferClosed)` - Buffer closed (graceful shutdown)
266    /// - `Err(SerializationFailed)` - Failed to serialize value to JSON
267    fn poll_recv_json(&mut self, cx: &mut Context<'_>) -> Poll<Result<serde_json::Value, DbError>>;
268
269    /// Non-blocking receive as JSON — returns immediately.
270    ///
271    /// Returns `Err(DbError::BufferEmpty)` if no pending values.
272    fn try_recv_json(&mut self) -> Result<serde_json::Value, DbError>;
273
274    /// Poll for the next value as owned JSON bytes.
275    ///
276    /// Existing implementations inherit a compatibility path that calls
277    /// [`poll_recv_json`](Self::poll_recv_json) and serializes the returned
278    /// tree. Typed record readers override this to encode `T` directly.
279    fn poll_recv_json_bytes(&mut self, cx: &mut Context<'_>) -> Poll<Result<Vec<u8>, DbError>> {
280        match self.poll_recv_json(cx) {
281            Poll::Ready(Ok(value)) => Poll::Ready(
282                serde_json::to_vec(&value)
283                    .map_err(|_| DbError::runtime_error("Failed to serialize value to JSON")),
284            ),
285            Poll::Ready(Err(error)) => Poll::Ready(Err(error)),
286            Poll::Pending => Poll::Pending,
287        }
288    }
289
290    /// Non-blocking receive as owned JSON bytes.
291    ///
292    /// Existing implementations inherit a compatibility path through
293    /// [`try_recv_json`](Self::try_recv_json).
294    fn try_recv_json_bytes(&mut self) -> Result<Vec<u8>, DbError> {
295        let value = self.try_recv_json()?;
296        serde_json::to_vec(&value)
297            .map_err(|_| DbError::runtime_error("Failed to serialize value to JSON"))
298    }
299}
300
301/// Snapshot of buffer metrics at a point in time
302///
303/// Used for introspection and diagnostics. All counters are monotonically
304/// increasing (except after reset).
305#[cfg(feature = "observability")]
306#[derive(Debug, Clone, Default)]
307pub struct BufferMetricsSnapshot {
308    /// Total items pushed to this buffer since creation
309    pub produced_count: u64,
310
311    /// Total items successfully consumed from this buffer (aggregate across all readers)
312    pub consumed_count: u64,
313
314    /// Total items dropped due to overflow/lag (SPMC ring only)
315    ///
316    /// **Note**: When multiple readers lag simultaneously on a broadcast buffer,
317    /// each reader reports its own dropped count independently. This means the
318    /// aggregate dropped_count may exceed the actual number of unique items that
319    /// overflowed from the ring buffer (each lagged reader adds its own lag count).
320    /// This is intentional: it reflects total "missed reads" across all consumers,
321    /// which is useful for diagnosing per-consumer backpressure issues.
322    pub dropped_count: u64,
323
324    /// Current buffer occupancy: (items_in_buffer, capacity)
325    /// Returns (0, 0) for SingleLatest/Mailbox where occupancy is not meaningful
326    pub occupancy: (usize, usize),
327}
328
329/// Optional buffer metrics for introspection (std only, feature-gated)
330///
331/// Implemented by buffer types when the `observability` feature is enabled.
332/// Provides counters for diagnosing producer-consumer imbalances.
333#[cfg(feature = "observability")]
334pub trait BufferMetrics {
335    /// Get a snapshot of current buffer metrics
336    ///
337    /// Returns counters for produced, consumed, and dropped items,
338    /// plus current buffer occupancy.
339    fn metrics(&self) -> BufferMetricsSnapshot;
340
341    /// Reset all metrics counters to zero
342    ///
343    /// Useful for windowed metrics collection. Note that this affects
344    /// all observers of this buffer's metrics.
345    fn reset_metrics(&self);
346}
347
348#[cfg(test)]
349mod tests {
350    use super::*;
351    use alloc::string::ToString;
352
353    // Mock implementation for testing trait bounds
354    struct MockBuffer<T: Clone + Send + Sync> {
355        _phantom: core::marker::PhantomData<T>,
356    }
357
358    struct MockReader<T: Clone + Send> {
359        _phantom: core::marker::PhantomData<T>,
360    }
361
362    impl<T: Clone + Send + Sync + 'static> Buffer<T> for MockBuffer<T> {
363        type Reader = MockReader<T>;
364
365        fn new(_cfg: &BufferCfg) -> Self {
366            Self {
367                _phantom: core::marker::PhantomData,
368            }
369        }
370
371        fn push(&self, _value: T) {
372            // No-op for testing
373        }
374
375        fn subscribe(&self) -> Self::Reader {
376            MockReader {
377                _phantom: core::marker::PhantomData,
378            }
379        }
380    }
381
382    // Explicit DynBuffer implementation for MockBuffer
383    // (no blanket impl - adapters provide their own)
384    impl<T: Clone + Send + Sync + 'static> DynBuffer<T> for MockBuffer<T> {
385        fn push(&self, value: T) {
386            <Self as Buffer<T>>::push(self, value)
387        }
388
389        fn subscribe_boxed(&self) -> Box<dyn BufferReader<T> + Send> {
390            Box::new(self.subscribe())
391        }
392
393        fn as_any(&self) -> &dyn core::any::Any {
394            self
395        }
396
397        #[cfg(feature = "observability")]
398        fn metrics_snapshot(&self) -> Option<BufferMetricsSnapshot> {
399            None // Mock doesn't track metrics
400        }
401    }
402
403    impl<T: Clone + Send> BufferReader<T> for MockReader<T> {
404        fn poll_recv(&mut self, _cx: &mut Context<'_>) -> Poll<Result<T, DbError>> {
405            // Return closed for testing
406            Poll::Ready(Err(DbError::BufferClosed {
407                buffer_name: "mock".to_string(),
408            }))
409        }
410
411        fn try_recv(&mut self) -> Result<T, DbError> {
412            Err(DbError::BufferEmpty)
413        }
414    }
415
416    #[test]
417    fn test_buffer_trait_bounds() {
418        // Verify trait bounds compile
419        fn assert_send<T: Send>() {}
420        fn assert_sync<T: Sync>() {}
421
422        assert_send::<MockBuffer<i32>>();
423        assert_sync::<MockBuffer<i32>>();
424        assert_send::<MockReader<i32>>();
425    }
426
427    #[test]
428    fn test_dyn_buffer_impl() {
429        // Verify DynBuffer can be used as trait object
430        let buffer = MockBuffer::<i32> {
431            _phantom: core::marker::PhantomData,
432        };
433
434        // Should be able to use as DynBuffer
435        let _: &dyn DynBuffer<i32> = &buffer;
436    }
437
438    // WriteHandle impl that always rejects with the buffer full. Used to verify that when
439    // try_push fails, it returns the value that it failed to push.
440    struct FullWriteHandle;
441
442    impl WriteHandle<i32> for FullWriteHandle {
443        fn push(&self, _value: i32) {}
444        fn try_push(&self, value: i32) -> Result<(), TryProduceError<i32>> {
445            Err(TryProduceError::Full(value))
446        }
447    }
448
449    // WriteHandle impl that always rejects because the record is closed. Mirrors
450    // `FullWriteHandle` for the terminal arm.
451    struct ClosedWriteHandle;
452
453    impl WriteHandle<i32> for ClosedWriteHandle {
454        fn push(&self, _value: i32) {}
455        fn try_push(&self, value: i32) -> Result<(), TryProduceError<i32>> {
456            Err(TryProduceError::Closed(value))
457        }
458    }
459
460    #[test]
461    fn try_push_full_round_trips_value_through_producer() {
462        use alloc::sync::Arc;
463        let producer = crate::typed_api::Producer::new(Arc::new(FullWriteHandle));
464        let result = producer.try_produce(42_i32);
465        assert!(
466            matches!(result, Err(TryProduceError::Full(42))),
467            "expected Full(42), got {result:?}",
468        );
469    }
470
471    #[test]
472    fn try_push_closed_round_trips_value_through_producer() {
473        use alloc::sync::Arc;
474        let producer = crate::typed_api::Producer::new(Arc::new(ClosedWriteHandle));
475        let result = producer.try_produce(42_i32);
476        assert!(
477            matches!(result, Err(TryProduceError::Closed(42))),
478            "expected Closed(42), got {result:?}",
479        );
480    }
481
482    #[cfg(feature = "std")]
483    #[test]
484    fn try_produce_error_displays_failure_mode_without_value() {
485        use alloc::string::ToString;
486        assert_eq!(
487            TryProduceError::Full(42_i32).to_string(),
488            "buffer is at capacity and configured not to overwrite",
489        );
490        assert_eq!(
491            TryProduceError::Closed(42_i32).to_string(),
492            "buffer or record has been torn down",
493        );
494    }
495}