29ffb42898
---ci--- project: atelier phase: 0 milestone: v0.4 status: complete requirements: covered: [ATELIER-92, ATELIER-93, ATELIER-94, ATELIER-95, ATELIER-96, ATELIER-97, ATELIER-98, ATELIER-99, ATELIER-100, ATELIER-101, ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105, ATELIER-106, ATELIER-107, ATELIER-108, ATELIER-109, ATELIER-110, ATELIER-111, ATELIER-112, ATELIER-113, ATELIER-114, ATELIER-115, ATELIER-116, ATELIER-117] partial: [] ---/ci---
6.8 KiB
6.8 KiB
Rust Async — Derived Application
Applies Atelier's domain principles to Rust async specifically. Derives from
domains/docs; introduces no new P-rules (D-063). Seelanguages/rust.mdfor the language first-principles stub.
tokio and the Async Runtime (Concurrency P5 Lock Minimization, C6 Composability)
tokiois the default async runtime:#[tokio::main]for the entry;tokio::spawnfor a task. The runtime owns the reactor, the I/O driver, and the timer.tokio::spawnreturns aJoinHandlelikestd::thread::spawn: a droppedJoinHandledetaches (the task keeps running);awaitthe handle to join. Prefer await to detach.tokio::task::JoinSetfor structured concurrency: a set of tasks awaited together; on drop, all remaining tasks are cancelled. Mirrorserrgroup/TaskGroupsemantics.runtimefeatures are explicit:tokio = { version = "1", features = ["full"] }for a binary;["rt", "rt-multi-thread", "macros"]for a library. Pullingfullinto a library bloats downstream.
#[tokio::main]
async fn main() {
let mut set = tokio::task::JoinSet::new();
for id in ["a", "b", "c"] {
set.spawn(fetch_user(id.to_string()));
}
while let Some(res) = set.join_next().await {
match res {
Ok(Ok(u)) => println!("{}", u.name),
Ok(Err(e)) => eprintln!("err: {e}"),
Err(join_err) => eprintln!("panic: {join_err}"),
}
}
}
Async Traits (Concurrency P7 Cancellation Support, C6 Composability)
async fnin traits stabilized in Rust 1.75:trait Repo { async fn get(&self, id: &str) -> Result<User, Error>; }. Noasync-traitcrate needed for new code on recent toolchains.Box<dyn Trait>with async methods needsdyn-compatibility: the returned future isPin<Box<dyn Future>>; the compiler boxes it. For hot paths, use generics (impl Trait) overdyn.async-traitcrate for older toolchains: macro that desugars to aPin<Box<dyn Future>>. Migrate to nativeasync fn in traitwhen the toolchain allows.Sendbounds on async traits for cross-thread spawn:trait Repo: Send { async fn get(&self, id: &str) -> Result<User, Error>; }— the returned future must beSendto spawn on a multi-thread runtime.
trait UserRepo: Send + Sync {
async fn get(&self, id: &str) -> Result<User, Error>;
}
struct PgRepo { pool: PgPool }
impl UserRepo for PgRepo {
async fn get(&self, id: &str) -> Result<User, Error> {
sqlx::query_as::<_, User>("SELECT * FROM users WHERE id = $1")
.bind(id).fetch_one(&self.pool).await.map_err(Error::from)
}
}
Cancellation (Concurrency P7 Cancellation Support, Concurrency P8 Timeout Discipline)
- Cancellation is cooperative via dropping the future:
tokio::select!drops the unselected branch, cancelling it. A dropped future stops at its next.awaitpoint. tokio::time::timeoutfor a deadline:timeout(Duration::from_secs(5), op).awaitreturnsOk(Ok(v))on success,Ok(Err(e))on inner error,Err(Elapsed)on timeout. Every externalawaitraces against a deadline (Concurrency P8).tokio::select!for cancel-aware waits:select! { res = op => res, _ = cancel => return Err(Cancelled), }. The unselected branch is dropped, cancelling it.- Cancellation is not atomic: a future dropped mid-
awaitmay have partial state.Dropruns on cancellation; clean up there (e.g., rollback a transaction). - Applies
concurrency/P7: cancellation is a first-class signal; the runtime propagates it via drop. NoCancelledErrorto catch — the future is gone.
use tokio::time::timeout;
use std::time::Duration;
async fn fetch_with_timeout(url: &str) -> Result<Response, Error> {
match timeout(Duration::from_secs(5), fetch(url)).await {
Ok(Ok(r)) => Ok(r),
Ok(Err(e)) => Err(e.into()),
Err(_elapsed) => Err(Error::Timeout),
}
}
async fn cancellable(op: impl Future<Output=()>, mut cancel: tokio::sync::oneshot::Receiver<()>) {
tokio::select! {
_ = op => {},
_ = &mut cancel => println!("cancelled"),
}
}
Pin and Self-Referential Futures (Concurrency P5 Lock Minimization, C1 Correctness)
async fnreturns aFuturethat is often self-referential: the generated state machine may hold a borrow into its own stack. Such a future must bePinned to move safely.Pin<Box<T>>to box and pin:Box::pin(async { ... })returns aPin<Box<dyn Future>>. The cost is a heap alloc; the win isSend/dyn-compatibility.Pin<&mut T>for in-place polling:Pin::new(&mut fut)pins a stack future; the borrow checker prevents moving it. Use for stack-allocated futures inselect!.- Do not
unsafeunpin:Pin::get_unchecked_mutopts out of the pin guarantees. Application code never needs it; library code uses it forpollimplementations.
use std::pin::Pin;
async fn boxed() -> Pin<Box<dyn std::future::Future<Output = ()> + Send>> {
Box::pin(async {
// self-referential state machine is safe to move once pinned
})
}
Bounded Channels and Backpressure (Concurrency P9 Bounded Queues)
tokio::sync::mpsc::channel(N)is bounded:send().awaitblocks when full (backpressure, Concurrency P9). Unboundedunbounded_channel()lets the producer run ahead and OOM.tokio::sync::mpsc::Sender::try_sendfor non-blocking send: returnsErr(TrySendError::Full(v))when full; the caller decides to drop, log, or back off. A bounded queue +try_sendis the backpressure-aware pattern.tokio::sync::broadcastfor fan-out: multiple receivers each get a copy; a slow receiver misses (lag). Use for telemetry, not for commands.- Applies
messaging/queues: a bounded tokio channel is an in-process broker — bounded buffer, backpressure, at-most-once handoff. The same semantics apply; the broker is local.
use tokio::sync::mpsc;
async fn producer(tx: mpsc::Sender<Job>) {
for j in jobs() {
if tx.send(j).await.is_err() { return; } // receiver dropped
}
}
async fn consumer(rx: mpsc::Receiver<Job>) {
while let Some(j) = rx.recv().await {
process(j).await;
}
}
let (tx, rx) = mpsc::channel::<Job>(16); // bounded: backpressure
Cross-References
domains/concurrency/patterns.md— the cancellation/timeout/semaphore patterns applied here.domains/concurrency/first-principles.md— Concurrency P5 Lock Minimization, P7 Cancellation Support, P8 Timeout Discipline, P9 Bounded Queues.domains/messaging/delivery-semantics.md— at-most-once vs at-least-once framing for async retry/cancel (IDEATE-40).languages/rs-ownership.md—Send/Syncbounds on futures build on the ownership model here.languages/rs-tooling.md—tokiofeature flags and thecargobuild profiles detailed there.languages/rs-testing.md—#[tokio::test]and async test patterns.