Skip to content

Commit f8207f7

Browse files
authored
add hello-single-latest sync example (#182)
1 parent 074b2b8 commit f8207f7

7 files changed

Lines changed: 187 additions & 3 deletions

File tree

Cargo.lock

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ members = [
3737
"examples/weather-mesh-demo/weather-station-gamma",
3838
"examples/hello-mailbox",
3939
"examples/hello-mailbox-async",
40+
"examples/hello-single-latest",
4041
"examples/hello-single-latest-async",
4142
"examples/hello-spmc-ring-async",
4243
"examples/readme-quickstart",

Makefile

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ test:
184184

185185
fmt:
186186
@printf "$(GREEN)Formatting code (workspace members only)...$(NC)\n"
187-
@for pkg in aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-tcp-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo embassy-bench-stm32h5 weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-mailbox-async hello-single-latest-async hello-spmc-ring-async aimdb-bench; do \
187+
@for pkg in aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-tcp-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo embassy-bench-stm32h5 weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-mailbox-async hello-single-latest hello-single-latest-async hello-spmc-ring-async aimdb-bench; do \
188188
printf "$(YELLOW) → Formatting $$pkg$(NC)\n"; \
189189
cargo fmt -p $$pkg 2>/dev/null || true; \
190190
done
@@ -193,7 +193,7 @@ fmt:
193193
fmt-check:
194194
@printf "$(GREEN)Checking code formatting (workspace members only)...$(NC)\n"
195195
@FAILED=0; \
196-
for pkg in aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-tcp-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo embassy-bench-stm32h5 weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-mailbox-async hello-single-latest-async hello-spmc-ring-async aimdb-bench; do \
196+
for pkg in aimdb-derive aimdb-data-contracts aimdb-core aimdb-client aimdb-embassy-adapter aimdb-tokio-adapter aimdb-wasm-adapter aimdb-sync aimdb-persistence aimdb-persistence-sqlite aimdb-mqtt-connector aimdb-knx-connector aimdb-ws-protocol aimdb-websocket-connector aimdb-uds-connector aimdb-serial-connector aimdb-tcp-connector aimdb-codegen aimdb-cli aimdb-mcp sync-api-demo tokio-mqtt-connector-demo embassy-mqtt-connector-demo tokio-knx-connector-demo embassy-knx-connector-demo embassy-serial-connector-demo embassy-bench-stm32h5 weather-mesh-common weather-hub weather-station-alpha weather-station-beta hello-mailbox hello-mailbox-async hello-single-latest hello-single-latest-async hello-spmc-ring-async aimdb-bench; do \
197197
printf "$(YELLOW) → Checking $$pkg$(NC)\n"; \
198198
if ! cargo fmt -p $$pkg -- --check 2>&1; then \
199199
printf "$(RED)❌ Formatting check failed for $$pkg$(NC)\n"; \
@@ -420,6 +420,8 @@ examples:
420420
cargo build --package hello-mailbox
421421
@printf "$(YELLOW) → Building hello-mailbox-async $(NC)\n"
422422
cargo build --package hello-mailbox-async
423+
@printf "$(YELLOW) → Building hello-single-latest$(NC)\n"
424+
cargo build --package hello-single-latest
423425
@printf "$(YELLOW) → Building hello-single-latest-async$(NC)\n"
424426
cargo build --package hello-single-latest-async
425427
@printf "$(YELLOW) → Building hello-spmc-ring-async$(NC)\n"

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ docker compose up
132132
| Buffer | Semantics | Use cases |
133133
| --- | --- | --- |
134134
| [**SPMC Ring**](examples/hello-spmc-ring-async) | Bounded stream with independent consumers | Sensor telemetry, event logs |
135-
| [**SingleLatest**](examples/hello-single-latest-async) | Only the current value matters | Feature flags, config, UI state |
135+
| [**SingleLatest**](examples/hello-single-latest) / [**SingleLatest async**](examples/hello-single-latest-async) | Only the current value matters | Feature flags, config, UI state |
136136
| [**Mailbox**](examples/hello-mailbox) / [**async Mailbox**](examples/hello-mailbox-async)| Latest instruction wins | Device commands, actuation, RPC |
137137

138138
**One async API across runtimes.** Tokio, Embassy, WASM — swap the runtime adapter, keep the code. → [How the runtime abstraction works](https://aimdb.dev/blog/building-aimdb-one-async-api)
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[package]
2+
name = "hello-single-latest"
3+
version.workspace = true
4+
edition.workspace = true
5+
license.workspace = true
6+
description = "AimDB minimal sync example demonstrating SingleLatest buffer semantics"
7+
publish = false
8+
9+
[dependencies]
10+
aimdb-core = { path = "../../aimdb-core", features = ["std"] }
11+
aimdb-tokio-adapter = { path = "../../aimdb-tokio-adapter", features = [
12+
"tokio-runtime",
13+
] }
14+
aimdb-sync = { path = "../../aimdb-sync" }
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# hello-single-latest: SingleLatest buffer demo
2+
3+
The `SingleLatest` buffer stores the current value of a record. Each new write replaces the previous value, so subscribers read the latest value instead of replaying every intermediate update. This is useful for feature flags, configuration, UI state, and other records where stale values can be skipped.
4+
5+
## How it works
6+
7+
This example registers a `FeatureFlag` record with:
8+
- `BufferCfg::SingleLatest`
9+
- 2 async `.tap()` that observe rollout updates
10+
- One of the tap observers is "fast", that doesn't wait between receives
11+
- One of the tap observers is "slow", that waits for 2000 ms between receives
12+
13+
The program attaches AimDB through its sync API, and publishes rollout updates using the sync, blocking `producer.set()` method. The taps are managed by AimDB's internal async runtime, so the application can use a synchronous `main` function without `async` or `#[tokio::main]`.
14+
15+
The synchronous producer publishes one update per second. If multiple values are written before the tap reads them, SingleLatest retains only the newest value. In the example, while the fast tap reads the value in the single latest slot, the slow one skips overwritten intermediate values.
16+
17+
## How to run
18+
19+
From the workspace root, run:
20+
21+
```bash
22+
cargo run -p hello-single-latest
23+
```
24+
25+
Expected output includes lines similar to:
26+
27+
```text
28+
=== AimDB SingleLatest Sync Demo ===
29+
30+
1. Building database and attaching for sync API...
31+
2. Creating producer and producing values...
32+
3. Producing rollout percentage values
33+
Main: Setting FeatureFlag 0%
34+
[info] [fast] rollout: 0%
35+
[info] [slow] rollout: 0%
36+
Main: Setting FeatureFlag 25%
37+
[info] [fast] rollout: 25%
38+
Main: Setting FeatureFlag 50%
39+
[info] [fast] rollout: 50%
40+
[info] [slow] rollout: 50%
41+
Main: Setting FeatureFlag 75%
42+
[info] [fast] rollout: 75%
43+
Main: Setting FeatureFlag 100%
44+
[info] [fast] rollout: 100%
45+
[info] [slow] rollout: 100%
46+
4. Shutting down...
47+
Done. The sync producer published rollout updates observed by a SingleLatest tap.
48+
```
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
//! # AimDB SingleLatest Sync Demo
2+
//! This example demonstrates the `SingleLatest` buffer primitive using AimDB's synchronous API.
3+
//!
4+
//! ## What This Demo Shows
5+
//!
6+
//! 1. Building an AimDB instance with a typed record and 2 tap observers (slow and fast)
7+
//! 2. Attaching the database to get a sync handle
8+
//! 3. Creating a producer in sync context
9+
//! 4. Setting values using blocking operations
10+
//! 5. Clean shutdown with detach()
11+
12+
use aimdb_core::{buffer::BufferCfg, AimDbBuilder, Consumer, RuntimeContext};
13+
use aimdb_sync::AimDbBuilderSyncExt; // Extension trait for .attach()
14+
use aimdb_tokio_adapter::{TokioAdapter, TokioRecordRegistrarExt}; // Extension for .buffer()
15+
use std::{sync::Arc, thread, time::Duration};
16+
17+
#[derive(Debug, Clone)]
18+
struct FeatureFlag {
19+
rollout_percent: f32,
20+
}
21+
22+
const KEY: &str = "config.checkout_rollout";
23+
24+
fn main() -> Result<(), Box<dyn std::error::Error>> {
25+
println!("=== AimDB SingleLatest Sync Demo ===\n");
26+
27+
// Step 1: Build the database and attach it for sync API usage
28+
println!("1. Building database and attaching for sync API...");
29+
30+
let adapter = Arc::new(TokioAdapter);
31+
let mut builder = AimDbBuilder::new().runtime(adapter);
32+
33+
builder.configure::<FeatureFlag>(KEY, |reg| {
34+
// Configure a SingleLatest buffer
35+
reg.buffer(BufferCfg::SingleLatest)
36+
// Register 2 taps that observe rollout percentage values
37+
.tap(rollout_observer_fast)
38+
.tap(rollout_observer_slow);
39+
});
40+
41+
// Build happens inside the runtime thread where Tokio context exists
42+
let handle = builder.attach()?;
43+
44+
// Step 2: Create a synchronous producer
45+
println!("2. Creating producer and producing values...");
46+
47+
let producer = handle.producer::<FeatureFlag>(KEY)?;
48+
49+
// Step 3: Produce values
50+
println!("3. Producing rollout percentage values");
51+
52+
for i in 0..5 {
53+
let feature_flag = FeatureFlag {
54+
rollout_percent: i as f32 * 25.0,
55+
};
56+
println!(
57+
" Main: Setting FeatureFlag {}%",
58+
feature_flag.rollout_percent
59+
);
60+
61+
// Use blocking send
62+
producer.set(feature_flag)?;
63+
64+
// Producer publishes new value every second
65+
if i < 4 {
66+
thread::sleep(Duration::from_millis(1000));
67+
}
68+
}
69+
70+
// Give the slow tap time to observe the final update before shutdown. Its read cycle is 2000ms, therefore wait a little longer than that.
71+
thread::sleep(Duration::from_millis(2500));
72+
73+
// Step 4: Clean shutdown
74+
// Detach the handle to gracefully shut down the runtime thread
75+
76+
println!("4. Shutting down...");
77+
78+
handle.detach()?;
79+
80+
println!("Done. The sync producer published rollout updates observed by fast and slow SingleLatest taps");
81+
82+
Ok(())
83+
}
84+
85+
// A fast tap observer
86+
async fn rollout_observer_fast(ctx: RuntimeContext, consumer: Consumer<FeatureFlag>) {
87+
let mut reader = consumer.subscribe();
88+
89+
while let Ok(feature_flag) = reader.recv().await {
90+
ctx.log().info(&format!(
91+
"[fast] rollout: {}%",
92+
feature_flag.rollout_percent
93+
));
94+
}
95+
}
96+
97+
// A slow tap observer
98+
async fn rollout_observer_slow(ctx: RuntimeContext, consumer: Consumer<FeatureFlag>) {
99+
let mut reader = consumer.subscribe();
100+
let time = ctx.time();
101+
102+
while let Ok(feature_flag) = reader.recv().await {
103+
ctx.log().info(&format!(
104+
"[slow] rollout: {}%",
105+
feature_flag.rollout_percent
106+
));
107+
108+
time.sleep_millis(2000).await;
109+
}
110+
}

0 commit comments

Comments
 (0)