Summary
aimdb-sync is the blocking/synchronous bridge that lets plain fn main() code talk to the async AimDB without #[tokio::main]. Today it is hard-wired to std + tokio (OS thread hosting a tokio runtime, bridged with tokio/std channels), so it cannot run in no_std.
The goal is to remove the hard tokio/std dependency so the sync bridge can run on a no_std runtime — enabling a future FreeRTOS adapter (and legacy/sync/C-interop tasks) to use AimDB from a blocking context. Embassy itself gains little (it's already async); the value is forward-looking.
Decisions (locked)
- Scope now: Phases 1–2 on std. Make the crate no_std-ready and collapse the bridge onto the sync core API + a
block_on seam, validated on tokio. Phases 3–4 (Embassy / FreeRTOS backends) are deferred.
- Single crate, feature-gated. Keep one
aimdb-sync; the std/tokio bridge sits behind a default-on std feature; a future embassy feature adds the no_std backend. (No aimdb-sync-core split.)
std keeps hosting the executor. On the std feature the runtime thread + tokio::runtime::Handle stay (they drive runner.run() and back block_on). Only the future no_std backend delegates executor hosting to its adapter. This preserves "no #[tokio::main] needed" for current users.
Key insight — most of the bridge is already unnecessary
The core API is already synchronous on both ends:
AimDb::produce() / AimDb::subscribe() are sync (aimdb-core/src/builder.rs:946, :963); Producer::try_produce (non-blocking) just landed.
- The buffer reader exposes both async
recv() and sync try_recv() (aimdb-core/src/buffer/traits.rs:179, :189).
| Operation |
Status in core |
Bridge needed? |
| Produce / non-blocking produce |
db.produce / try_produce sync |
No — call directly |
| Non-blocking consume |
reader.try_recv() sync |
No — poll directly |
| Blocking consume (wait for next value) |
only reader.recv().await |
Yes — the only runtime-specific piece |
So the work is simplification, not re-architecture: lean on the sync API and isolate one small seam.
The block_on seam (concrete design)
aimdb-executor::RuntimeOps has no block_on/spawn — only name/now_nanos/unix_time/sleep/log (aimdb-executor/src/ops.rs:51). The seam is therefore net-new, but small, and lives in aimdb-sync (not widened into RuntimeOps).
- In the simplified model
aimdb-sync needs only block_on, never spawn: producer + non-blocking paths call the sync API directly; blocking-consume blocks the calling thread.
- Because the
std/embassy features are mutually exclusive per build, the waiter is a concrete #[cfg]-selected type, not a trait object — sidestepping object-safety/generic-dispatch entirely.
#[cfg(feature = "std")] → a waiter holding the tokio::runtime::Handle:
get() → handle.block_on(reader.recv())
get_with_timeout(d) → handle.block_on(async { tokio::time::timeout(d, reader.recv()).await }) (timed variant must go through the runtime so the tokio timer is in context — this is why it can't be a naive futures::block_on)
#[cfg(feature = "embassy")] (Phase 3) → embassy_futures::block_on + embassy_time.
- This deletes the per-consumer forwarder task and the
std::sync::mpsc::sync_channel: SyncConsumer holds the Box<dyn BufferReader<T> + Send> directly and blocks on it per get(). (Handle::block_on is called from the user's sync thread, never from inside the runtime, so this is valid.)
SyncProducer becomes a thin wrapper over db.produce / Producer::try_produce — no channel, no oneshot, no forwarder task, no runtime handle.
How the bridge works today (for reference)
User threads (sync) → channels → runtime thread (tokio async) → AimDb
- Runtime thread:
attach() spawns std::thread (handle.rs:151) + tokio::runtime::Runtime (:155), builds in block_on (:173), drives runner.run() (:192), keeps a Handle (:130).
- Producer:
set() → runtime_handle.block_on (producer.rs:78) → tokio::mpsc + tokio::time::timeout (:81) → task (handle.rs:399) calls sync db.produce() (:402) → oneshot back.
- Consumer: task (
handle.rs:462) db.subscribe + reader.recv().await → std::sync::mpsc::sync_channel (:454); get() blocks on recv[_timeout] (consumer.rs:102/:143).
Phase 1 — crate hygiene (no behavior change)
Phase 2 — collapse the bridge onto the sync API + seam (std)
Phases 3–4 — deferred (no_std backends)
Out of scope for this issue; tracked for when a no_std consumer is real:
- Phase 3 (Embassy backend) and Phase 4 (FreeRTOS backend) add the
#[cfg(feature = "embassy")] waiter etc.
- Open decisions parked here (don't need resolving for Phases 1–2):
- Blocking model for no_std: pure-Rust
embassy_futures::block_on (may busy-idle on bare metal) vs RTOS-native queue-wait.
- First no_std target: Embassy-first (CI-friendly) vs wait for FreeRTOS (the real motivation).
- Hard blocker: no FreeRTOS adapter exists yet — runtimes today are tokio/embassy/wasm.
Caveats no amount of API-sync removes
- The DB's async stages (sources, transforms, taps, connectors) still need an executor driving
runner.run(). On std that's the existing thread; on no_std it's the adapter's job. A buffer can't substitute for this.
- Blocking consume still needs a real park/wake primitive per runtime; the seam hides it, it doesn't eliminate it.
Alternatives considered
Dedicated async/sync "bridge buffer" type — rejected. Buffers are per-adapter (see #115): a new kind means a BufferCfg variant + a fresh impl in tokio/embassy/wasm/FreeRTOS, for a feature whose hard kernel is a small block_on. And the seam it would wrap has already dissolved given sync produce/try_produce/try_recv. It would relocate the executor/park caveats, not remove them.
Acceptance criteria (Phases 1–2)
aimdb-sync compiles as #![no_std] + alloc; today's std/tokio bridge is behind a default-on std feature; current users + sync-api-demo see no change.
SyncProducer calls the sync core API directly (no tokio channels / forwarder task).
SyncConsumer blocking ops go through the concrete block_on seam (Handle::block_on(reader.recv())); non-blocking try_get uses reader.try_recv(); the std::sync::mpsc forwarder is gone.
- Existing tests green + new blocking/timeout tests pass;
make check clean.
References
- Crate:
aimdb-sync/src/{handle,producer,consumer,lib}.rs, aimdb-sync/Cargo.toml
- Already sync in core:
aimdb-core/src/builder.rs:946/:963; aimdb-core/src/buffer/traits.rs:189; Producer::try_produce
- Seam reality:
aimdb-executor/src/ops.rs:51 (RuntimeOps has no block_on/spawn); aimdb-embassy-adapter/src/runtime.rs (no_std reference for Phase 3)
aimdb-core/src/lib.rs:16 — already no_std + alloc
Summary
aimdb-syncis the blocking/synchronous bridge that lets plainfn main()code talk to the async AimDB without#[tokio::main]. Today it is hard-wired to std + tokio (OS thread hosting a tokio runtime, bridged with tokio/std channels), so it cannot run inno_std.The goal is to remove the hard tokio/std dependency so the sync bridge can run on a
no_stdruntime — enabling a future FreeRTOS adapter (and legacy/sync/C-interop tasks) to use AimDB from a blocking context. Embassy itself gains little (it's already async); the value is forward-looking.Decisions (locked)
block_onseam, validated on tokio. Phases 3–4 (Embassy / FreeRTOS backends) are deferred.aimdb-sync; the std/tokio bridge sits behind a default-onstdfeature; a futureembassyfeature adds the no_std backend. (Noaimdb-sync-coresplit.)stdkeeps hosting the executor. On thestdfeature the runtime thread +tokio::runtime::Handlestay (they driverunner.run()and backblock_on). Only the future no_std backend delegates executor hosting to its adapter. This preserves "no#[tokio::main]needed" for current users.Key insight — most of the bridge is already unnecessary
The core API is already synchronous on both ends:
AimDb::produce()/AimDb::subscribe()are sync (aimdb-core/src/builder.rs:946,:963);Producer::try_produce(non-blocking) just landed.recv()and synctry_recv()(aimdb-core/src/buffer/traits.rs:179,:189).db.produce/try_producesyncreader.try_recv()syncreader.recv().awaitSo the work is simplification, not re-architecture: lean on the sync API and isolate one small seam.
The
block_onseam (concrete design)aimdb-executor::RuntimeOpshas noblock_on/spawn— onlyname/now_nanos/unix_time/sleep/log(aimdb-executor/src/ops.rs:51). The seam is therefore net-new, but small, and lives inaimdb-sync(not widened intoRuntimeOps).aimdb-syncneeds onlyblock_on, neverspawn: producer + non-blocking paths call the sync API directly; blocking-consume blocks the calling thread.std/embassyfeatures are mutually exclusive per build, the waiter is a concrete#[cfg]-selected type, not a trait object — sidestepping object-safety/generic-dispatch entirely.#[cfg(feature = "std")]→ a waiter holding thetokio::runtime::Handle:get()→handle.block_on(reader.recv())get_with_timeout(d)→handle.block_on(async { tokio::time::timeout(d, reader.recv()).await })(timed variant must go through the runtime so the tokio timer is in context — this is why it can't be a naivefutures::block_on)#[cfg(feature = "embassy")](Phase 3) →embassy_futures::block_on+embassy_time.std::sync::mpsc::sync_channel:SyncConsumerholds theBox<dyn BufferReader<T> + Send>directly and blocks on it perget(). (Handle::block_onis called from the user's sync thread, never from inside the runtime, so this is valid.)SyncProducerbecomes a thin wrapper overdb.produce/Producer::try_produce— no channel, no oneshot, no forwarder task, no runtime handle.How the bridge works today (for reference)
attach()spawnsstd::thread(handle.rs:151) +tokio::runtime::Runtime(:155), builds inblock_on(:173), drivesrunner.run()(:192), keeps aHandle(:130).set()→runtime_handle.block_on(producer.rs:78) →tokio::mpsc+tokio::time::timeout(:81) → task (handle.rs:399) calls syncdb.produce()(:402) →oneshotback.handle.rs:462)db.subscribe+reader.recv().await→std::sync::mpsc::sync_channel(:454);get()blocks onrecv[_timeout](consumer.rs:102/:143).Phase 1 — crate hygiene (no behavior change)
#![cfg_attr(not(feature = "std"), no_std)]+extern crate alloconaimdb-sync.stdfeature; move the tokio dep,aimdb-tokio-adapterdep, and all current code behind it.std::{Arc, Duration}etc. foralloc/coreequivalents where they're not behind thestdfeature; replaceeprintln!with the crate's log facade.make check+cargo build -p aimdb-sync+sync-api-demounchanged.Phase 2 — collapse the bridge onto the sync API + seam (std)
block_onseam as a concrete#[cfg(feature = "std")]waiter holding theHandle.SyncProducerto calldb.produce/Producer::try_producedirectly (droptokio::mpsc/oneshot, the forwarder task, and the stored runtime handle for produce).SyncConsumerto hold the reader and use the seam forget()/get_with_timeout(); drop the forwarder task andstd::sync::mpsc.try_get()→reader.try_recv().detach/Droplifecycle (still hosts the executor); re-auditunsafe impl Send/Sync for AimDbHandle(handle.rs:653) against the smaller surface.aimdb-syncsuite +sync-api-demogreen; add a test proving blockingget()wakes on a produce andget_with_timeouttimes out.Phases 3–4 — deferred (no_std backends)
Out of scope for this issue; tracked for when a no_std consumer is real:
#[cfg(feature = "embassy")]waiter etc.embassy_futures::block_on(may busy-idle on bare metal) vs RTOS-native queue-wait.Caveats no amount of API-sync removes
runner.run(). Onstdthat's the existing thread; on no_std it's the adapter's job. A buffer can't substitute for this.Alternatives considered
Dedicated async/sync "bridge buffer" type — rejected. Buffers are per-adapter (see #115): a new kind means a
BufferCfgvariant + a fresh impl in tokio/embassy/wasm/FreeRTOS, for a feature whose hard kernel is a smallblock_on. And the seam it would wrap has already dissolved given syncproduce/try_produce/try_recv. It would relocate the executor/park caveats, not remove them.Acceptance criteria (Phases 1–2)
aimdb-synccompiles as#![no_std]+alloc; today's std/tokio bridge is behind a default-onstdfeature; current users +sync-api-demosee no change.SyncProducercalls the sync core API directly (no tokio channels / forwarder task).SyncConsumerblocking ops go through the concreteblock_onseam (Handle::block_on(reader.recv())); non-blockingtry_getusesreader.try_recv(); thestd::sync::mpscforwarder is gone.make checkclean.References
aimdb-sync/src/{handle,producer,consumer,lib}.rs,aimdb-sync/Cargo.tomlaimdb-core/src/builder.rs:946/:963;aimdb-core/src/buffer/traits.rs:189;Producer::try_produceaimdb-executor/src/ops.rs:51(RuntimeOpshas noblock_on/spawn);aimdb-embassy-adapter/src/runtime.rs(no_std reference for Phase 3)aimdb-core/src/lib.rs:16— alreadyno_std + alloc