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

تلاش مجدد و سهمیه

تلاش مجدد و سهمیهٔ آن را تنظیم کنید. ببینید تلاش مجدد به چه سه چیزی نیاز دارد و وقتی سرور می‌گوید «بعد از 60 ثانیه»، Reqtide چه می‌کند.

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

در این صفحه

سهمیهٔ تلاش مجدد به یک پردازش PHP تعلق دارد و Reqtide هیچ‌وقت callback در حال اجرا را قطع نمی‌کند. محدودیت‌های Reqtide را ببینید.

بلوک‌های کد از examples/06-getting-started.php (opens in a new tab)، examples/07-error-handling.php (opens in a new tab) و examples/09-choosing-policies.php (opens in a new tab) گرفته شده‌اند. اسکریپتی آن‌ها را اجرا می‌کند و خروجی را با خروجی ثبت‌شده مقایسه می‌کند.

تلاش مجدد سه چیز لازم دارد

کتابخانهٔ Reqtide هیچ‌وقت خودسرانه تلاش مجدد نمی‌کند. تلاش دوم فقط وقتی انجام می‌شود که هر سه شرط زیر برقرار باشد:

چیز لازمدلیلجای تعیین آن
اعلام شما که تکرار تماس بی‌خطر استکتابخانه نمی‌تواند بفهمد callable اثر جانبی دارد یا نه، و از HTTP هم چیزی نمی‌داند.مقدار OperationSafety در newContext()
سهمیهٔ تلاش مجددتلاش مجدد باید هزینه داشته باشد تا وابستگی خراب زیر فشار تلاش‌های پیاپی نماند.متد retryBudget()، که هر وقت retry() بیش از یک تلاش را مجاز کند الزامی است
کلاس استثنای قابل تلاش مجددشما تعیین می‌کنید کدام شکست‌ها گذرا هستند.متد ExceptionClassifier::retryOn()
چیزهایی که تلاش مجدد لازم دارد و جای تعیین هر کدام.

اگر این اعلام را نکنید، context به‌طور پیش‌فرض OperationSafety::unsafe() می‌شود و پایپ‌لاینی که بیش از یک تلاش را مجاز کند اجرا نمی‌شود. استثنایی که فهرست نکرده باشید یک شکست ساده است. Reqtide آن را به شما برمی‌گرداند و دوباره امتحان نمی‌کند.

تلاش مجدد و سهمیه‌اش را تنظیم کنید

PHP
    // At most 3 attempts in total (the first call counts), exponential backoff from 10 ms up to 100 ms.
    ->retry(3, BackoffMode::Exponential, Duration::milliseconds(10), Duration::milliseconds(100), JitterMode::Full)
    // Retries draw from a budget so a failing dependency is not hammered: 10 tokens, 10 more per minute.
    ->retryBudget(10, 10, Duration::seconds(60), Duration::seconds(10))
  • پارامتر maxAttempts تلاش اول را هم می‌شمارد. پس 3 یعنی یک تماس و حداکثر دو تلاش مجدد. مقدار آن باید بین 1 و 64 باشد.
  • فاصله‌گذاری بین تلاش‌ها (backoff) از baseDelay شروع می‌شود، به‌صورت ثابت یا نمایی بیشتر می‌شود و از maximumDelay بالاتر نمی‌رود.
  • حالت JitterMode::Full هر تأخیر را تصادفی بین صفر و تأخیر محاسبه‌شده انتخاب می‌کند. حالت JitterMode::None دقیقاً به اندازهٔ تأخیر محاسبه‌شده صبر می‌کند.
  • آرگومان آخر اختیاری است. مقدار آن زمانی است که پس از یک تأخیر هنوز باید باقی مانده باشد تا شروع تلاش دیگر ارزش داشته باشد. پیش‌فرض آن هیچ است.

سهمیهٔ تلاش مجدد یک سطل توکن است. retryBudget(10, 10, Duration::seconds(60), Duration::seconds(10)) حداکثر 10 توکن نگه می‌دارد و در هر دقیقه 10 توکن به آن اضافه می‌کند. هنگام ارسال یک تلاش مجدد، رزرو را حداکثر 10 ثانیه نگه می‌دارد. این عدد آخر باید بیش از 1 ثانیه از مهلت هر تلاش بیشتر باشد.

سهمیه مخزنی مطلق از توکن‌هاست، نه نسبتی از تلاش‌های اول، و اندازهٔ آن با شماست. مخزن بزرگ‌تر از نیاز هیچ محافظتی نمی‌دهد. مخزن کوچک‌تر از نیاز تلاش‌های مجدد را زود و با retry_budget_exhausted متوقف می‌کند.

اعلام بی‌خطر بودن را در هر تماس بدهید

PHP
// One context per operation. Its deadline covers all attempts and waits together. forReadOnlyCallable() is YOUR
// statement that running this callable again is safe; Reqtide cannot check it, and refuses to retry without it.
$context = $pipeline->newContext(
    Duration::seconds(30),
    OperationSafety::replayableRead(ReplaySafetyContract::forReadOnlyCallable('get-payment', Duration::seconds(3600))),
);

متد forReadOnlyCallable() اعلام شماست که اجرای دوبارهٔ callable بی‌خطر است. Reqtide نمی‌تواند این را بررسی کند. این متد مدت اعتبار اعلام را هم می‌گیرد. دو نوع دیگر هم وجود دارد: providerIdempotentMutation() برای ارائه‌دهنده‌ای که تغییرهای تکراری را حذف می‌کند، و unsafe() که پیش‌فرض است.

