Background
Spawn is currently composed into the Runtime bundle trait:
pub trait Runtime: RuntimeAdapter + TimeOps + Logger + Spawn {}
This propagates R: Spawn through the entire type system — AimDb<R>, RuntimeContext<R>, Producer<T, R>, Consumer<T, R>, TypedRecord<T, R> — and forces each runtime adapter to implement dynamic task spawning. On Embassy this requires a non-trivial workaround: every future is heap-allocated, type-erased, and fed into a compile-time-fixed task pool with an unsafe { Pin::new_unchecked } block. On WASM it forces unsafe impl Send/Sync.
Audit finding
All spawn() calls in aimdb occur inside a single phase: database.build(). No task is ever spawned after initialization completes. Every spawn count is determined by database configuration — not by runtime events.
| Site |
File |
Count |
| Producer service tasks |
aimdb-core/src/typed_record.rs |
one per .source() |
| Consumer tasks |
aimdb-core/src/typed_record.rs |
one per .tap() |
| Transform forwarder tasks |
aimdb-core/src/transform/join.rs |
one per join input |
| Join transform tasks |
aimdb-core/src/typed_record.rs |
one per .transform_join() |
| Start handlers |
aimdb-core/src/builder.rs |
one per .on_start() |
The set of futures to run is fully known by the time build() is called. This is exactly the use case FuturesUnordered is designed for.
Proposed change
database.build() collects futures instead of spawning them. A new database.run() drives them:
let db = Database::build(config)?;
let handle = db.handle();
db.run().await; // drives all internal futures, blocks until shutdown
Internally, run() uses FuturesUnordered<Pin<Box<dyn Future<Output = ()>>>> — the same boxing that already happens today in both the Tokio task allocator and the Embassy adapter, so there is no regression in allocation overhead.
Impact
- Removes
Spawn from the Runtime bundle trait — RuntimeAdapter + TimeOps + Logger is sufficient for all three runtimes
- Deletes the Embassy task-pool workaround —
generic_task_runner, BoxedFuture, TASK_POOL_SIZE feature flags, and unsafe { Pin::new_unchecked } all go away
- Removes
unsafe impl Send/Sync from EmbassyAdapter and WasmAdapter — these exist solely because Spawn requires F: Send + 'static; re-evaluate remaining unsafe impls on Producer/Consumer in typed_api.rs as part of this work
- Uniform behaviour across all runtimes — no adapter-specific workarounds
Work items
Breaking changes
database.build() no longer starts internal tasks — callers must call db.run().await
Spawn is removed from the Runtime supertrait — custom adapter implementations no longer need to implement it
EmbassyAdapter feature flags embassy-task-pool-8/16/32 are removed
Background
Spawnis currently composed into theRuntimebundle trait:This propagates
R: Spawnthrough the entire type system —AimDb<R>,RuntimeContext<R>,Producer<T, R>,Consumer<T, R>,TypedRecord<T, R>— and forces each runtime adapter to implement dynamic task spawning. On Embassy this requires a non-trivial workaround: every future is heap-allocated, type-erased, and fed into a compile-time-fixed task pool with anunsafe { Pin::new_unchecked }block. On WASM it forcesunsafe impl Send/Sync.Audit finding
All
spawn()calls in aimdb occur inside a single phase:database.build(). No task is ever spawned after initialization completes. Every spawn count is determined by database configuration — not by runtime events.aimdb-core/src/typed_record.rs.source()aimdb-core/src/typed_record.rs.tap()aimdb-core/src/transform/join.rsaimdb-core/src/typed_record.rs.transform_join()aimdb-core/src/builder.rs.on_start()The set of futures to run is fully known by the time
build()is called. This is exactly the use caseFuturesUnorderedis designed for.Proposed change
database.build()collects futures instead of spawning them. A newdatabase.run()drives them:Internally,
run()usesFuturesUnordered<Pin<Box<dyn Future<Output = ()>>>>— the same boxing that already happens today in both the Tokio task allocator and the Embassy adapter, so there is no regression in allocation overhead.Impact
Spawnfrom theRuntimebundle trait —RuntimeAdapter + TimeOps + Loggeris sufficient for all three runtimesgeneric_task_runner,BoxedFuture,TASK_POOL_SIZEfeature flags, andunsafe { Pin::new_unchecked }all go awayunsafe impl Send/SyncfromEmbassyAdapterandWasmAdapter— these exist solely becauseSpawnrequiresF: Send + 'static; re-evaluate remainingunsafe impls onProducer/Consumerintyped_api.rsas part of this workWork items
Spawnfrom theRuntimesupertrait inaimdb-executor/src/lib.rsSpawnimpl fromEmbassyAdapter(aimdb-embassy-adapter/src/runtime.rs) and all associated pool machinerySpawnimpl fromWasmAdapterdatabase.build()to collect futures into aVecinstead of spawning themdatabase.run()driving collected futures viaFuturesUnorderedR: Spawnbounds throughoutaimdb-core(typed_api.rs,typed_record.rs,context.rs,database.rs)unsafe impl Send/SynconProducer<T, R>andConsumer<T, R>— remove if no longer neededdb.run().awaitBreaking changes
database.build()no longer starts internal tasks — callers must calldb.run().awaitSpawnis removed from theRuntimesupertrait — custom adapter implementations no longer need to implement itEmbassyAdapterfeature flagsembassy-task-pool-8/16/32are removed