مدیریت خطا در Rust غیرهمزمان — از سطح تجربه تا عمق معماری

نویسنده leen 03 Dec 2025 · 13:40

دنیایی که منتظر نمی‌ماند: چرا اصلاً باید چند درخواست را هم‌زمان اجرا کنیم؟

دنیا دیگر منتظر نمی‌ماند.
سرویس‌ها با سرعتی کار می‌کنند که اگر کمی مکث کنیم، نتیجه‌ای که می‌گیریم دیگه بلا استفاده می‌شود. در معماری سیستم‌های امروزی، «هم‌زمانی» یک ویژگی لوکس نیست؛ بلکه یک ضرورت است. و هر زبان برنامه‌نویسی که ادعای حضور در دنیای واقعی دارد، باید روشی سالم، قابل اطمینان و پیش‌بینی‌پذیر برای اجرای چند کار به صورت موازی ارائه دهد.

Rust این مسئله را جدی گرفته:
زبان راست به‌جای اینکه اجازه مطلقا «هر کاری» را بدهد، برنامه‌نویس را مجبور به «پیروی از راه درست» می‌کند.

در ویدیو زیر، روی یک درخواست ساده تمرکز کردیم؛ یک URL، یک پاسخ، یک خطا، یک context. دنیایی قابل فهم.

اما کافی است بخواهیم سه URL را هم‌زمان چک کنیم؛ یا روی هرکدام یک timeout متفاوت بگذاریم؛ یا انتظار داشته باشیم اگر یکی از taskها panic کرد، برنامه‌ی اصلی از متوقف نشود. ناگهان وارد لایه‌ای از واقعیت می‌شویم که خیلی از آموزش‌های اینترنتی آن را نادیده می‌گیرند.

اینجا همان نقطه‌ای است که asynchronous programming از یک «ترفند» تبدیل می‌شود به یک «ابزار معماری».
و هدف این پست همین است: برخورد جدی با واقعیت.

ما یک پروژه‌ی مستقل می‌سازیم: async-joinset-timeoutتا ببینیم چطور سه درخواست HTTP را هم‌زمان می‌فرستیم، روی آن‌ها timeout می‌گذاریم، و نتیجه را به شکلی واضح و تمیز بررسی می‌کنیم.

این دقیقاً همان‌جایی اسن که تفاوت میان یک برنامه‌نویس تازه‌کار و یک مهندس نرم‌افزار مشخص می‌شود.


دنیای واقعی جایی برای «یک‌درخواست» ندارد: ساخت پروژه و تحلیل هسته‌ی برنامه

ویدیو یوتیوب این پروژه کوچیک:

شروع کار ساده است: یک پروژه‌ی تازه، و کمی وابستگی.

cargo new async-joinset-timeout
cd async-joinset-timeout

و این هم Cargo.toml:

[package]
name = "async-joinset-timeout"
version = "0.1.0"
edition = "2021"

[dependencies]
anyhow = "1"
reqwest = { version = "0.11", default-features = false, features = ["rustls-tls"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }

سه کتابخانه؛ سه ستون اصلی برنامه:

  • tokio: قلب تپنده async runtime
  • reqwest: درخواست HTTP با rustls
  • anyhow: مدیریت خطای انسانی و قابل خواندن

اما اصل ماجرا در main.rs اتفاق می‌افتد؛ جایی که یک JoinSet می‌سازیم، سه درخواست را هم‌زمان اجرا می‌کنیم، روی آن‌ها timeout قرار می‌دهیم، و نتیجه را بر اساس چهار وضعیت دسته‌بندی می‌کنیم:

  • موفق
  • timeout
  • خطای شبکه
  • panic

همه کد برنامه به صورت یکجا:

use anyhow::{Context, Result};
use reqwest::Client;
use tokio::{
    task::JoinSet,
    time::{timeout, Duration},
};

#[tokio::main]
async fn main() -> Result<()> {
    println!("🚀 Async JoinSet + Timeout demo");

    let urls = vec![
        "https://httpbin.org/status/200".to_string(),
        "https://httpbin.org/delay/5".to_string(),
        "https://httpbin.org/status/503".to_string(),
    ];

    let client = Client::builder()
        .user_agent("drunkleen-joinset-demo/0.1")
        .build()
        .context("failed to build HTTP client")?;

    let mut set = JoinSet::new();

    for url in urls {
        let client = client.clone();
        let url_clone = url.clone();

        set.spawn(async move {
            let result = timeout(Duration::from_secs(3), async {
                let response = client
                    .get(&url_clone)
                    .send()
                    .await
                    .with_context(|| format!("request to {} failed", &url_clone))?;

                Ok::<u16, anyhow::Error>(response.status().as_u16())
            })
            .await
            .context("request timed out")??;

            Ok::<(String, u16), anyhow::Error>((url_clone, result))
        });
    }

    while let Some(join_result) = set.join_next().await {
        match join_result {
            Ok(inner_result) => match inner_result {
                Ok((url, status)) => {
                    println!("✅ {url} -> HTTP {status}");
                }
                Err(err) => {
                    eprintln!("❌ worker error:");
                    eprintln!("{err:#}");
                }
            },
            Err(join_err) => {
                eprintln!("💥 task panicked:");
                eprintln!("{join_err:#}");
            }
        }
    }

    println!("✨ Done.");
    Ok(())
}

در نگاه اول شاید ساده به‌نظر برسد، اما این برنامه ترکیبی از چند مفهوم بسیار مهم است:

  • parallel task execution
  • per-task timeout
  • structured error management
  • panic isolation
  • predictable behavior under load

نه فقط زمینه‌ای که یک back-end کار حرفه‌ای نیاز دارد، بلکه باید با پوست‌واستخوان تجربه‌اش کند.


زیر پوست معماری: معنای واقعی خطا در دنیای async

نکته‌ی جذاب ماجرا این است که در دنیای async چیزی به اسم فقط «یک نوع خطا» وجود ندارد. خطا مثل یک درخت است:

  • یک شاخه‌ی آن timeout است
  • شاخه‌ی دیگرش خطای شبکه است
  • شاخه‌ی بعدی panic در یک task
  • و شاخه‌ی آخر، انتخاب نحوه‌ی نمایش خطا به انسان

چیزی که Rust انجام می‌دهد، فقط اجرای هم‌زمان نیست؛ بلکه «معنابخشیدن» به خطاهاست.

مثلاً:

اگر یک درخواست بیش از سه ثانیه طول بکشد:

Error: request timed out

و اگر تسک panic کند:

💥 task panicked

این تفکیک، همان جایی است که سیستم قابل مشاهده (observable) می‌شود. سیستمی که خطایش را توضیح می‌دهد، نه اینکه آن را مخفی کند.

در واقع، در پروژه‌های واقعی، معماری‌ای که خطاها را «به شکل انسانی و قابل فهم» دسته‌بندی کند، ارزشمندتر از معماری‌ای است که صرفاً فقط سریع باشد. سرعت مهم است؛ اما معنا مهم‌تر است.

اینجا دقیقاً همان فلسفه‌ای است که Rust و async را کنار هم قرار می‌دهد: سرعت خام + ساختار انسانی.

و این پروژه، نسخه‌ی کوچک‌شده‌ی یک جهان بزرگ است؛ یک نمونه‌ی فشرده از سیستم‌های واقعی که باید زیر فشار کار کنند، و در عین حال تابع منطق باشند.


در نهایت، این پست یک مقدمه نیست بلکه یک نقشه‌ی ذهنی است برای هر کسی که می‌خواهد از «نوشتن کد async» عبور کند و به «فهمیدن async» برسد.

قانون ساده است:

وقتی چند کار را هم‌زمان انجام می‌دهی، فقط دنبال سرعت نباش—دنبال معنا هم باش. و Rust دقیقاً همین را از ما می‌خواهد.

دیدگاه‌ها

اولین نفری باشید که دیدگاه می‌گذارد.

دیدگاه بگذارید