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

آزمون با ساعت مجازی

تلاش مجدد، تأخیر بین تلاش‌ها و بازیابی مدار را بدون انتظار واقعی آزمون کنید. یک ساعت مجازی را به پایپ‌لاین وصل کنید و ارسال‌ها را بررسی کنید.

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

در این صفحه

ابزارهای آزمون داخل همان بستهٔ enaxon/reqtide و در فضای‌نام Enaxon\Reqtide\Testing (پوشهٔ src/Testing) قرار دارند. بسته را همان‌طور که در نصب آمده است نصب کنید.

این ابزارها فقط برای آزمون‌اند. هرگز آن‌ها را از کد محیط تولید فراخوانی نکنید. هر کلاس ساده است، به کلاس پایهٔ آزمون نیاز ندارد و نشان @api-for-tests را دارد.

همهٔ بلوک‌های کد این صفحه از examples/08-testing.php (opens in a new tab) آمده‌اند. مثال از تابع کوچکی به نام expect() استفاده می‌کند تا بدون چارچوب آزمون اجرا شود. در مجموعهٔ آزمون واقعی از assertionهای چارچوب خودتان استفاده کنید.

ابزارها چه چیزی به شما می‌دهند

کلاسکاربرد
کلاس VirtualClockیک Clock که فقط با advance()، advanceMonotonicOnly()، setMonotonic() (هیچ‌وقت به عقب) و setWall() جلو می‌رود. پرش ساعت دیواری از حوزهٔ ساعت یکنواخت (monotonic) مستقل است. failNextMonotonicReads(n) و failNextWallReads(n) خطای ClockFailureException را تزریق می‌کنند. تعداد خواندن‌ها قابل مشاهده است.
کلاس SeededRandomSourceیک RandomSource روی Random\Randomizer با seed از نوع Mt19937. seed یکسان دنبالهٔ یکسان می‌دهد.
کلاس ScriptedRandomSourceیک RandomSource که فهرستی از عددهای صحیح را به ترتیب برمی‌گرداند. بازه‌های درخواست‌شده را ثبت می‌کند و وقتی فهرست تمام شود یا مقداری بیرون از بازه باشد، با خطای صریح شکست می‌خورد.
کلاس VirtualDelaySchedulerیک DelayScheduler (از نوع BoundedBlocking) که به‌جای خوابیدن، یک VirtualClock را جلو می‌برد. ترتیب بررسی‌اش مثل BoundedBlockingScheduler است: اول مهلت کل، بعد لغو، بعد پایان. برابری یعنی منقضی شده. cancelDuringNextDelay($token, $after) وسط یک تأخیر لغو می‌کند. هر تأخیر را به شکل یک DelayRecord ثبت می‌کند.
کلاس RecordingEventSinkیک EventSink که رویدادها را نگه می‌دارد (events()، ofType()). throwOnEmit($e, $times) بعد از ثبت، استثنا پرتاب می‌کند تا جداسازی شکست را آزمون کنید.
کلاس ControlledRuntimeAdapterیک RuntimeAdapter همگام که با اشیای DispatchScript کنترل می‌شود. یک اسکریپت می‌تواند مقدار برگرداند، استثنا پرتاب کند، عملیات واقعی را فراخوانی کند، ساعت مجازی را در حین فراخوانی جلو ببرد یا در حین فراخوانی درخواست لغو بدهد. این آداپتور dispatchCount()، attemptContexts() و dispatches() را ثبت می‌کند، پس آزمون‌ها ارسال‌هایی را بررسی می‌کنند که واقعاً رخ داده‌اند.
کلاس DispatchScriptرفتار تغییرناپذیر یک ارسال (returning، throwing، callingOperation، advancing، requestingCancellation).
کلاس ControlledExecutionهمان RuntimeExecution تمام‌شده که آداپتور برمی‌گرداند. اگر زمان مشاهده‌شده برابر مهلت کل یا بعد از آن باشد، نتیجه دیرهنگام است (DeadlineExceeded، پیش از لغو بررسی می‌شود). در غیر این صورت لغو درخواست‌شده Cancelled می‌دهد. اگر ساعت خوانده نشود، زمان‌بندی تهی، حسابداری ناقص و یک تشخیص ثبت می‌شود. نتیجهٔ واقعی همیشه نگه داشته می‌شود.
کلاس‌های RecordingAttemptOperation و RecordingPreparedAttemptیک AttemptOperation که تعداد فراخوانی prepare، rewinds() (آماده‌سازی‌های بعد از اولی)، تأیید تکرار، فراخوانی‌ها و نتیجه‌های دورریخته‌شده را می‌شمارد. می‌تواند یک شکست آماده‌سازی و یک شکست بازپخش تزریق کند.
کلاس‌های DispatchRecord و DelayRecordرکوردهای فقط‌خواندنی که آداپتور و scheduler در اختیار می‌گذارند.
کلاس‌های Enaxon\Reqtide\Testing و کار هر کدام.

برای Deadline، scheduler و آداپتور فقط یک نمونهٔ VirtualClock به کار ببرید. هر ساعت دیگری را Deadline رد می‌کند.

گام ۱: یک ساعت مجازی را به همه‌چیز وصل کنید

از یک VirtualClock برای scheduler، آداپتور اجرا و backendهای حافظه استفاده کنید. آن‌وقت زمان فقط وقتی جلو می‌رود که شما بگویید.

PHP
/** One VirtualClock drives the scheduler, the runtime and the memory backends, so time only moves when you say so. */
function rig(bool $withCircuit): array
{
    $clock = new VirtualClock();
    $scheduler = new VirtualDelayScheduler($clock);
    $runtime = new ControlledRuntimeAdapter($clock);
    $events = new RecordingEventSink();

    $builder = PipelineBuilder::profile('virtual-time', 'v1')
        ->partition('shop', 'test', 'orders-api')
        ->attemptTimeout(Duration::seconds(2), AttemptTimeoutRequirement::Cooperative)
        ->retry(4, BackoffMode::Exponential, Duration::milliseconds(100), Duration::seconds(1), JitterMode::None)
        ->retryBudget(10, 10, Duration::seconds(60), Duration::seconds(10));
    if ($withCircuit) {
        $builder = $builder->circuitBreaker(PipelineBuilder::lastCalls(2), 2, Ratio::of(1, 2), Duration::seconds(30), 1, 1, Duration::seconds(10));
    }

    $pipeline = $builder->compile(new RuntimeEnvironment(
        $clock,
        $scheduler,
        new SeededRandomSource(1),
        ExceptionClassifier::create()->retryOn(UpstreamTimeout::class),
        $runtime,
        $events,
        null,
        $withCircuit ? new MemoryCircuitBackend($clock) : null,
        null,
        new MemoryRetryBudgetBackend($clock),
        null,
    ));

    return [$pipeline, $clock, $scheduler, $runtime, $events];
}

کلاس InMemoryEnvironment همان چیزی است که compileInMemory() به کار می‌برد. این کلاس ساعت واقعی و یک scheduler را وصل می‌کند که واقعاً می‌خوابد. برای همین آزمون باید RuntimeEnvironment را مثل کد بالا دستی بسازد. هرگز VirtualClock را با BoundedBlockingScheduler جفت نکنید. آن scheduler منتظر زمانی می‌ماند که هیچ‌وقت نمی‌گذرد.

گام ۲: context را روی همان ساعت لنگر کنید

PHP
/** A context whose deadline and replay-safety statement are both measured on the pipeline's (virtual) clock. */
function context(CompiledPipeline $pipeline, VirtualClock $clock): ExecutionContext
{
    $contract = ReplaySafetyContract::forReadOnlyCallable('get-order', Duration::seconds(3600), $clock);

    return $pipeline->newContext(Duration::seconds(10), OperationSafety::replayableRead($contract));
}

متد newContext() مهلت کل را روی ساعت پایپ‌لاین می‌سازد، پس آن بخش خودکار است. هر چیزی که به زمان دیواری وابسته باشد باید از همان ساعت بیاید. قرارداد ایمنی تکرار یک نمونه است. قراردادی که با ساعت واقعی ساخته شود، نسبت به ساعت مجازی در گذشته یا آینده قرار می‌گیرد و Reqtide آن را نامعتبر می‌داند. در نتیجه تلاش مجدد رد می‌شود و replay_contract_not_valid ثبت می‌شود.

شیء VirtualClock به‌جای زمان واقعی از یک تاریخ ثابت شروع می‌کند: 2026-01-01T00:00:00+00:00 به وقت UTC. پس آزمون هر روز همان جواب را می‌دهد. با startingAt() می‌توانید شروع دیگری انتخاب کنید.

گام ۳: تأخیر بین تلاش‌ها را بدون خوابیدن آزمون کنید

در اینجا runtime سناریوشده نتیجهٔ هر تلاش را تعیین می‌کند، پس callable‌ای که می‌دهید هیچ‌وقت اجرا نمی‌شود:

PHP
// The scripted runtime decides every outcome, so the callable you pass is never invoked here.
$unused = static fn(): string => throw new \LogicException('the scripted runtime replaces the callable');
PHP
[$pipeline, $clock, $scheduler, $runtime, $events] = rig(false);
$runtime->enqueue(
    DispatchScript::throwing(new UpstreamTimeout('503')),
    DispatchScript::throwing(new UpstreamTimeout('503')),
    DispatchScript::returning('ok'),
);
$start = $clock->peekMonotonic();
$result = $pipeline->execute($unused, context($pipeline, $clock));
$delays = array_map(static fn(Duration $d): int => intdiv($d->nanoseconds, 1_000_000), $scheduler->requestedDelays());

expect($result === 'ok', 'the third dispatch succeeds');
expect($runtime->dispatchCount() === 3, 'exactly three dispatches');
expect($delays === [100, 200], 'exponential backoff waited 100 ms, then 200 ms');
expect($clock->peekMonotonic() - $start === 300_000_000, 'the virtual clock moved by exactly those two delays');
expect(count($events->ofType(RetryScheduled::class)) === 2, 'two RetryScheduled events were recorded');

دو ارسال اول از پیش تعیین‌شده همان خطای قابل تلاش مجدد را پرتاب می‌کنند. پایپ‌لاین اول 100 میلی‌ثانیه و بعد 200 میلی‌ثانیه صبر می‌کند (تأخیر نمایی، بدون لرزش تصادفی یا jitter) و ارسال سوم موفق می‌شود. این انتظار روی ساعت مجازی رخ می‌دهد، پس هیچ چیز نمی‌خوابد.

گام ۴: بازیابی مدار را با جلو بردن زمان آزمون کنید

PHP
[$pipeline, $clock, , $runtime] = rig(true);
$runtime->enqueue(
    DispatchScript::throwing(new \DomainException('down')),
    DispatchScript::throwing(new \DomainException('down')),
    DispatchScript::returning('recovered'),
);
for ($i = 0; $i < 2; ++$i) {
    try {
        $pipeline->execute($unused, context($pipeline, $clock));
    } catch (\DomainException) {
        // two failures open the circuit
    }
}
try {
    $pipeline->execute($unused, context($pipeline, $clock));
} catch (AdmissionRejectedException $e) {
    expect($e->reason()->value === 'circuit_open', 'the third call is refused while the circuit is open');
}
expect($runtime->dispatchCount() === 2, 'the refused call was never dispatched');

$clock->advance(Duration::seconds(31)); // no sleeping: time just moves
expect($pipeline->execute($unused, context($pipeline, $clock)) === 'recovered', 'a probe is admitted after the open duration');
expect($runtime->dispatchCount() === 3, 'the probe was dispatched');

خروجی کل اسکریپت:

Text
-- backoff without sleeping
ok: the third dispatch succeeds
ok: exactly three dispatches
ok: exponential backoff waited 100 ms, then 200 ms
ok: the virtual clock moved by exactly those two delays
ok: two RetryScheduled events were recorded
-- circuit recovery by advancing the clock
ok: the third call is refused while the circuit is open
ok: the refused call was never dispatched
ok: a probe is admitted after the open duration
ok: the probe was dispatched

نکته‌ها

  • وقتی jitter را آزمون می‌کنید، تصادفی بودن را با SeededRandomSource قطعی کنید و دو اجرا را با یک seed مقایسه کنید. کران بالا و پایین را بررسی کنید، نه یک عدد ثابت. لرزش تصادفی کامل بین صفر و تأخیر محاسبه‌شده می‌ماند.
  • ارسال‌های مشاهده‌شده را با dispatchCount() بررسی کنید، نه آنچه فکر می‌کنید پایپ‌لاین انجام داده است. فراخوانی ردشده باید صفر ارسال نشان بدهد.
  • برای هر آزمون یک پایپ‌لاین تازه بسازید. حالت در backendها می‌ماند، پس پایپ‌لاینی که دوباره استفاده شود، محدودیت نرخ، سهمیهٔ تلاش مجدد و وضعیت مدارشکن را از یک آزمون به آزمون بعد می‌برد.

آزمون مجازی چه چیزی را نشان می‌دهد

آزمونی که روی زمان مجازی موفق شود نشان می‌دهد منطق سیاست‌ها همان کاری را می‌کند که پیکربندی کرده‌اید. این آزمون دربارهٔ زمان‌بندی واقعی، انتقال‌های واقعی یا حالت مشترک مدرکی نمی‌دهد. ابزارها پایان کار و تأخیرها را شبیه‌سازی می‌کنند. قطع شدن یک callback یا توقف فیزیکی یک انتقال را مدل نمی‌کنند.