رفتن به متن راهنما
همه‌ی راهنماها

مدیریت خطا

استثناهای خودتان را از تصمیم‌های Reqtide تشخیص دهید. کد دلیل و گزارش مشکل را بخوانید و ببینید حالت تفصیلی همان اطلاعات را چگونه به شکل داده برمی‌گرداند.

برنامه Reqtide PHP 0.1.0آخرین بررسی زمان خواندن 9 دقیقه

در این صفحه

همهٔ بلوک‌های کد این صفحه از examples/07-error-handling.php (opens in a new tab) آمده‌اند. ابتدا مفهوم‌ها را می‌گوییم، بعد کد را نشان می‌دهیم.

دو نوع شکست

نوعچه چیزی دریافت می‌کنیدمثال
شکست خودتاناستثنای خودتان، بدون تغییر و با همان کلاس و پیام. Reqtide هیچ‌وقت آن را در استثنای دیگری نمی‌پیچد.مهلت upstream تمام شد
تصمیم Reqtideاستثنایی با نوع مشخص که ProblemCarrier و ReqtideException را پیاده می‌کندمحدودیت نرخ فراخوانی را رد کرد، مدار باز است یا مهلت کل گذشته است
دو نوع شکست و آنچه هر کدام به شما می‌دهند.

گروه سوم اشتباه پیکربندی است. این اشتباه InvalidConfigurationException را پرتاب می‌کند، که آن هم یک ProblemCarrier است، پیش از آنکه چیزی ارسال شود.

گام ۱: پایپ‌لاینی بسازید که رد می‌کند

این پایپ‌لاین در هر ساعت یک فراخوانی را می‌پذیرد. فراخوانی اول انجام می‌شود و فراخوانی دوم رد می‌شود.

PHP
$base = static fn(): PipelineBuilder => PipelineBuilder::profile('orders', 'v1')
    ->partition('shop', 'prod', 'orders-api')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative);

// A pipeline that admits exactly one call per hour, so the second call is refused.
$pipeline = $base()
    ->rateLimit(PipelineBuilder::tokenBucket(1, 1, Duration::seconds(3600)))
    ->compileInMemory(ExceptionClassifier::create());

$fetchOrder = static fn(): string => 'order 42';

echo $pipeline->execute($fetchOrder, $pipeline->newContext(Duration::seconds(30), operationId: 'orders-1')), "\n";

خروجی تا اینجا:

Text
order 42

گام ۲: ردشدن را بگیرید و دلیلش را بخوانید

PHP
// 1. A refusal is a typed exception. Every policy exception implements ProblemCarrier.
try {
    $pipeline->execute($fetchOrder, $pipeline->newContext(Duration::seconds(30), operationId: 'orders-2'));
} catch (ReqtideException $e) {
    if (!$e instanceof ProblemCarrier) {
        throw $e;
    }
    echo get_class($e), "\n";
    echo 'reason: ', $e->reason()->value, "\n";
    echo json_encode($e->problem(), JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), "\n";

    // 2. Decide what to do from the machine-readable fields, not from the message.
    echo match ($e->problem()->retryability()) {
        Retryability::AfterWait => "action: ask the caller to try again later\n",
        Retryability::Transient => "action: retry now\n",
        Retryability::Never => "action: fix the configuration or the code\n",
        Retryability::Unknown => "action: reconcile before repeating\n",
    };
}

خروجی:

Text
Enaxon\Reqtide\Exception\AdmissionRejectedException
reason: rate.exhausted
{
    "type": "tag:enaloo.com,2026:reqtide:problem:rate.exhausted",
    "title": "Rate limit exhausted",
    "category": "admission",
    "code": "rate.exhausted",
    "stage": "admission",
    "severity": "info",
    "retryability": "after_wait",
    "detail": "Rate limit exhausted. Category: admission; retryability: after_wait; stage: admission.",
    "operationId": "orders-2",
    "profileVersion": "v1",
    "attemptId": null,
    "correlationId": "orders-2",
    "causes": []
}
action: ask the caller to try again later
  • متد reason() یک ReasonCode برمی‌گرداند. شرط‌های برنامه را روی آن بگذارید، نه روی getMessage().
  • متد problem() یک Problem برمی‌گرداند. این گزارشی پاک‌سازی‌شده و قابل تبدیل به JSON است. شکل آن از RFC 9457 گرفته شده (type، title، detail) و عضوهای مخصوص Reqtide را هم دارد (category، code، stage، severity، retryability و شناسه‌ها). این استثنا از execute() بیرون آمده است، پس عملیات را با operationId، profileVersion و correlationId مشخص می‌کند.
  • برای ساختن Problem فقط فراداده‌های کد به کار می‌رود. پیام استثناها، داده‌های درخواست و اطلاعات محرمانه هیچ‌وقت وارد آن نمی‌شوند. علت زیرین به شکل نام کلاس و کد می‌آید.
  • نام عضوها و ترتیب آن‌ها بخشی از API است. متن title و detail بخشی از API نیست، پس آن را تجزیه نکنید. عضوهای ناشناخته را نادیده بگیرید.

قابلیت تلاش مجدد

مقدارمعنی
مقدار after_waitیک سیاست فراخوانی را رد کرده است. پس از کمی انتظار دوباره امتحان کنید.
مقدار transientتلاش مجدد بعداً ممکن است موفق شود و ابهامی دربارهٔ اثر جانبی وجود ندارد.
مقدار neverپیکربندی یا کد را درست کنید. تکرار همان فراخوانی کمکی نمی‌کند.
مقدار unknownممکن است یک تغییر اعمال شده باشد. به‌جای تکرار کورکورانه، وضعیت را بررسی و تطبیق دهید.
چهار مقدار قابلیت تلاش مجدد در یک مشکل و معنی هر کدام.

این مقدارها تصمیم Reqtide را توصیف می‌کنند. دربارهٔ نتیجهٔ کسب‌وکار شما چیزی نمی‌گویند.

دسته‌ها و شدت

مقدار category() یکی از این‌هاست: policy، infrastructure، configuration، classification، fallback، admission، retry، accounting یا preparation.

مقدار severity() فقط جنبهٔ راهنما دارد. نام سطح‌های PSR-3 (debug، info، notice، warning، error، critical) را به شکل رشتهٔ ساده به کار می‌برد، چون Core به PSR-3 وابسته نیست. امروز هیچ کدی از فهرست کدها (registry) از critical استفاده نمی‌کند. ردشدن‌های این صفحه info هستند، چون ردشدن یعنی محافظت کار می‌کند.

گام ۳: استثناهای خودتان عبور می‌کنند

PHP
// 3. Your own exceptions are never wrapped. They come out exactly as your callable threw them.
$failing = $base()->compileInMemory(ExceptionClassifier::create());
try {
    $failing->execute(static fn(): string => throw new UpstreamTimeout('upstream timed out'), $failing->newContext(Duration::seconds(30)));
} catch (UpstreamTimeout $e) {
    echo 'business exception: ', $e::class, ' (', $e->getMessage(), ")\n";
}

خروجی:

Text
business exception: Reqtide\Examples\ErrorHandling\UpstreamTimeout (upstream timed out)

گام ۴: حالت تفصیلی همین اطلاعات را به شکل داده برمی‌گرداند

متد executeDetailed() ردشدن را پرتاب نمی‌کند و آن را به شکل داده در یک ExecutionCompletion برمی‌گرداند.

PHP
// 4. Detailed mode returns the same facts as data instead of throwing.
$completion = $pipeline->executeDetailed($fetchOrder, $pipeline->newContext(Duration::seconds(30), operationId: 'orders-3'));
echo 'disposition: ', $completion->disposition->value, "\n";
echo 'dispatched: ', $completion->summary->dispatched, "\n";
echo 'problem code: ', $completion->problem()?->code->value, "\n";

خروجی:

Text
disposition: rejected
dispatched: 0
problem code: rate.exhausted

مقدارهای ممکن disposition این‌ها هستند: succeeded، failed، rejected، deferred، cancelled، deadline_exceeded، fallback_succeeded و fallback_failed.

شیء completion رکورد هر تلاش، تشخیص‌های محدودشده و خلاصهٔ شمارش‌ها را هم دارد (dispatched، retries، admittedNotDispatched، fallbackExecutions و بیشتر). وقتی Reqtide چیزی برای گزارش ندارد، مثلاً بعد از موفقیت یا یک شکست ساده در منطق کسب‌وکار، problem() برابر null است.

گام ۵: وقتی سیاست تلاش مجدد دست می‌کشد

وقتی تلاش‌های مجدد به دلیل خود سیاست تلاش مجدد متوقف شوند، آخرین استثنای شما به‌عنوان نتیجه می‌ماند. Reqtide دلیل را به شکل یک مشکل در مرحلهٔ retry ثبت می‌کند.

اگر در تلاشی بعدی محافظت دیگری فراخوانی را رد کند، رفتار فرق دارد. این محافظت‌ها مدارشکن، سقف اجرای هم‌زمان (Bulkhead)، محدودیت نرخ و دورهٔ انتظار هستند. ردشدن همان نتیجه می‌شود و آخرین استثنای شما با getPrevious() به آن متصل می‌شود.