احتمالاً در ابتدا دو اشتباه می‌کنید. Reqtide هر دو را پیش از ارسال هر چیزی اعلام می‌کند. اولی هنگام ساخت پایپ‌لاین پیش می‌آید:

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.

دومی وقتی پیش می‌آید که پایپ‌لاینِ دارای تلاش مجدد، context بدون این اعلام بگیرد:

Text
maxAttempts greater than one requires an explicit replay-safety contract; the operation is Unsafe.

مهلت کل همچنان حاکم است

تلاش مجددی که تأخیرش در مهلت باقی‌مانده جا نشود، شروع نمی‌شود. دلیل آن retry_delay_exceeds_deadline است. مهلت کل همهٔ تلاش‌ها و همهٔ انتظارها را با هم پوشش می‌دهد. در طول تأخیر چیزی نگه داشته نمی‌شود، نه مجوز مدارشکن و نه جایی در سقف اجرای هم‌زمان (Bulkhead).

هر تلاش مجدد یک واحد از محدودیت نرخ هم مصرف می‌کند. محدودیت نرخ را ببینید.

وقتی سیاست تلاش مجدد از ادامه دست می‌کشد

وقتی تلاش‌ها به‌خاطر خود سیاست تلاش مجدد متوقف شوند، Reqtide آخرین استثنای شما را به‌عنوان نتیجه نگه می‌دارد و دلیل را به‌صورت مشکلی در مرحلهٔ تلاش مجدد ثبت می‌کند. فراخوانی ساده با execute() خود استثنای شما را پرتاب می‌کند. حالت تفصیلی نشان می‌دهد چرا متوقف شد:

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

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

رعایت Retry-After یا برگرداندن آن به شما

انتظاری که سرور اعلام کند، مثل هدر Retry-After در پاسخ HTTP 429، کران پایین تلاش بعدی است. Reqtide هیچ‌وقت زودتر از آن نمی‌فرستد و انتظار را هم کوتاه نمی‌کند. Reqtide از HTTP خبر ندارد. پس تبدیل 429 و هدر آن به Duration کار طبقه‌بند (classifier) شماست و با retryAfterFrom() انجام می‌شود. کلاس زیر جای استثنایی را می‌گیرد که هنگام پاسخ 429 پرتاب می‌کنید:

PHP
/** Stands in for an HTTP 429 with a Retry-After header. */
final class Throttled extends \RuntimeException
{
    public function __construct(public readonly Duration $wait)
    {
        parent::__construct('throttled');
    }
}

اگر رعایت انتظار ممکن نباشد، نتیجه یک تعویق (deferral) است و Reqtide منتظر نمی‌ماند:

PHP
echo "-- 4. Retry-After: a wait too long for the policy is returned to you, not slept through\n";
$pipeline = $base()
    ->retry(3, BackoffMode::Constant, Duration::milliseconds(5), Duration::milliseconds(100), JitterMode::None)
    ->retryBudget(10, 10, Duration::seconds(60), Duration::seconds(10))
    ->cooldown(Duration::seconds(10))
    ->compileInMemory(
        ExceptionClassifier::create()
            ->retryOn(Throttled::class)
            ->retryAfterFrom(static fn(\Throwable $e): ?Duration => $e instanceof Throttled ? $e->wait : null),
    );
$safety = static fn(): OperationSafety => OperationSafety::replayableRead(
    ReplaySafetyContract::forReadOnlyCallable('list-products', Duration::seconds(3600)),
);
$calls = 0;
$throttled = static function () use (&$calls): string {
    ++$calls;

    throw new Throttled(Duration::seconds(60)); // "retry after 60 seconds", but the longest retry delay is 100 ms
};
try {
    $pipeline->execute($throttled, $pipeline->newContext(Duration::seconds(30), $safety()));
} catch (AdmissionDeferredException $e) {
    echo "deferred with {$e->reason()->value}; wait hint present: ", $e->decision->retryAfter !== null ? 'yes' : 'no', "\n";
    echo 'the throttling exception is kept as previous: ', $e->getPrevious() instanceof Throttled ? 'yes' : 'no', "\n";
}
Text
-- 4. Retry-After: a wait too long for the policy is returned to you, not slept through
deferred with retry_after_exceeds_maximum_delay; wait hint present: yes
the throttling exception is kept as previous: yes

تعویق در دو حالت رخ می‌دهد:

  • انتظار از maximumDelay سیاست تلاش مجدد بیشتر است: retry_after_exceeds_maximum_delay.
  • انتظار در مهلت کل باقی‌مانده جا نمی‌شود: retry_after_exceeds_deadline.

استثنا از نوع AdmissionDeferredException است و انتظار را همراه دارد. استثنایی که محدودیت را گزارش کرده بود، با getPrevious() در آن در دسترس است. اگر Retry-After مثبت باشد، دورهٔ انتظار مشترک پارتیشن هم تمدید می‌شود. در نتیجه عملیات بعدی بدون تماس با API به تعویق می‌افتد. سقف اجرای هم‌زمان و دورهٔ انتظار این را توضیح می‌دهد.

آزمون تلاش مجدد

آزمون backoff نمایی نباید واقعاً صبر کند. با ساعت مجازی، یک callable با رفتار از پیش تعیین‌شده و فهرست رویدادهای ثبت‌شده می‌توانید تأخیرهای دقیق را بررسی کنید. آزمون با ساعت مجازی را ببینید.