Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e9b26ea
extracted background code; to be refactored further
fresheed Jul 25, 2026
94c31d7
wip: simplifying producer
fresheed Jul 25, 2026
e95b99c
very wip
fresheed Jul 26, 2026
8f19428
extracted buffer forwarding of consumer
fresheed Jul 27, 2026
2789b8d
wip: updating consumer
fresheed Jul 27, 2026
f858702
wip: rewriting consumer, need clarifications from maintainer
fresheed Jul 28, 2026
899aa24
code seems to be fixed
fresheed Jul 29, 2026
44bbb12
integration tests passing; units are failing
fresheed Jul 29, 2026
fc5e1f6
moved compile-time checks for Send/Sync
fresheed Aug 1, 2026
f334bc7
removed everything capacity-related
fresheed Aug 1, 2026
cd1f994
`make check` passes
fresheed Aug 1, 2026
d804b2d
clarified shutdown test
fresheed Aug 1, 2026
3aa1fdf
made settable_integration run upon tests
fresheed Aug 1, 2026
7335f77
updated cargo.lock (see previous commit)
fresheed Aug 1, 2026
a20f2b4
added test for shutting down after producing
fresheed Aug 1, 2026
3d794f0
refactored integration tests
fresheed Aug 1, 2026
1c43f92
more detailed error handling for subscribing
fresheed Aug 1, 2026
ccb981d
fixed example (mut; called detach)
fresheed Aug 1, 2026
a8db4a2
cleaning up docs; removed `unsafe impl`s from handle
fresheed Aug 1, 2026
2a41f1d
some cleanup in integratino tests
fresheed Aug 1, 2026
4e65534
cleaned up docs; removed previously added .detach from example
fresheed Aug 1, 2026
6a4d135
removed mentions of channels and removed methods; removed claim about…
fresheed Aug 1, 2026
2d38d81
added test for failing non-blocking methods
fresheed Aug 1, 2026
94fdd92
pass after the initial review
fresheed Aug 2, 2026
4be9c2d
removed mentions of static_assertions
fresheed Aug 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 63 additions & 89 deletions aimdb-sync/src/consumer.rs
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
//! Synchronous consumer for typed records.

use aimdb_core::{DbError, Reader};

use crate::waiter::Waiter;
use crate::{SyncError, SyncResult};
use alloc::sync::Arc;
use core::fmt::Debug;
use core::time::Duration;
use std::sync::mpsc;
use std::sync::Mutex;

/// Synchronous consumer for records of type `T`.
///
/// Thread-safe, can be cloned and shared across threads.
/// Each clone receives data independently according to buffer semantics (SPMC, etc.).
///
/// # Thread Safety
///
/// Multiple clones of `SyncConsumer<T>` can be used concurrently from
/// different threads. Each receives data independently based on the
/// configured buffer type (SPMC, SingleLatest, etc.).
/// Not thread-safe - can be moved to another thread, but not cloned.
/// Each instance of SyncConsumer<T> reading from the same producer
/// receives data independently according to buffer semantics (SPMC, etc.).
///
/// # Example
///
Expand All @@ -25,7 +20,7 @@ use std::sync::Mutex;
/// # use serde::{Serialize, Deserialize};
/// # #[derive(Debug, Clone, Serialize, Deserialize)]
/// # struct Temperature { celsius: f32 }
/// # fn example(consumer: &SyncConsumer<Temperature>) -> SyncResult<()> {
/// # fn example(consumer: &mut SyncConsumer<Temperature>) -> SyncResult<()> {
/// // Get value (blocks until available)
/// let temp = consumer.get()?;
/// println!("Temperature: {}°C", temp.celsius);
Expand All @@ -47,22 +42,27 @@ use std::sync::Mutex;
/// ```
pub struct SyncConsumer<T>
where
T: Send + Sync + 'static + Debug + Clone,
T: Send + Debug + Clone,
{
/// Channel receiver for consumer data
/// Wrapped in `Arc<Mutex>` so it can be shared but only one thread receives at a time
rx: Arc<Mutex<mpsc::Receiver<T>>>,
waiter: Waiter,
reader: Reader<T>,
}

