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

درخواست‌های Guzzle با Reqtide

درخواست‌های Guzzle را از یک پایپ‌لاین Reqtide عبور دهید، تعیین کنید چه چیزی دوباره ارسال شود، Retry-After را رعایت کنید و محدودیت‌های client را بشناسید.

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

در این صفحه

بستهٔ enaxon/reqtide-guzzle نسخهٔ 0.x (بتا) است. آن را روی خط فرمان، در Docker و در CI، با handlerهای cURL آزموده‌ایم. PHP-FPM، Guzzle 8، stream handler و PSR-18 پشتیبانی نمی‌شوند. محدودیت‌های Reqtide را ببینید.

بلوک‌های کد این صفحه از packages/guzzle/examples (opens in a new tab) آمده‌اند. خروجی‌های نمایش‌داده‌شده همان خروجی ثبت‌شدهٔ این اسکریپت‌هاست. اسکریپت‌ها یک سرور آزمایشی کوچک را روی رابط loopback راه می‌اندازند و آنچه را سرور دریافت کرده چاپ می‌کنند. در کد شما درخواست به API خودتان می‌رود.

نصب

دستور composer require enaxon/reqtide enaxon/reqtide-guzzle را اجرا کنید. هر دو بسته باید هم‌نسخه باشند. نصب را ببینید.

کلون را یک بار برای بستهٔ اصلی و یک بار برای پوشهٔ این بسته ثبت کنید:

JSON
{
    "repositories": [
        { "type": "path", "url": "../reqtide", "options": { "symlink": false } },
        { "type": "path", "url": "../reqtide/packages/guzzle", "options": { "symlink": false } }
    ]
}
Bash
composer require "enaxon/reqtide:@dev" "enaxon/reqtide-guzzle:@dev"

این بسته به PHP نسخهٔ 8.3 یا بالاتر، افزونهٔ curl و guzzlehttp/guzzle نسخهٔ 7.15.5 یا بالاتر در شاخهٔ 7.x نیاز دارد. همین نسخه از enaxon/reqtide را هم لازم دارد.

نام بستهٔ اصلی را در دستور هم بنویسید. آداپتور همان نسخهٔ dev آن را لازم دارد و پروژه‌ای که حداقل پایداری پیش‌فرض دارد، در غیر این صورت آن را رد می‌کند. این دستور در 2026-10-11 با Composer 2 روی PHP 8.3 در Docker، از یک کلون و با همین مخزن‌های مسیری آزموده شد.

یک فراخوانی را بپیچید

کلاس ReqtideClient نقطهٔ ورود است. handlerهای cURL را خودش نگه می‌دارد و هر عملیات منطقی را، با همهٔ تلاش‌هایش، از یک پایپ‌لاین کامپایل‌شدهٔ Core می‌گذراند. پایپ‌لاین را مثل شروع به کار با PipelineBuilder می‌سازید و builder را به client می‌دهید.

  • متد ReqtideClient::inMemory($builder, $configuration) حالت را در همین پردازه نگه می‌دارد. زیر PHP-FPM هر درخواست با حالت تازه شروع می‌شود.
  • متد ReqtideClient::withBackends($builder, rateLimit: ..., circuit: ..., ...) backendهای حالتی را می‌گیرد که خودتان می‌دهید، مثلاً backendهای Redis از حالت مشترک با Redis. برای هر سیاستی که پایپ‌لاین پیکربندی کرده، یک backend بدهید. ساعت تصمیم‌ها همان ساعت backend است.

هر دو یک GuzzleConfiguration اختیاری و یک EventSink اختیاری می‌گیرند. هر فراخوانی یک HttpOperation و مهلت کل عملیات را می‌گیرد. این مهلت همهٔ تلاش‌ها و همهٔ انتظارهای بین تلاش‌ها را شامل می‌شود. سه متد عملیات را اجرا می‌کنند:

  • متد send() پاسخ را برمی‌گرداند یا شکست عملیات را پرتاب می‌کند.
  • متد sendDetailed() به‌جای پرتاب، یک ExecutionCompletion با خلاصهٔ تلاش‌ها و تصمیم‌ها برمی‌گرداند.
  • متد sendAsync() یک promise برمی‌گرداند. بخش Promiseها را ببینید.

قطعه‌کدهای این صفحه این importها را لازم دارند:

PHP
use Enaxon\Reqtide\Guzzle\HttpOperation;
use Enaxon\Reqtide\Guzzle\HttpOutcomeClassifier;
use Enaxon\Reqtide\Guzzle\ReqtideClient;
use Enaxon\Reqtide\Guzzle\Tests\Support\FixtureServer;
use Enaxon\Reqtide\Pipeline\PipelineBuilder;
use Enaxon\Reqtide\Policy\AttemptTimeoutRequirement;
use Enaxon\Reqtide\Policy\BackoffMode;
use Enaxon\Reqtide\Policy\JitterMode;
use Enaxon\Reqtide\Value\Duration;
use GuzzleHttp\Psr7\Request;

مثال اول یک GET به سرور سالم و یک GET به سروری می‌فرستد که همیشه با 503 جواب می‌دهد. پایپ‌لاین سه تلاش با فاصلهٔ 20 میلی‌ثانیه و یک سهمیهٔ تلاش مجدد (retry budget) دارد:

PHP
$server = FixtureServer::start();

$client = ReqtideClient::inMemory(
    PipelineBuilder::profile('orders-api', 'v1')
        ->partition('shop', 'prod', 'orders-api')
        // The transport enforces this timeout on every attempt.
        ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::TransportEnforced)
        // Up to three attempts, 20 ms apart, and a budget so that a failing server is not hammered.
        ->retry(3, BackoffMode::Constant, Duration::milliseconds(20), Duration::seconds(1), JitterMode::None)
        ->retryBudget(10, 10, Duration::seconds(60), Duration::seconds(12)),
);

$read = static fn(string $path): HttpOperation => HttpOperation::read(
    new Request('GET', $server->baseUri . $path),
    'orders.read',
    Duration::seconds(60),
    HttpOutcomeClassifier::starter()->retryableOutcomeCodes(),
);

عددهای خروجی شمارش درخواست‌هایی است که خود سرور دریافت کرده است:

PHP
$response = $client->send($read('/status/200'), Duration::seconds(10));
echo 'healthy server: status ', $response->getStatusCode(), ', requests at the server: ', $server->count('/status/200'), "\n";

$response = $client->send($read('/status/503'), Duration::seconds(10));
echo 'failing server: status ', $response->getStatusCode(), ', requests at the server: ', $server->count('/status/503'), "\n";

$completion = $client->sendDetailed($read('/status/503'), Duration::seconds(10));
echo 'detailed: ', $completion->summary->dispatched, ' dispatched, ', $completion->summary->retries, ' retries, disposition ', $completion->disposition->value, "\n";
Text
healthy server: status 200, requests at the server: 1
failing server: status 503, requests at the server: 3
detailed: 3 dispatched, 2 retries, disposition failed

پاسخ 503 استثنا نیست. پس از تلاش سوم، آخرین پاسخ را دریافت می‌کنید. sendDetailed() سه تلاش ارسال‌شده و دو تلاش مجدد را نشان می‌دهد. مهلت هر تلاش را TransportEnforced اعلام کرده‌ایم، چون مهلت‌های cURL آن را اعمال می‌کنند.

چه چیزی را می‌شود دوباره فرستاد

کتابخانهٔ Reqtide از روی متد یا header حدس نمی‌زند. هر عملیات خودش اعلام می‌کند که می‌شود دوباره ارسال شود یا نه. سه سازنده در HttpOperation این اعلام را انجام می‌دهند:

سازندهبرای چه چیزیچه چیزی اعلام می‌کنید
سازندهٔ HttpOperation::read()درخواست‌های GET و HEADتکرار درخواست اثر جانبی ندارد (متد ایمن بر پایهٔ RFC 9110) و این اعلام به‌اندازهٔ مدتی که در validFor می‌دهید معتبر است.
سازندهٔ HttpOperation::idempotentMutation()درخواست‌های POST، PUT، PATCH و DELETEسرور درخواست را با یک هویت، مثل Idempotency-Key، تکراری‌زدایی می‌کند. هویت، محدوده‌ای که هویت در آن یکتاست و رفتار سرور با درخواست تکراری را شما مشخص می‌کنید.
سازندهٔ HttpOperation::single()هر چیز دیگرهیچ چیز. درخواست یک بار ارسال می‌شود و هرگز تکرار نمی‌شود.
سه سازندهٔ HttpOperation و اعلامی که هر کدام انجام می‌دهد.
  • مقدار validFor مدتی است که اعلام شما معتبر می‌ماند و از همین لحظه حساب می‌شود. باید از مهلت کلی که به send() می‌دهید بلندتر باشد، چون اعلام شما کمی زودتر از مهلت کل شروع می‌شود. حاشیه‌ای اضافه کنید، مثلاً یک ثانیه. اگر مقدار برابر یا کوتاه‌تر باشد، send() روی پایپ‌لاینی که تلاش مجدد دارد پیش از ارسال هر چیزی InvalidConfigurationException پرتاب می‌کند، چون چنین اعلامی هرگز اجازهٔ تلاش مجدد نمی‌دهد.
  • برای تغییردهنده (mutation)، هویت پیش از هر تلاش بررسی می‌شود و بدنه هم بررسی می‌شود. digest از نوع SHA-256 بدنه باید با digest تلاش اول برابر باشد، پس یک هویت هرگز دو payload متفاوت را حمل نمی‌کند. بدنهٔ درخواست و ارسال دوباره را ببینید. header پیش‌فرض Idempotency-Key است. اگر هویت از جای دیگری می‌رود، identityHeader: null بدهید. آن‌وقت خودتان تضمین می‌کنید که هویت ثابت می‌ماند. نام header از حرف، رقم و خط تیره ساخته می‌شود و حداکثر 64 نویسه است. مقدار هویت نباید با فاصله یا tab شروع یا تمام شود، چون Guzzle مقدار headerها را trim می‌کند.
  • بستهٔ Core عملیات single() را در پایپ‌لاینی که بیش از یک بار تلاش می‌کند رد می‌کند. این رد پیش از ارسال هر چیزی انجام می‌شود.

مثال دوم یک POST می‌فرستد که سرور هر بار با 503 به آن جواب می‌دهد. سپس گزارش (log) سرور را بررسی می‌کند:

PHP
$profile = static fn(int $attempts): PipelineBuilder => PipelineBuilder::profile('billing-api', 'v1')
    ->partition('shop', 'prod', 'billing-api')
    ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::TransportEnforced)
    ->retry($attempts, BackoffMode::Constant, Duration::milliseconds(20), Duration::seconds(1), JitterMode::None);

$retrying = ReqtideClient::inMemory($profile(3)->retryBudget(10, 10, Duration::seconds(60), Duration::seconds(12)));
$once = ReqtideClient::inMemory($profile(1));

$charge = HttpOperation::idempotentMutation(
    new Request('POST', $server->baseUri . '/status/503', ['Idempotency-Key' => 'order-1042'], 'amount=1999&currency=CAD'),
    'billing.charge',
    'order-1042',
    'this account and endpoint',
    'returns the stored response for 24 hours',
    Duration::seconds(60),
    retryableOutcomeCodes: HttpOutcomeClassifier::starter()->retryableOutcomeCodes(),
);
$response = $retrying->send($charge, Duration::seconds(10));
PHP
$log = $server->log();
$bodies = array_unique(array_column($log, 'bodySha1'));
$keys = array_unique(array_map(static fn(array $entry): string => implode(',', $entry['headers']['idempotency-key'] ?? []), $log));
echo 'server answered ', $response->getStatusCode(), ' to ', count($log), ' attempts', "\n";
echo 'same body every time: ', count($bodies) === 1 ? 'yes' : 'no', "\n";
echo 'same key every time: ', $keys === ['order-1042'] ? 'yes' : 'no', "\n";

$server->reset();
$single = HttpOperation::single(new Request('POST', $server->baseUri . '/status/503', [], 'amount=1999'), 'billing.charge');
$once->send($single, Duration::seconds(10));
echo 'a single operation: ', count($server->log()), " request\n";

try {
    $retrying->send($single, Duration::seconds(10));
} catch (InvalidConfigurationException) {
    echo 'a single operation in a retrying pipeline is refused before anything is sent: ', count($server->log()), " request in all\n";
}
Text
server answered 503 to 3 attempts
same body every time: yes
same key every time: yes
a single operation: 1 request
a single operation in a retrying pipeline is refused before anything is sent: 1 request in all

تغییردهنده سه بار با همان بدنه و همان کلید ارسال شد. عملیات single() در پایپ‌لاینی که فقط یک تلاش اجازه می‌دهد، یک بار ارسال شد. پایپ‌لاینی که تلاش مجدد دارد آن را با InvalidConfigurationException رد کرد.

ایمنی همان اعلام شماست

اگر read() را برای درخواستی صدا بزنید که چیزی را تغییر می‌دهد، یا idempotentMutation() را برای سروری که تکراری‌زدایی نمی‌کند، Reqtide درخواست را دوباره می‌فرستد. Reqtide نمی‌تواند درستی این ادعا را بررسی کند. سرور باید تکرار را بی‌ضرر کند.

نتیجه‌های قابل تلاش مجدد

تلاش مجدد سه چیز لازم دارد که در تلاش مجدد و سهمیه توضیح داده شده است. اول، اعلام شما که تکرار ایمن است. دوم، سهمیهٔ تلاش مجدد. سوم، طبقه‌بند (classifier) که نتیجه را مجاز بداند.

در اینجا اعلام همان عملیاتی است که می‌سازید، سهمیه با retryBudget() روی پایپ‌لاین تنظیم می‌شود و طبقه‌بند HttpOutcomeClassifier است. پروتکل HTTP یک بررسی دیگر هم اضافه می‌کند: کد دلیل نتیجه باید در فهرستی باشد که به عملیات می‌دهید. این فهرست آرگومان $retryableOutcomeCodes در read() و idempotentMutation() است.

فهرست خالی هیچ‌وقت تلاش مجدد نمی‌کند

