Skip to content

Make aimdb-sync no_std-capable by removing the hard tokio/std dependency #46

Description

@lxsaah

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().awaitstd::sync::mpsc::sync_channel (:454); get() blocks on recv[_timeout] (consumer.rs:102/:143).

Phase 1 — crate hygiene (no behavior change)

  • #![cfg_attr(not(feature = "std"), no_std)] + extern crate alloc on aimdb-sync.
  • Add a default-on std feature; move the tokio dep, aimdb-tokio-adapter dep, and all current code behind it.
  • Swap std::{Arc, Duration} etc. for alloc/core equivalents where they're not behind the std feature; replace eprintln! with the crate's log facade.
  • make check + cargo build -p aimdb-sync + sync-api-demo unchanged.

Phase 2 — collapse the bridge onto the sync API + seam (std)

  • Define the block_on seam as a concrete #[cfg(feature = "std")] waiter holding the Handle.
  • Rewrite SyncProducer to call db.produce / Producer::try_produce directly (drop tokio::mpsc/oneshot, the forwarder task, and the stored runtime handle for produce).
  • Rewrite SyncConsumer to hold the reader and use the seam for get() / get_with_timeout(); drop the forwarder task and std::sync::mpsc. try_get()reader.try_recv().
  • Keep the runtime thread + handle + detach/Drop lifecycle (still hosts the executor); re-audit unsafe impl Send/Sync for AimDbHandle (handle.rs:653) against the smaller surface.
  • Tests: existing aimdb-sync suite + sync-api-demo green; add a test proving blocking get() wakes on a produce and get_with_timeout times out.

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)

  1. 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.
  2. SyncProducer calls the sync core API directly (no tokio channels / forwarder task).
  3. 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.
  4. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions