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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions