aimdb_sync/
lib.rs

1//! # AimDB Sync API
2//!
3//! Synchronous API wrapper for AimDB that enables blocking operations
4//! on the async database. Perfect for FFI, legacy codebases, and simple scripts.
5//!
6//! ## Overview
7//!
8//! This crate provides a synchronous interface to AimDB by running the
9//! async runtime on a dedicated background thread and using channels
10//! to bridge between synchronous and asynchronous contexts.
11//!
12//! ## Features
13//!
14//! ### Producer Operations
15//! - **`set()`**: Blocking send, waits if channel is full
16//! - **`set_timeout()`**: Blocking send with timeout
17//! - **`try_set()`**: Non-blocking send, returns immediately
18//!
19//! ### Consumer Operations
20//! - **`get()`**: Blocking receive, waits for value
21//! - **`get_timeout()`**: Blocking receive with timeout
22//! - **`try_get()`**: Non-blocking receive, returns immediately
23//!
24//! ### General
25//! - **Thread-Safe**: All types are `Send + Sync` and can be shared across threads
26//! - **Type-Safe**: Full compile-time type safety with generics
27//! - **Pure Sync Context**: No `#[tokio::main]` required - works in plain `fn main()`
28//!
29//! ## Architecture
30//!
31//! ```text
32//! User Threads (sync)  →  Channels  →  Runtime Thread (async)
33//!                                        ↓
34//!                                     AimDB (async)
35//!                                        ↓
36//!                                    Buffers (SPMC, etc.)
37//!                                        ↓
38//!                                     Channels  →  Consumer Threads (sync)
39//! ```
40//!
41//! The runtime thread is created automatically when you call `attach()` on the builder.
42//! It stays alive until `detach()` is called or the handle is dropped.
43//!
44//! ## Quick Start
45//!
46//! ```rust,ignore
47//! use aimdb_core::{AimDbBuilder, buffer::BufferCfg};
48//! use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt};
49//! use aimdb_sync::AimDbBuilderSyncExt;
50//! use serde::{Serialize, Deserialize};
51//! use std::sync::Arc;
52//!
53//! #[derive(Debug, Clone, Serialize, Deserialize)]
54//! struct Temperature {
55//!     celsius: f32,
56//! }
57//!
58//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
59//! // Build and attach database (NO #[tokio::main] NEEDED!)
60//! let adapter = Arc::new(TokioAdapter);
61//! let mut builder = AimDbBuilder::new().runtime(adapter);
62//!
63//! builder.configure::<Temperature>(|reg| {
64//!     reg.buffer(BufferCfg::SpmcRing { capacity: 10 });
65//! });
66//!
67//! let handle = builder.attach()?;
68//!
69//! // Create producer and consumer
70//! let producer = handle.producer::<Temperature>()?;
71//! let consumer = handle.consumer::<Temperature>()?;
72//!
73//! // Producer: blocking operations
74//! producer.set(Temperature { celsius: 25.0 })?;
75//!
76//! // Consumer: blocking operations
77//! let temp = consumer.get()?;
78//! println!("Temperature: {:.1}°C", temp.celsius);
79//!
80//! // Clean shutdown
81//! handle.detach()?;
82//! # Ok(())
83//! # }
84//! ```
85//!
86//! ## Multi-threaded Usage
87//!
88//! Both `SyncProducer` and `SyncConsumer` can be cloned and shared across threads:
89//!
90//! ```rust,ignore
91//! use std::thread;
92//!
93//! // Clone for use in another thread
94//! let producer_clone = producer.clone();
95//! let consumer_clone = consumer.clone();
96//!
97//! thread::spawn(move || {
98//!     producer_clone.set(Temperature { celsius: 22.0 }).ok();
99//! });
100//!
101//! thread::spawn(move || {
102//!     if let Ok(temp) = consumer_clone.get() {
103//!         println!("Got: {:.1}°C", temp.celsius);
104//!     }
105//! });
106//! ```
107//!
108//! ## Independent Subscriptions
109//!
110//! Note: Cloning a `SyncConsumer` shares the same channel, so only one thread
111//! will receive each value. For independent subscriptions, create multiple consumers:
112//!
113//! ```rust,ignore
114//! let consumer1 = handle.consumer::<Temperature>()?;
115//! let consumer2 = handle.consumer::<Temperature>()?;
116//!
117//! // Both receive independent copies of all values
118//! ```
119//!
120//! ## Channel Capacity Configuration
121//!
122//! By default, both producers and consumers use a channel capacity of 100.
123//! You can customize this per record type using the `_with_capacity` methods:
124//!
125//! ```rust,ignore
126//! // High-frequency sensor data needs larger buffer
127//! let producer = handle.producer_with_capacity::<SensorData>(1000)?;
128//!
129//! // Rare events can use smaller buffer
130//! let consumer = handle.consumer_with_capacity::<RareEvent>(10)?;
131//!
132//! // SingleLatest-like behavior: use capacity=1 to minimize queueing
133//! let consumer = handle.consumer_with_capacity::<LatestOnly>(1)?;
134//! ```
135//!
136//! **When to adjust capacity:**
137//! - **Increase**: High-frequency data, bursty traffic, slow consumers
138//! - **Decrease**: Memory-constrained, rare events, strict backpressure needed
139//! - **Capacity=1**: Approximate SingleLatest semantics (see limitation below)
140//! - **Default (100)**: Good for most use cases
141//!
142//! ## Buffer Semantics Limitation
143//!
144//! **Important**: The sync API adds a queueing layer (`std::sync::mpsc` channel)
145//! between the database buffer and your code. This means:
146//!
147//! - ✅ **SPMC Ring**: Works as expected - each consumer gets independent data
148//! - ✅ **Mailbox**: Works well - last value is preserved
149//! - ⚠️ **SingleLatest**: Best effort only - the sync channel may queue multiple values
150//!
151//! ### Solutions for SingleLatest Semantics
152//!
153//! 1. **Use `get_latest()`** - Drains the channel to get the most recent value:
154//!    ```rust,ignore
155//!    // Always get the latest value, skipping queued intermediates
156//!    let latest = consumer.get_latest()?;
157//!    ```
158//!
159//! 2. **Use capacity=1** - Minimize queueing:
160//!    ```rust,ignore
161//!    let consumer = handle.consumer_with_capacity::<T>(1)?;
162//!    ```
163//!
164//! 3. **Use the async API directly** - For perfect semantic preservation.
165//!
166//! The sync API is optimized for simplicity and ease of use, not for perfect
167//! semantic preservation across all buffer types.
168//!
169//! ## Threading Model
170//!
171//! - **User threads**: Unlimited - any number of threads can call operations concurrently
172//! - **Runtime thread**: One dedicated thread named "aimdb-sync-runtime"
173//! - **Channels**: Lock-free MPSC channels for efficient communication
174//!
175//! ## Performance
176//!
177//! - **Overhead**: ~100-500μs per operation vs pure async (channel + context switch)
178//! - **Throughput**: Limited by channel capacity (default: 100 items)
179//! - **Latency**: Excellent for <50ms target, not suitable for hard low-latency requirements
180//!
181//! ## Error Handling
182//!
183//! All operations return `DbResult<T>` which wraps standard `DbError` variants:
184//!
185//! - `RuntimeShutdown`: The runtime thread stopped
186//! - `SetTimeout`: Producer timeout expired
187//! - `GetTimeout`: Consumer timeout expired or no data (try_get)
188//! - `AttachFailed`: Failed to start runtime thread
189//! - `DetachFailed`: Failed to stop runtime thread
190//! - `RecordNotFound`: Attempted to produce/consume unregistered type
191//! - Plus any other errors from the underlying `produce()` operation
192//!
193//! ### Error Propagation
194//!
195//! Producer errors are propagated synchronously back to the caller:
196//! - `set()` and `set_with_timeout()` block until the produce operation completes
197//!   and return any errors that occur in the async context
198//! - `try_set()` sends immediately without waiting for the produce result (fire-and-forget)
199//!
200//! ```rust,ignore
201//! // Errors are properly propagated to the caller
202//! match producer.set(data) {
203//!     Ok(()) => println!("Successfully produced"),
204//!     Err(DbError::RecordNotFound { .. }) => eprintln!("Type not registered"),
205//!     Err(e) => eprintln!("Production failed: {}", e),
206//! }
207//! ```
208//!
209//! ## Safety
210//!
211//! All types are thread-safe and can be shared across threads via `Clone`.
212//! The API ensures proper resource cleanup through RAII and explicit `detach()`.
213
214#![warn(missing_docs)]
215#![warn(clippy::all)]
216#![cfg_attr(docsrs, feature(doc_cfg))]
217
218mod consumer;
219mod handle;
220mod producer;
221
222pub use consumer::SyncConsumer;
223pub use handle::{AimDbBuilderSyncExt, AimDbHandle, AimDbSyncExt, DEFAULT_SYNC_CHANNEL_CAPACITY};
224pub use producer::SyncProducer;
225
226// Re-export error types from aimdb-core
227pub use aimdb_core::{DbError, DbResult};