Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
abe3294
extracted background code; to be refactored further
fresheed Jul 25, 2026
3284af5
wip: simplifying producer
fresheed Jul 25, 2026
59105b4
very wip
fresheed Jul 26, 2026
5829f86
extracted buffer forwarding of consumer
fresheed Jul 27, 2026
d040cc0
wip: updating consumer
fresheed Jul 27, 2026
e23298f
wip: rewriting consumer, need clarifications from maintainer
fresheed Jul 28, 2026
24c74ee
code seems to be fixed
fresheed Jul 29, 2026
ae2b75b
integration tests passing; units are failing
fresheed Jul 29, 2026
54c87d0
moved compile-time checks for Send/Sync
fresheed Aug 1, 2026
ba3d89f
removed everything capacity-related
fresheed Aug 1, 2026
009b30f
`make check` passes
fresheed Aug 1, 2026
11059d7
clarified shutdown test
fresheed Aug 1, 2026
c1fcc2d
made settable_integration run upon tests
fresheed Aug 1, 2026
01d1876
updated cargo.lock (see previous commit)
fresheed Aug 1, 2026
42dd038
added test for shutting down after producing
fresheed Aug 1, 2026
650fcd0
refactored integration tests
fresheed Aug 1, 2026
7f7486d
more detailed error handling for subscribing
fresheed Aug 1, 2026
ec8d9fb
fixed example (mut; called detach)
fresheed Aug 1, 2026
c8e1fce
cleaning up docs; removed `unsafe impl`s from handle
fresheed Aug 1, 2026
efdba37
some cleanup in integratino tests
fresheed Aug 1, 2026
bd01915
cleaned up docs; removed previously added .detach from example
fresheed Aug 1, 2026
4fa9093
removed mentions of channels and removed methods; removed claim about…
fresheed Aug 1, 2026
4c679ff
added test for failing non-blocking methods
fresheed Aug 1, 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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions aimdb-sync/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ tokio = { version = "1.40", features = ["full", "test-util"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

# Self-dependency to force `data-contracts` on for test targets only (feature
# unification is per-target under resolver = "2"), so `settable_integration.rs`
# runs under plain `cargo test -p aimdb-sync` without making `data-contracts`
# a default feature for downstream consumers of the lib.
aimdb-sync = { path = ".", features = ["data-contracts"] }

[features]
default = ["std"]

Expand Down
152 changes: 64 additions & 88 deletions aimdb-sync/src/consumer.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,17 @@
//! Synchronous consumer for typed records.

use aimdb_core::{DbError, Reader};

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

/// 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 @@ -24,7 +20,7 @@ use std::time::Duration;
/// # 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 @@ -46,22 +42,27 @@ use std::time::Duration;
/// ```
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 @@ -76,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 +94,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 +116,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 @@ -132,20 +134,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 @@ -157,6 +157,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 @@ -173,25 +174,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 @@ -203,8 +205,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 @@ -221,25 +225,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 @@ -274,7 +277,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 @@ -284,49 +287,22 @@ 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
// TODO: is it possible with static_assertions?
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