PHP
// 5. When the retry policy gives up, the last business failure is kept, and the reason is recorded.
$retrying = $base()
    ->retry(2, BackoffMode::Constant, Duration::milliseconds(5), Duration::milliseconds(5), JitterMode::None)
    ->retryBudget(10, 10, Duration::seconds(60), Duration::seconds(10))
    ->compileInMemory(ExceptionClassifier::create()->retryOn(UpstreamTimeout::class));
$safety = OperationSafety::replayableRead(ReplaySafetyContract::forReadOnlyCallable('get-order', Duration::seconds(3600)));
$completion = $retrying->executeDetailed(
    static fn(): string => throw new UpstreamTimeout('upstream timed out'),
    $retrying->newContext(Duration::seconds(30), $safety, operationId: 'orders-4'),
);
echo 'disposition: ', $completion->disposition->value, "\n";
echo 'dispatched: ', $completion->summary->dispatched, "\n";
echo 'problem code: ', $completion->problem()?->code->value, "\n";
echo 'last business failure: ', $completion->terminalOutcome?->exception()::class, "\n";

خروجی:

Text
disposition: failed
dispatched: 2
problem code: attempts_exhausted
last business failure: Reqtide\Examples\ErrorHandling\UpstreamTimeout

فراخوانی ساده با execute() در این مثال همان UpstreamTimeout شما را پرتاب می‌کرد. حالت تفصیلی دلیل توقف تلاش‌ها را هم می‌گوید: attempts_exhausted.

گام ۶: اشتباه پیکربندی نام گزینه را می‌گوید

PHP
// 6. A configuration mistake is an InvalidConfigurationException that names the option. It is raised before
//    anything is dispatched: while building the pipeline, or when a call lacks what a retry requires.
try {
    $base()->retry(3, BackoffMode::Constant, Duration::milliseconds(5), Duration::milliseconds(5), JitterMode::None)
        ->compileInMemory(ExceptionClassifier::create());
} catch (InvalidConfigurationException $e) {
    echo $e->getMessage(), "\n";
}
try {
    $retrying->execute(static fn(): string => 'order 42', $retrying->newContext(Duration::seconds(30)));
} catch (InvalidConfigurationException $e) {
    echo $e->getMessage(), "\n";
}

خروجی:

Text
retry(maxAttempts=3) requires retryBudget(): retries must be paid for so a failing dependency is not hammered. Every call also needs an explicit replay-safety assertion, for example ReplaySafetyContract::forReadOnlyCallable(); Reqtide never infers that repeating a callable is safe.
maxAttempts greater than one requires an explicit replay-safety contract; the operation is Unsafe.

کار با فهرست کدها (registry)

  • شرط‌ها را روی $exception->reason()، $problem->code، reason() یک رویداد یا reason() یک تشخیص بگذارید. وقتی روی ReasonCode از match استفاده می‌کنید، شاخهٔ default را نگه دارید. در نسخه‌های minor مورد تازه اضافه می‌شود، پس match کامل روی آن پشتیبانی نمی‌شود. مثال match در گام ۲ روی Retryability نوشته شده است.
  • این فهرست فقط اضافه می‌شود و چیزی از آن کم نمی‌شود. مقداری که منتشر شده هیچ‌وقت معنی‌اش عوض نمی‌شود و دوباره به کار نمی‌رود. تغییر دسته یا قابلیت تلاش مجدد یک کد، تغییر ناسازگار است. شدت و متن پیام فقط جنبهٔ راهنما دارند. کدی که کنار گذاشته شود دست‌کم تا یک نسخهٔ minor دیگر هنوز تولید می‌شود.
  • کدهای اجزای بیرونی عضو فهرست نیستند. کدهای نتیجهٔ طبقه‌بند خودتان و دلیل‌های backend شخص ثالث نمونه‌اند. برای این کدها ReasonCode::tryFrom() مقدار null برمی‌گرداند.
  • فهرست کامل همان enum با نام ReasonCode در src/Error/ReasonCode.php (opens in a new tab) است.

اشتباه‌های رایج

  • گرفتن \Throwable و برخورد با هر استثنا مثل شکست upstream. ردشدن به این معنی نیست که callable شما شکست خورده است. وقتی پایپ‌لاین رد می‌کند، callable شما اصلاً صدا زده نشده است.
  • تلاش فوری و حلقه‌ای پس از ردشدن rate.exhausted. قابلیت تلاش مجدد آن after_wait است.
  • ثبت $e->getMessage() استثنای Reqtide در لاگ به‌عنوان شناسهٔ پایدار. به‌جای آن reason()->value و problem()->type() را ثبت کنید.