مقدار پیش‌فرض $retryableOutcomeCodes یک فهرست خالی است. با این پیش‌فرض، پایپ‌لاینی که تلاش مجدد دارد هرگز تلاش مجدد نمی‌کند و فقط دلیل outcome_not_retryable را گزارش می‌دهد. فهرستی را بدهید که HttpOutcomeClassifier::starter()->retryableOutcomeCodes() برمی‌گرداند، یا فهرست خودتان را. اگر طبقه‌بند خودتان را می‌سازید، کدها را از همان طبقه‌بند بگیرید.

متد HttpOutcomeClassifier::starter() یک مجموعهٔ شروع مستند است و برای هر API توصیه نشده است. طبقه‌بند HttpOutcomeClassifier::empty() هیچ نتیجه‌ای را تلاش مجدد نمی‌کند، برای هیچ نتیجه‌ای راهکار جایگزین (Fallback) نمی‌گذارد و Retry-After را نمی‌خواند. هر فراخوانی with...() طبقه‌بند تازه‌ای برمی‌گرداند. چیزی از روی کلاس وضعیت حدس زده نمی‌شود. قاعده‌های مجموعهٔ شروع این‌هاست:

نتیجهمجاز برای تلاش مجددحساب برای مدارشکنکد دلیل
وضعیت کمتر از 400خیرموفقیتکد http_status_<code>
وضعیت 408، 502، 503 یا 504بلهشکستکد http_status_<code>
وضعیت 429بلهخنثی (با Retry-After و دورهٔ انتظار مشترک رسیدگی می‌شود)کد http_status_429
وضعیت 500خیرشکستکد http_status_500
هر 4xx یا 5xx دیگرخیرموفقیتکد http_status_<code>
خطای انتقال 6، 7، 28، 52، 55 یا 56بلهشکستکد http_transport_errno_<n>
خطای TLS یا گواهیخیرشکستکد http_transport_errno_<n>
درخواست ناقص یا پشتیبانی‌نشدهخیرخنثیکد http_transport_errno_<n>
هر خطای انتقال دیگرخیرشکستکد http_transport_errno_<n>
قاعده‌های مجموعهٔ شروع HttpOutcomeClassifier.

وضعیت 4xx غیر از 408 و 429 نشان می‌دهد سرور زنده است و درخواست را بررسی کرده. این وضعیت هرگز تلاش مجدد نمی‌شود و مدارشکن نیمه‌باز را می‌بندد. وضعیت 500 به‌طور پیش‌فرض تلاش مجدد نمی‌شود، چون اغلب نشانهٔ عیبی است که تکرار می‌شود. اگر API شما چیز دیگری می‌گوید، آن را با withRetryableStatuses() اضافه کنید.

خطاهای TLS و گواهی و خطاهای درخواست ناقص را هرگز نمی‌شود قابل تلاش مجدد کرد. در مجموعهٔ شروع هیچ وضعیتی برای fallback مجاز نیست. استثنایی که پاسخ همراه دارد دقیقاً مثل خود پاسخ طبقه‌بندی می‌شود.

بدنهٔ درخواست و ارسال دوباره

درخواستی که ممکن است دوباره ارسال شود، بدنه‌ای لازم دارد که بشود دوباره ارسالش کرد:

  • بدنهٔ قابل seek پیش از هر تلاش به موقعیت 0 برگردانده و بررسی می‌شود.
  • بدنه‌ای که نمی‌شود به عقب برگرداند، body factory لازم دارد. این یک closure است که هر بار یک stream تازه با اندازهٔ مشخص برمی‌گرداند. آن را با bodyFactory بدهید.
  • هر چیز دیگر پیش از اولین درخواست رد می‌شود.

محتوای بدنه هم محافظت می‌شود. در حالت پیش‌فرض (BodyIntegrity::Verified)، Reqtide هنگام ارسال تلاش اول، digest از نوع SHA-256 را از همان بایت‌هایی می‌سازد که فرستاده می‌شوند. پس تلاش اول بدنه را فقط یک بار می‌خواند. پیش از هر تلاش مجدد، بررسی آخر بدنه را یک بار دیگر می‌خواند و digest را مقایسه می‌کند. سپس خود تلاش مجدد هم هنگام ارسال دوباره بررسی می‌شود. بدنه‌ای که بعد از بررسی عوض شود متوقف می‌شود: اگر کمتر از 1 MB باشد پیش از ارسال هر چیزی، و اگر بیشتر باشد پیش از آخرین قطعه. بدنه‌ای که digest دیگری دارد پیش از ارسال رد می‌شود. بدنه‌ای که داده‌اش بیشتر یا کمتر از اندازهٔ اعلام‌شده باشد هم رد می‌شود. بیش از اندازه به‌اضافهٔ یک بایت خوانده نمی‌شود و بدنه در حافظه نگه داشته نمی‌شود.

