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}