Aller au contenu principal
IngénierieMar 28, 2026

Deep EVM #24 : Propagation de contexte en Rust async — Délais, annulation et traçage

OS
Open Soft Team

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.