پس بدنه باید قابل seek باشد (factory باید stream قابل seek برگرداند) و حداکثر maxVerifiedBodyBytes حجم داشته باشد (به‌طور پیش‌فرض 16 MiB). برای بدنهٔ بزرگ‌تر، یا بدنه‌ای که دوبار خواندنش ممکن نیست، BodyIntegrity::CallerAsserted بدهید. آن‌وقت خودتان تضمین می‌کنید که بایت‌ها ثابت می‌مانند و فقط اندازه و موقعیت شروع بررسی می‌شود. زمانی که صرف خواندن بدنه می‌شود جزو مهلت هر تلاش نیست.

اگر انتقال فقط بخشی از بدنه را برده باشد (آپلود قطع‌شدهٔ 1 MB یا بیشتر)، payload عملیات دیگر قابل تعیین نیست و عملیات دوباره ارسال نمی‌شود. انتقالی که هیچ بایتی از بدنه را نبرده باشد مثل همیشه تکرار می‌شود. اگر چنین آپلودی باید دوباره ارسال شود، BodyIntegrity::CallerAsserted بدهید.

عملیات single() می‌تواند بدنه‌ای داشته باشد که به عقب برنمی‌گردد، چون فقط یک بار ارسال می‌شود. درخواستی که بدنه‌اش اندازهٔ مشخص دارد، اگر از قبل چنین headerی نداشته باشد، یک header خالی Expect می‌گیرد. آن‌وقت cURL منتظر 100 Continue نمی‌ماند.

پاسخ‌ها، شکست‌ها، تغییر مسیرها و مهلت‌ها

  • وضعیت 4xx یا 5xx یک پاسخ است. پس از آخرین تلاش، آخرین پاسخ را می‌گیرید. گزینهٔ http_errors برای هر انتقال به زور خاموش می‌شود.
  • شکست انتقال، مثل اتصال ردشده یا اتمام مهلت، همان استثنای اصلی Guzzle است، مگر اینکه پیش از آن یک سیاست عملیات را تمام کند. پیام این استثنا آدرس کامل را دارد. این نتیجهٔ شماست و Reqtide آن را همان‌طور که هست برمی‌گرداند.
  • رد شدن به‌دست سیاست‌ها یعنی وقتی محدودیت نرخ، مدارشکن، سقف اجرای هم‌زمان (Bulkhead) یا دورهٔ انتظار مشترک درخواست را رد کند، استثنای Reqtide پرتاب می‌شود. بخش مدیریت خطا را ببینید.
  • تغییر مسیرها دنبال نمی‌شوند. پاسخ 3xx یک پاسخ است و مقصدش هرگز درخواست نمی‌شود. گزینهٔ allow_redirects به زور خاموش است و قابل تنظیم نیست.
  • مهلت‌ها. مهلت هر تلاش برابر است با کمترین مقدار از این دو: مهلتی که شما برای تلاش تعیین کرده‌اید و زمان باقی‌ماندهٔ عملیات. این مقدار به‌صورت timeout و connect_timeout به cURL می‌رسد. گزینهٔ read_timeout هرگز به کار نمی‌رود، چون handlerهای cURL آن را نادیده می‌گیرند.

دورهٔ انتظار مشترک و Retry-After

طبقه‌بند برای پاسخ‌های 429 و 503 مقدار Retry-After را می‌خواند. فهرست را با withRetryAfterStatuses() تغییر دهید. مقدار می‌تواند تعداد ثانیه یا تاریخ HTTP باشد، در شکل IMF-fixdate یا شکل‌های قدیمی‌تر RFC 850 و asctime.

مقداری که خوانده نشود، تأخیری ایجاد نمی‌کند. همین برای مقداری هم صدق می‌کند که از بازهٔ مدت‌های حالت مشترک بیشتر باشد، یعنی از 9,007,199,254 ثانیه. Reqtide هرگز چنین مقداری را کوتاه نمی‌کند و مقداری از خودش نمی‌سازد. اگر چند خط header باشد، بزرگ‌ترین تأخیر معتبر برنده است. اختلاف ساعت بین سرور و میزبان شما برای مقدارهای تاریخ همچنان یک محدودیت است.

این تأخیر به دو شکل به کار می‌رود. اول، حد پایین تلاش بعدی همین عملیات است: Reqtide هرگز زودتر نمی‌فرستد. دوم، تأخیری که در حداکثر تأخیر سیاست تلاش مجدد یا در مهلت کل جا نشود، عملیات را به تعویق می‌اندازد و مقدار تأخیر را به شما برمی‌گرداند.

اگر پایپ‌لاین سیاست cooldown() داشته باشد، یک تأخیر مثبت یک دورهٔ انتظار مشترک هم برای پارتیشن شروع می‌کند. فراخوانی‌های بعدی بدون ارسال درخواست به تعویق می‌افتند. مثال سوم این را نشان می‌دهد:

PHP
$client = ReqtideClient::inMemory(
    PipelineBuilder::profile('search-api', 'v1')
        ->partition('shop', 'prod', 'search-api')
        ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::TransportEnforced)
        ->retry(1, BackoffMode::Constant, Duration::milliseconds(20), Duration::seconds(1), JitterMode::None)
        // How long the cooldown record is kept after it has ended.
        ->cooldown(Duration::seconds(60)),
);

$read = static fn(string $path): HttpOperation => HttpOperation::read(
    new Request('GET', $server->baseUri . $path),
    'search.read',
    Duration::seconds(60),
    HttpOutcomeClassifier::starter()->retryableOutcomeCodes(),
);
PHP
$explanation = $client->explain();
echo 'policies: ', implode(', ', $explanation->enabledPolicies), "\n";
echo 'requests after explain(): ', count($server->log()), "\n";

$response = $client->send($read('/status/429?ra=30'), Duration::seconds(5));
echo 'throttled: status ', $response->getStatusCode(), ', requests at the server: ', count($server->log()), "\n";

$completion = $client->sendDetailed($read('/status/200'), Duration::seconds(1));
echo 'next call during the cooldown: ', $completion->disposition->value, ', requests at the server: ', count($server->log()), "\n";
echo 'reason: ', $completion->problem()?->code->value ?? 'none', "\n";
Text
policies: attempt, retry(maxAttempts=1), cooldown
requests after explain(): 0
throttled: status 429, requests at the server: 1
next call during the cooldown: deferred, requests at the server: 1
reason: cooldown_exceeds_deadline

پاسخ 429 مقدار Retry-After: 30 داشت. فراخوانی بعدی مهلت کل 1 ثانیه داشت و Reqtide آن را فوراً به تعویق انداخت. شمارش در سرور روی 1 ماند. دلیل cooldown_exceeds_deadline است، چون 30 ثانیه در مهلت 1 ثانیه جا نمی‌شود. با مهلت بلندتر، دلیل cooldown_active است. فراخوانی explain() هیچ چیز نفرستاد.

سقف دورهٔ انتظار

سرور ممکن است عمداً یا از روی اشتباه یک Retry-After بسیار بلند بفرستد. سیاست دورهٔ انتظار مشترک (cooldown) در Core برای دورهٔ انتظاری که یک تأخیر گزارش‌شده می‌تواند شروع کند سقف می‌گذارد. سقف پیش‌فرض 3,600 ثانیه است، یعنی یک ساعت.

تأخیر بلندتر همچنان تلاش مجدد همان عملیاتی را تعیین می‌کند که آن را دریافت کرده. دورهٔ انتظاری که عملیات‌های دیگر می‌بینند تا سقف کوتاه می‌شود. سقف خودتان را با آرگومان دوم PipelineBuilder::cooldown($cleanupGrace, $maximumExtension) بدهید. بخش سقف اجرای هم‌زمان و دورهٔ انتظار را ببینید.

پیش‌فرض‌های client

ویژگی GuzzleConfiguration::$clientDefaults فقط این گزینه‌های Guzzle را می‌پذیرد: headers، auth، proxy، verify، cert، ssl_key، crypto_method، force_ip_resolve، version و decode_content.

  • گزینهٔ headers فقط headerهایی را پر می‌کند که درخواست ندارد، پس نمی‌تواند header هویتی را که Reqtide در هر تلاش بررسی می‌کند عوض کند. همچنین نمی‌تواند Content-Length یا Transfer-Encoding را تنظیم کند: transport چارچوب‌بندی (framing) را از بدنه‌ای می‌گیرد که فرستاده می‌شود.
  • هر گزینهٔ دیگر با InvalidConfigurationException رد می‌شود که نام همان گزینه را می‌گوید. نمونه‌ها delay، query، body، json، sink، curl، cookies، on_headers، on_stats، timeout، base_uri و handler هستند.
  • دلیلش این است که پیش‌فرض client روی هر انتقال، بیرون از بررسی‌های ارسال دوباره و بیرون از مهلت کل اعمال می‌شود. یک delay پیش‌فرض بیشتر از مهلت کل می‌خوابد. یک curl پیش‌فرض می‌تواند مهلت‌ها و قاعده‌های اتصال را، که در ادامه می‌آید، عوض کند.

مقدارهای مخصوص هر درخواست را در خود درخواست بگذارید. client handlerهایش را خودش می‌سازد و مالک آن‌هاست: CurlHandler برای send() و CurlMultiHandler برای sendAsync(). نمی‌توانید handler بدهید.

استفادهٔ دوباره از اتصال