impl<T> SyncConsumer<T>
where
T: Send + Sync + 'static + Debug + Clone,
T: Send + Debug + Clone,
{
/// Create a new sync consumer (internal use only)
pub(crate) fn new(rx: mpsc::Receiver<T>) -> Self {
Self {
rx: Arc::new(Mutex::new(rx)),
}
pub(crate) fn new(waiter: Waiter, reader: Reader<T>) -> Self {
Self { waiter, reader }
}

async fn get_impl(reader: &mut Reader<T>) -> SyncResult<T> {
let res = reader.recv().await;
res.map_err(|e| match e {
DbError::BufferClosed { .. } => SyncError::RuntimeShutdown,
e => SyncError::Db(e),
})
}

/// Get a value, blocking until one is available.
Expand All @@ -77,6 +77,7 @@ where
/// # Errors
///
/// - `SyncError::RuntimeShutdown` if the runtime thread has stopped
/// - `SyncError::Db` for other errors occured during read
///
/// # Example
///
Expand All @@ -92,15 +93,14 @@ where
/// let handle = AimDbBuilder::new()
/// .runtime(Arc::new(TokioAdapter))
/// .attach()?;
/// let consumer = handle.consumer::<MyData>("my_data")?;
/// let mut consumer = handle.consumer::<MyData>("my_data")?;
/// let data = consumer.get()?; // blocks until value available
/// println!("Got: {:?}", data);
/// # Ok(())
/// # }
/// ```
pub fn get(&self) -> SyncResult<T> {
let rx = self.rx.lock().unwrap();
rx.recv().map_err(|_| SyncError::RuntimeShutdown)
pub fn get(&mut self) -> SyncResult<T> {
self.waiter.block_on(Self::get_impl(&mut self.reader))
}

/// Get a value with a timeout.
Expand All @@ -115,6 +115,7 @@ where
///
/// - `SyncError::GetTimeout` if the timeout expires
/// - `SyncError::RuntimeShutdown` if the runtime thread has stopped
/// - `SyncError::Db` for other errors occured during read
///
/// # Example
///
Expand All @@ -131,20 +132,18 @@ where
/// let handle = AimDbBuilder::new()
/// .runtime(Arc::new(TokioAdapter))
/// .attach()?;
/// let consumer = handle.consumer::<MyData>("my_data")?;
/// let mut consumer = handle.consumer::<MyData>("my_data")?;
/// match consumer.get_with_timeout(Duration::from_millis(100)) {
/// Ok(data) => println!("Got: {:?}", data),
/// Err(_) => println!("No data available"),
/// }
/// # Ok(())
/// # }
/// ```
pub fn get_with_timeout(&self, timeout: Duration) -> SyncResult<T> {
let rx = self.rx.lock().unwrap();
rx.recv_timeout(timeout).map_err(|e| match e {
mpsc::RecvTimeoutError::Timeout => SyncError::GetTimeout,
mpsc::RecvTimeoutError::Disconnected => SyncError::RuntimeShutdown,
})
pub fn get_with_timeout(&mut self, timeout: Duration) -> SyncResult<T> {
let fut = async { tokio::time::timeout(timeout, Self::get_impl(&mut self.reader)).await };
let res = self.waiter.block_on(fut);
res.unwrap_or_else(|_| Err(SyncError::GetTimeout))
}

/// Try to get a value without blocking.
Expand All @@ -156,6 +155,7 @@ where
///
/// - `SyncError::GetTimeout` if no data is available (non-blocking)
/// - `SyncError::RuntimeShutdown` if the runtime thread has stopped
/// - `SyncError::Db` for other errors occured during read
///
/// # Example
///
Expand All @@ -171,25 +171,26 @@ where
/// let handle = AimDbBuilder::new()
/// .runtime(Arc::new(TokioAdapter))
/// .attach()?;
/// let consumer = handle.consumer::<MyData>("my_data")?;
/// let mut consumer = handle.consumer::<MyData>("my_data")?;
/// match consumer.try_get() {
/// Ok(data) => println!("Got: {:?}", data),
/// Err(_) => println!("No data yet"),
/// }
/// # Ok(())
/// # }
/// ```
pub fn try_get(&self) -> SyncResult<T> {
let rx = self.rx.lock().unwrap();
rx.try_recv().map_err(|e| match e {
mpsc::TryRecvError::Empty => SyncError::GetTimeout,
mpsc::TryRecvError::Disconnected => SyncError::RuntimeShutdown,
pub fn try_get(&mut self) -> SyncResult<T> {
let res = self.reader.try_recv();
res.map_err(|e| match e {
DbError::BufferClosed { .. } => SyncError::RuntimeShutdown,
DbError::BufferEmpty => SyncError::GetTimeout,
e => SyncError::Db(e),
})
}

/// Get the latest value by draining all queued values.
///
/// This method drains the internal channel to get the most recent value,
/// This method drains the buffer to get the most recent value,
/// discarding any intermediate values. This is useful for SingleLatest-like
/// semantics where you only care about the most recent data.
///
Expand All @@ -201,8 +202,10 @@ where
/// The most recent available record of type `T`.
///
/// # Errors
///
/// Note that the error is only reported if no value was retrieved at all.
/// Errors occuring after that are ignored; the latest obtained value is returned instead.
/// - `SyncError::RuntimeShutdown` if the runtime thread has stopped
/// - `SyncError::Db` if another error occured upon the very first read.
///
/// # Example
///
Expand All @@ -218,25 +221,24 @@ where
/// let handle = AimDbBuilder::new()
/// .runtime(Arc::new(TokioAdapter))
/// .attach()?;
/// let consumer = handle.consumer::<MyData>("my_data")?;
/// let mut consumer = handle.consumer::<MyData>("my_data")?;
///
/// // Get the latest value, skipping any queued intermediate values
/// let latest = consumer.get_latest()?;
/// println!("Latest: {:?}", latest);
/// # Ok(())
/// # }
/// ```
pub fn get_latest(&self) -> SyncResult<T> {
let rx = self.rx.lock().unwrap();

// First, block until we have at least one value
let mut latest = rx.recv().map_err(|_| SyncError::RuntimeShutdown)?;

// Then drain all remaining values to get the most recent
while let Ok(value) = rx.try_recv() {
latest = value;
pub fn get_latest(&mut self) -> SyncResult<T> {
// 1) can simply sequence get and try_get -
// no one else does it simultaneously thanks to &mut self
// 2) if draining ends up with an error, we follow the previous impl
// and return the latest succesfully read value
// 3) potentially loops forever if producer keeps producing
let mut latest = self.get()?;
while let Ok(upd) = self.try_get() {
latest = upd;
}

Ok(latest)
}

Expand Down Expand Up @@ -270,7 +272,7 @@ where
/// let handle = AimDbBuilder::new()
/// .runtime(Arc::new(TokioAdapter))
/// .attach()?;
/// let consumer = handle.consumer::<MyData>("my_data")?;
/// let mut consumer = handle.consumer::<MyData>("my_data")?;
///
/// // Get the latest value within 100ms
/// match consumer.get_latest_with_timeout(Duration::from_millis(100)) {
Expand All @@ -280,49 +282,21 @@ where
/// # Ok(())
/// # }
/// ```
pub fn get_latest_with_timeout(&self, timeout: Duration) -> SyncResult<T> {
let rx = self.rx.lock().unwrap();

// First, block with timeout until we have at least one value
let mut latest = rx.recv_timeout(timeout).map_err(|e| match e {
mpsc::RecvTimeoutError::Timeout => SyncError::GetTimeout,
mpsc::RecvTimeoutError::Disconnected => SyncError::RuntimeShutdown,
})?;

// Then drain all remaining values to get the most recent
while let Ok(value) = rx.try_recv() {
latest = value;
pub fn get_latest_with_timeout(&mut self, timeout: Duration) -> SyncResult<T> {
// see internal comments for get_latest
let mut latest = self.get_with_timeout(timeout)?;
while let Ok(upd) = self.try_get() {
latest = upd;
}

Ok(latest)
}
}

impl<T> Clone for SyncConsumer<T>
where
T: Send + Sync + 'static + Debug + Clone,
{
/// Clone the consumer to share across threads.
///
/// Note: All clones share the same receiver, so only one thread
/// will receive each value. For independent subscriptions, call
/// `handle.consumer()` multiple times instead.
fn clone(&self) -> Self {
Self {
rx: self.rx.clone(),
}
}
}

// Safety: SyncConsumer uses Arc internally and is safe to send/share
unsafe impl<T> Send for SyncConsumer<T> where T: Send + Sync + 'static + Debug + Clone {}
unsafe impl<T> Sync for SyncConsumer<T> where T: Send + Sync + 'static + Debug + Clone {}

#[cfg(test)]
mod tests {
#[test]
fn test_sync_consumer_is_send_sync() {
// Just checking that the type implements Send + Sync
// Actual functionality tests will come later
fn assert_send<T: Send>() {}
#[allow(dead_code)]
fn check<X: Send + std::fmt::Debug + Clone>() {
assert_send::<crate::SyncConsumer<X>>();
}
}
6 changes: 3 additions & 3 deletions aimdb-sync/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ use aimdb_core::DbError;

/// Errors from the synchronous (blocking) API.
///
/// Facade-specific failures (attach/detach, channel timeouts, runtime-thread
/// shutdown) are their own variants; anything from the underlying database
/// wraps a [`DbError`] via [`SyncError::Db`].
/// Facade-specific failures (attach/detach, runtime-thread shutdown) are their
/// own variants; anything from the underlying database wraps a [`DbError`]
/// via [`SyncError::Db`].
#[derive(Debug, thiserror::Error)]
pub enum SyncError {
/// Failed to attach the database to the runtime thread.
Expand Down
Loading