Deep EVM #24 : Propagation de contexte en Rust async — Délais, annulation et traçage
Engineering Team
Le problème du contexte
Dans les systèmes async, les opérations traversent de nombreuses tâches et services. Comment propager les délais, les identifiants de requête et les signaux d’annulation à travers cette chaîne ?
En Go, le package context.Context résout ce problème de manière standard. En Rust, il n’y a pas d’équivalent unique — vous devez combiner plusieurs outils.
Délais avec tokio::time::timeout
use tokio::time::{timeout, Duration};
async fn process_with_deadline(data: &Data) -> Result<Output> {
let result = timeout(
Duration::from_secs(5),
heavy_computation(data)
).await;
match result {
Ok(Ok(output)) => Ok(output),
Ok(Err(e)) => Err(e),
Err(_) => Err(Error::Timeout),
}
}
Délais imbriqués
Pour propager un délai à travers les sous-opérations :
async fn pipeline(deadline: Instant) -> Result<()> {
let remaining = deadline - Instant::now();
// Chaque étape utilise le temps restant
timeout(remaining, step_one()).await??;
let remaining = deadline - Instant::now();
timeout(remaining, step_two()).await??;
Ok(())
}
Annulation coopérative avec CancellationToken
use tokio_util::sync::CancellationToken;
let token = CancellationToken::new();
let child_token = token.child_token();
// Tâche de travail
tokio::spawn(async move {
loop {
tokio::select! {
_ = child_token.cancelled() => {
tracing::info!("Tâche annulée proprement");
break;
}
result = do_work() => {
handle(result);
}
}
}
});
// Annuler depuis le parent
token.cancel();
Le CancellationToken supporte les hiérarchies — annuler un parent annule tous les enfants.
Traçage distribué
use tracing::{instrument, info_span, Instrument};
#[instrument(skip(db), fields(user_id = %user_id))]
async fn get_user(db: &Database, user_id: UserId) -> Result<User> {
let user = db.query_user(user_id)
.instrument(info_span!("db_query"))
.await?;
let enriched = enrich_user(user)
.instrument(info_span!("enrich"))
.await?;
Ok(enriched)
}
OpenTelemetry
Pour le traçage inter-services :
use tracing_opentelemetry::OpenTelemetryLayer;
use opentelemetry::sdk::trace::TracerProvider;
fn init_tracing() {
let provider = TracerProvider::builder()
.with_exporter(opentelemetry_jaeger::new_agent_pipeline())
.build();
let telemetry = OpenTelemetryLayer::new(provider.tracer("my-service"));
tracing_subscriber::registry()
.with(telemetry)
.with(tracing_subscriber::fmt::layer())
.init();
}
Pattern complet : contexte de requête
Combinez le tout dans un contexte de requête :
struct RequestContext {
request_id: Uuid,
deadline: Instant,
cancellation: CancellationToken,
span: tracing::Span,
}
impl RequestContext {
fn remaining_time(&self) -> Duration {
self.deadline.saturating_duration_since(Instant::now())
}
fn child(&self, name: &str) -> Self {
Self {
request_id: self.request_id,
deadline: self.deadline,
cancellation: self.cancellation.child_token(),
span: info_span!(parent: &self.span, "child", name = name),
}
}
}
Conclusion
La propagation de contexte en Rust async combine timeout pour les délais, CancellationToken pour l’annulation coopérative et tracing pour l’observabilité. Ensemble, ils fournissent la fonctionnalité équivalente au context.Context de Go, avec la sécurité de type de Rust.