وقتی یک اتصال keep-alive که دوباره استفاده می‌شود هیچ بایتی برنگرداند، libcurl می‌تواند درخواست را، با هر متدی، روی اتصال تازه دوباره بفرستد. Reqtide این ارسال دوم را نمی‌بیند. پس قاعده این است:

  • عملیات single() گزینه‌های CURLOPT_FRESH_CONNECT و CURLOPT_FORBID_REUSE را تنظیم می‌کند و هیچ چیز نمی‌تواند آن را عوض کند. هزینه‌اش یک handshake تازهٔ TCP و TLS برای هر چنین درخواستی است.
  • عملیات‌های read() و idempotentMutation() می‌توانند از اتصال‌ها دوباره استفاده کنند، چون تکرارشان طبق اعلام خود شما ایمن است.

تلاش مجددی که proxy یا load balancer خودتان پشت اتصال انجام می‌دهد برای Reqtide دیده نمی‌شود.

کار با Promiseها

متد sendAsync() یک promise برمی‌گرداند که فقط پس از پایان کل عملیات و نهایی‌سازی (finalization) settle می‌شود. این promise تنبل (lazy) است. Guzzle حلقهٔ رویداد ندارد، پس عملیات وقتی اجرا می‌شود که wait() را صدا بزنید، و هر بار فقط یک عملیات.

بنابراین چند promise که به GuzzleHttp\Promise\Utils::all() یا settle() داده شوند، پشت سر هم اجرا می‌شوند. زمان کل جمع زمان عملیات‌هاست و هر عملیات مهلت خودش را دارد که از فراخوانی sendAsync() همان عملیات شمرده می‌شود. از sendAsync() برای رابط promise و لغو استفاده کنید، نه برای اجرای هم‌زمان چند عملیات.

  • در حین انتظار، driver صف کار promiseهای Guzzle را اجرا می‌کند که برای کل پردازه است. callbackهای نامرتبط promiseهای برنامهٔ شما هم داخل حلقهٔ مهلت کل اجرا می‌شوند.
  • هیچ ادعایی دربارهٔ عملیات هم‌زمان، تکمیل بدون wait() یا توقف تضمین‌شدهٔ یک انتقال در حال اجرا وجود ندارد.
  • لغو از انتقال می‌خواهد متوقف شود، اما ثابت نمی‌کند که متوقف شده است. یک Cancellation را به send()، sendDetailed() یا sendAsync() بدهید.
  • حالت سیاست‌ها بین runtime همگام و runtime مبتنی بر promise مشترک است.

داده‌های حساس و آنچه رویدادها همراه دارند

رویدادها، خلاصه‌ها، تشخیص‌ها (diagnostics) و مشکل‌ها (problems) فقط فیلدهای توصیفی دارند. این فیلدها عبارت‌اند از متد، اثر انگشت origin، گروه عملیات شما، و اثر انگشت headerهای Authorization و Proxy-Authorization درخواستی که به عملیات می‌دهید. این اثر انگشت با کلید credentialKey که می‌دهید ساخته می‌شود.

کلاس ReqtideClient اثر انگشت را با پیش‌فرض‌های client حل می‌کند، پس اعتبارنامه‌هایی را می‌پوشاند که واقعاً فرستاده می‌شوند:

  • مقدار پیش‌فرض headers برای headerهایی که درخواست ندارد.
  • مقدار پیش‌فرض auth از نوع Basic (که header Authorization را عوض می‌کند).
  • گزینهٔ auth از نوع Digest.
  • اطلاعات کاربر (userinfo) در URI درخواست. cURL آن را وقتی header Authorization نباشد به‌صورت Basic می‌فرستد.
  • اطلاعات کاربر در آدرس proxy که برای درخواست اعمال می‌شود.
  • نام headerهایی که در GuzzleConfiguration با نام credentialHeaders می‌آورید (مثلاً X-Api-Key).
  • مقدارهای پیش‌فرض cert و ssl_key.

اثر انگشت اعتبارنامه‌هایی را که Guzzle از محیط می‌خواند (HTTP_PROXY، HTTPS_PROXY، NO_PROXY) نمی‌بیند. header Cookie را هم نمی‌بیند، مگر Cookie را در credentialHeaders بیاورید.

بدون credentialKey یک کلید تصادفی برای پردازه به کار می‌رود، پس اثر انگشت‌ها فقط داخل یک پردازه قابل مقایسه‌اند. برای مقایسه بین پردازه‌ها، در همهٔ workerها یک کلید یکسان بدهید. اگر عملیات را با پایپ‌لاین خودتان اجرا می‌کنید، پیش از خواندن metadata() تابع $operation->forClient($configuration) را صدا بزنید.

رویدادها، خلاصه‌ها، تشخیص‌ها و مشکل‌ها هرگز URL، query، header یا بدنه را حمل نمی‌کنند. در همهٔ این‌ها مقدارهای محرمانهٔ تصادفی گذاشتیم و در همهٔ خروجی‌های Reqtide و در کلیدها و مقدارهای Redis جست‌وجو کردیم. چیزی پیدا نشد.

