ADR-003: Asynchronous Execution Model (Boost.Asio + Boost.Cobalt Coroutines)
Status |
Accepted |
|---|---|
Date |
2026-07-24 |
Author(s) |
Context
The Station must carry out many operations that take an unpredictable amount of time without blocking the guest- or operator-facing UI: communicating with several independently timed Pods at once, and driving a recipe as a sequence of dispensing actions, each waiting on its own Pod’s acknowledgement and, for longer-running actions, a later completion event (Protocol). Dispensing is a safety- and food-safety-relevant action (Quality Goals). Therefore, the concurrency model must make it easy to reason about which operations can run simultaneously and what state they can affect, rather than opening the door to data races across freely spawned threads. === Decision Drivers
-
Safety & State Simplicity: Dispensing is a safety-critical domain; the architecture must prevent data races and complex mutex synchronization around shared domain and UI state.
-
Sequential Readability: Multi-step asynchronous protocols (request → acknowledgement → delayed completion event) should be readable as straight-line code rather than fragmented callback chains.
-
Technology Exploration: As an experimental hobby project, a primary driver is evaluating modern C++20 coroutine ecosystems—specifically the recently introduced Boost.Cobalt library—in a realistic architectural setting. == Decision We will run Pod communication and recipe execution as coroutines on a single shared Boost.Asio execution context, using Boost.Cobalt for coroutine-friendly building blocks such as awaitable results and channels. == Alternatives Considered === Callback-based asynchronous I/O Boost.Asio can be used directly with completion-handler callbacks, without a coroutine layer on top. Rejected: the Station’s communication patterns—a request followed by an acknowledgement and, for some commands, a later completion event—are naturally sequential. Expressing them as chained callbacks was judged significantly harder to read, maintain, and debug than expressing the same sequence as a coroutine that cleanly suspends and resumes at each step. === A thread per Pod Each connected Pod could be handled on its own dedicated OS thread, communicating with the rest of the Station through explicit thread synchronization (mutexes, condition variables). Rejected: this would require synchronizing access to shared domain and UI state across multiple threads. In a system where reasoning correctly about concurrency is a food-safety concern, introducing multi-threaded state access adds a class of concurrency bugs (deadlocks, race conditions) that a single shared execution context avoids entirely. == Consequences
-
Good: the multi-step communication patterns used with Pods can be written as clear, straight-line coroutine code that suspends and resumes at each step, completely avoiding "callback hell."
-
Good: a single shared execution context means the domain and UI state that coroutines touch is never accessed concurrently, removing an entire class of synchronization bugs by design.
-
Good: every pending exchange carries its own timeout, so an unresponsive Pod suspends only its specific coroutine and cannot block the execution context or affect other running Pods.
-
Bad: Boost.Cobalt is a comparatively young and less widely adopted part of the Boost ecosystem than Boost.Asio itself, meaning fewer best-practice examples and community resources are available.
-
Bad: debugging a suspended C++20 coroutine is currently less straightforward in standard IDEs than stepping through a traditional synchronous call stack.
-
Bad: cooperative multitasking on a single execution context requires strict discipline; any accidental synchronous blocking call (e.g.,
std::this_thread::sleep_for) or heavy CPU-bound computation will stall the entire context and delay unrelated, concurrent coroutines.