خروجی var_dump یا print_r برای یک عملیات، پیکربندی یا client فقط نام‌ها، اثر انگشت‌ها و origin مبنای URI را نشان می‌دهد (هرگز مسیر، query، header، بدنه یا هویت را). این تضمین شامل var_export و reflection نمی‌شود و این اشیا قابل serialize نیستند.

دو مورد بیرون از این تضمین‌اند:

  • استثنایی که Guzzle برای شکست انتقال پرتاب می‌کند آدرس کامل را دارد. این نتیجهٔ شماست و Reqtide آن را همان‌طور که هست برمی‌گرداند.
  • ممکن است stack traceها آرگومان‌ها را هم داشته باشند. در محیط تولید zend.exception_ignore_args=On را تنظیم کنید.

آدرس پایه و متد uri()

به GuzzleConfiguration یک baseUri مطلق با http یا https بدهید. فراخوانی $client->uri('/orders/42') یک ارجاع را طبق RFC 3986 نسبت به آن حل می‌کند. درخواست یک HttpOperation همین نتیجهٔ مطلق را لازم دارد. ارجاعی که قالب درستی نداشته باشد InvalidConfigurationException پرتاب می‌کند.

آدرس پایه مرز امنیتی نیست

ارجاع مطلق، یا ارجاعی که با // شروع شود، میزبان پایه را عوض می‌کند. ارجاع orders نسبت به https://h/v1 می‌شود https://h/orders. Reqtide بررسی نمی‌کند درخواست به کجا می‌رود. با قاعده‌های خروج (egress) شبکهٔ خودتان جلوی درخواست به آدرس‌های داخلی را بگیرید.

سقف‌ها و هزینه

  • اندازهٔ پاسخ. Reqtide برای آن سقفی نمی‌گذارد. سرور مخرب یا خراب می‌تواند در بازهٔ مهلت، فضای ذخیره‌سازی موقت را پر کند. گزینهٔ sink را اینجا نمی‌شود تنظیم کرد: client آن را به‌عنوان پیش‌فرض رد می‌کند و عملیات گزینهٔ درخواست ندارد. اندازه را در لایهٔ خودتان محدود کنید، با proxy یا gateway جلوی client یا با قاعده‌های شبکهٔ میزبان.
  • صف کار. انتظار یک promise، callbackهای نامرتبط Guzzle را اجرا می‌کند. بخش Promiseها را ببینید.
  • پشتیبانی نمی‌شود: PSR-18، Guzzle 8، stream handler، بدنهٔ streaming و PHP-FPM. فقط handlerهای cURL به کار می‌روند و نمی‌توانید handler بدهید.
  • حالت مشترک backend لازم دارد، مثل backendهای Redis. متد inMemory() محدودیت‌ها را در همان پردازه نگه می‌دارد. محدودیت نرخ، مدارشکن و دورهٔ انتظار مشترک بین پردازه‌ها با حداکثر 6 پردازه آزموده شده‌اند.
  • اولین درخواست. client حدود 100 تا 150 فایل بیشتر از Guzzle خالی بارگذاری می‌کند. در محیط تولید از autoloader بهینه و OPcache استفاده کنید.

هزینه

این عددها روی یک لپ‌تاپ و زیر Docker Desktop اندازه‌گیری شدند. نوسان بین اجراها زیاد بود، پس ادعایی دربارهٔ سیستم شما نیستند.

یک فراخوانی از طریق client با حالت درون‌حافظه، با یک سیاست، حدود 30 تا 130 میکروثانیه به فراخوانی Guzzle خالی اضافه کرد. با پنج سیاست حدود 135 تا 235 میکروثانیه اضافه شد. اولین درخواست یک پردازه با Guzzle خالی 14 میلی‌ثانیه و با client بین 26 تا 35 میلی‌ثانیه طول کشید، وقتی سورس‌ها روی دیسک محلی بودند. وقتی سورس‌ها روی bind mount کند بودند، اولین درخواست 0.6 تا 1 ثانیه طول کشید.

بررسی بدنه در BodyIntegrity::Verified به تلاش اول خواندنی اضافه نمی‌کند، چون digest از بایت‌هایی ساخته می‌شود که انتقال می‌خواند. هر تلاش مجدد بدنه را در بررسی آخر یک بار دیگر می‌خواند. هر خواندن برای 1 MiB حدود 4 میلی‌ثانیه، برای 8 MiB حدود 30 تا 35 و برای 16 MiB حدود 85 تا 95 میلی‌ثانیه طول کشید، در حافظه یا در فایل موقت، بدون رشد حافظه. پس بدنهٔ 16 MiB حدود 0.1 ثانیه به هر تلاش مجدد اضافه می‌کند و این زمان جزو مهلت هر تلاش نیست. برای بدنهٔ بزرگ و مهلت کوتاه، maxVerifiedBodyBytes را کم کنید یا BodyIntegrity::CallerAsserted بدهید. اسکریپت اندازه‌گیری benchmarks/body-digest.php است.