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

۱. اجازهٔ درخواست را از نتیجهٔ واقعی سرویس جدا کنیم

مدارشکن نرم‌افزاری نتیجهٔ سرویس را اندازه نمی‌گیرد
تصویر مفهومی: مدارشکن نرم‌افزاری نتیجهٔ سرویس را اندازه نمی‌گیرد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

سه نام حالت داریم: CLOSED اجازهٔ عادی می‌دهد، OPEN در مهلت انتظار اجازه نمی‌دهد و HALF_OPEN برای آزمونِ پس از مهلت است. نام‌ها از الگوی مدارشکن می‌آیند؛ این برنامه وسیلهٔ حفاظت برقی، سامانهٔ تلفن یا پایشگر واقعی سلامت نیست. تغییر حالت در حافظه، خرابیِ بیرونی را تعمیر نمی‌کند.

توضیح اصلی Azure Architecture مسدودکردن موقتِ اقدامِ احتمالاً ناموفق را از الگوی تلاش دوباره جدا می‌کند. ما این تفکیک را به قرارداد آزمون تبدیل می‌کنیم: تابع acquire فقط اجازه و شمارهٔ محلی می‌دهد؛ تابع complete بعداً نتیجهٔ ساختگیِ همان شماره را ثبت می‌کند. اجازه به معنی اجرای واقعی یا موفقیت نیست.

برای کوچک‌ماندن مدل، در هر لحظه فقط یک مشاهدهٔ ناتمام داریم، حتی در حالت بسته. درخواست دوم تا ثبت نتیجه با دلیل «مشاهدهٔ در انتظار» رد می‌شود. این قید، پروفایل ترتیبیِ آموزش است؛ توصیهٔ محدودکردن همهٔ سرویس‌ها به یک درخواست نیست. شماره نیز فقط ارتباط دو گامِ محلی را حفظ می‌کند و مجوز دسترسی یا هویت مشتری نیست.

۲. خطاهای متوالی را تا آستانه بشماریم

موفقیت و خطای متوالی به سیاست شمارش روشن نیاز دارند
تصویر مفهومی: موفقیت و خطای متوالی به سیاست شمارش روشن نیاز دارند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

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

مقالهٔ Martin Fowler نمونهٔ سادهٔ شمارنده با بازنشانی پس از موفقیت را توضیح می‌دهد. ما کد روبی یا زمان‌سنجِ آن را اجرا نکرده‌ایم. راهنمای Azure شمارندهٔ خطا در پنجرهٔ زمانی و تعداد موفقیت‌های آزمون را هم شرح می‌دهد؛ قرارداد ما آگاهانه «خطاهای متوالی» و «یک موفقیت آزمون» است، نه بازسازی تمام گزینه‌های آن راهنما.

مدت انتظار از زمان ثبتِ شکستِ بازکننده حساب می‌شود، نه زمان صدور اجازه. اگر آستانه در میلی‌ثانیهٔ ده برسد و انتظار هزار باشد، مهلت ۱۰۱۰ است. درخواستِ ردشده در پانصد این مهلت را جلو نمی‌برد. بازپرشدن بودجه در مدل سطل توکن مسئلهٔ جداست؛ اینجا اعتبار، هزینهٔ درخواست و نرخ توکن نداریم.

۳. پس از مهلت فقط آزمون مشخص را وارد کنیم

حالت نیمه‌باز آزمون محدود و نتیجهٔ مشخص می‌خواهد
تصویر مفهومی: حالت نیمه‌باز آزمون محدود و نتیجهٔ مشخص می‌خواهد؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

سیاست مرزیِ خودمان چنین است: زمان کمتر از مهلت رد می‌شود و در برابری دقیق، اجازهٔ آزمون صادر می‌شود. پس ۱۰۰۹ زود است و ۱۰۱۰ مجاز است. این برابری انتخابِ قرارداد ماست؛ شرطِ زمانی نمونه‌های دیگر را قانون عمومی معرفی نمی‌کنیم. بدون فراخوانی تازه نیز نخ یا ساعتِ خودکاری حالت را عوض نمی‌کند.

پس از اجازهٔ آزمون، حالت نیمه‌باز و شمارهٔ آن تا ثبت نتیجه حفظ می‌شود. یک درخواستِ دیگر در همان زمان اجازه نمی‌گیرد. اگر نتیجهٔ آزمون شکست باشد، شمارنده یک واحد دیگر زیاد و انتظار کامل از زمان همان شکست آغاز می‌شود. اگر موفق باشد، حالت بسته، شمارنده صفر و مهلت تهی می‌شود؛ این صرفاً واکنش به برچسبِ ورودی است.

مدل برای مشاهدهٔ ناتمام، انقضای خودکار ندارد. اگر نتیجه هرگز ثبت نشود، اجازهٔ تازه همچنان بسته می‌ماند و زمانِ انتظارِ معلوم گزارش نمی‌شود. بنابراین انتظارِ مدارشکن را نباید با مهلتِ اجرای عملیات یکی گرفت. بازنشانی دستی هم اجازه ندارد مشاهدهٔ ناتمام را دور بریزد؛ ابتدا باید همان قرارداد ثبت نتیجه تعیین تکلیف شود.

۴. کد و خط زمانی را با جدول مستقل مقایسه کنیم

رد زمانی مدارشکن با جدول دستی مستقل بررسی می‌شود
تصویر مفهومی: رد زمانی مدارشکن با جدول دستی مستقل بررسی می‌شود؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

فایل circuit_breaker.py با Python 3.14.5 و کتابخانهٔ استاندارد اجرا شده است. Enum پایتون نام حالت‌ها را مشخص می‌کند؛ قفل و اتمی‌بودن نمی‌سازد. آستانه عدد صحیح یک تا چهار و انتظار یک تا ده‌هزار میلی‌ثانیه است. زمانِ داده‌شده صحیح، نامنفی و عقب‌نرونده است؛ ساعت تقویمی و خواب واقعی نداریم.

"""Deterministic offline sequential circuit breaker; no callable/network runs."""
from enum import Enum
import itertools
import json
import platform


class State(Enum):
    CLOSED = "CLOSED"
    OPEN = "OPEN"
    HALF_OPEN = "HALF_OPEN"


class SequentialBreaker:
    """One outstanding synthetic observation at a time, not thread-safe."""
    def __init__(self, threshold=2, cooldown_ms=1000):
        if type(threshold) is not int or not 1 <= threshold <= 4:
            raise ValueError("threshold must be an integer in 1..4")
        if type(cooldown_ms) is not int or not 1 <= cooldown_ms <= 10000:
            raise ValueError("cooldown must be integer milliseconds in 1..10000")
        self.threshold = threshold
        self.cooldown_ms = cooldown_ms
        self._state = State.CLOSED
        self._failures = 0
        self._open_until = None
        self._pending = None
        self._next_ticket = 1
        self._last_ms = 0

    def _time(self, now_ms):
        if type(now_ms) is not int or now_ms < self._last_ms or now_ms < 0:
            raise ValueError("nonnegative integer time cannot run backwards")

    def view(self):
        return {"state": self._state.value, "consecutive_failures": self._failures,
                "open_until_ms": self._open_until, "pending_ticket": self._pending,
                "next_ticket": self._next_ticket, "last_ms": self._last_ms}

    def acquire(self, now_ms):
        self._time(now_ms)
        self._last_ms = now_ms
        if self._pending is not None:
            return {"allowed": False, "reason": "pending_observation", "ticket": None,
                    "retry_after_ms": None, **self.view()}
        if self._state is State.OPEN:
            if now_ms < self._open_until:
                return {"allowed": False, "reason": "open", "ticket": None,
                        "retry_after_ms": self._open_until - now_ms, **self.view()}
            self._state = State.HALF_OPEN
        reason = "probe" if self._state is State.HALF_OPEN else "normal"
        self._pending = self._next_ticket
        self._next_ticket += 1
        return {"allowed": True, "reason": reason, "ticket": self._pending,
                "retry_after_ms": 0, **self.view()}

    def complete(self, now_ms, ticket, success):
        self._time(now_ms)
        if type(ticket) is not int or ticket != self._pending or self._pending is None:
            raise ValueError("completion must match the outstanding ticket")
        if type(success) is not bool:
            raise ValueError("success must be an actual bool, supplied by the fixture")
        self._last_ms = now_ms
        self._pending = None
        if success:
            self._failures = 0
            self._open_until = None
            self._state = State.CLOSED
        else:
            self._failures += 1
            if self._state is State.HALF_OPEN or self._failures >= self.threshold:
                self._state = State.OPEN
                self._open_until = now_ms + self.cooldown_ms
        return self.view()

    def reset(self, now_ms):
        self._time(now_ms)
        if self._pending is not None:
            raise ValueError("manual reset cannot discard a pending observation")
        self._last_ms = now_ms
        self._state = State.CLOSED
        self._failures = 0
        self._open_until = None
        # Ticket numbering is preserved: reset does not recycle old tickets.
        return self.view()


def _rejects_unchanged(breaker, function):
    before = breaker.view()
    try:
        function()
    except ValueError:
        return breaker.view() == before
    return False


def _instant(breaker, now_ms, outcome):
    permit = breaker.acquire(now_ms)
    if permit["allowed"]:
        return permit, breaker.complete(now_ms, permit["ticket"], outcome)
    return permit, breaker.view()


def independent_reference(threshold, cooldown_ms, times, outcomes):
    """Outcome-history/blocked-interval oracle; no State class or breaker calls."""
    accepted_outcomes = []
    blocked_until = None
    results = []
    for at, outcome in zip(times, outcomes):
        if blocked_until is not None and at < blocked_until:
            trailing = len(accepted_outcomes) - 1 - max(
                (i for i, ok in enumerate(accepted_outcomes) if ok), default=-1)
            results.append((False, "open", "OPEN", trailing, blocked_until))
            continue
        was_probe = blocked_until is not None
        accepted_outcomes.append(outcome)
        last_success = max((i for i, ok in enumerate(accepted_outcomes) if ok), default=-1)
        trailing = len(accepted_outcomes) - 1 - last_success
        if outcome:
            blocked_until = None
        elif trailing >= threshold:
            blocked_until = at + cooldown_ms
        results.append((True, "probe" if was_probe else "normal",
                        "OPEN" if blocked_until is not None else "CLOSED",
                        trailing, blocked_until))
    return results


def run_example():
    b = SequentialBreaker(2, 1000)
    trace = []
    def acquire(at):
        r = b.acquire(at)
        trace.append({"operation": "acquire", "at_ms": at, "result": r})
        return r
    def complete(at, ticket, success):
        r = b.complete(at, ticket, success)
        trace.append({"operation": "complete", "at_ms": at, "success": success, "result": r})
        return r
    p1 = acquire(0); c1 = complete(0, p1["ticket"], False)
    p2 = acquire(10); c2 = complete(10, p2["ticket"], False)
    blocked = acquire(500)
    before = acquire(1009)
    probe = acquire(1010)
    busy = acquire(1010)
    failed_probe = complete(1010, probe["ticket"], False)
    before_second = acquire(2009)
    probe2 = acquire(2010)
    recovered = complete(2010, probe2["ticket"], True)
    checks = {}
    checks["first_failure_remains_closed"] = c1["state"] == "CLOSED" and c1["consecutive_failures"] == 1
    checks["threshold_failure_opens_from_completion_time"] = c2["state"] == "OPEN" and c2["open_until_ms"] == 1010 and c2["consecutive_failures"] == 2
    checks["blocked_attempt_does_not_extend_deadline"] = not blocked["allowed"] and blocked["reason"] == "open" and blocked["retry_after_ms"] == 510 and blocked["open_until_ms"] == 1010
    checks["deadline_before_and_exact_equality"] = not before["allowed"] and before["retry_after_ms"] == 1 and probe["allowed"] and probe["reason"] == "probe" and probe["state"] == "HALF_OPEN"
    checks["pending_probe_denies_another_permission"] = not busy["allowed"] and busy["reason"] == "pending_observation" and busy["retry_after_ms"] is None and busy["pending_ticket"] == probe["ticket"]
    checks["failed_probe_restarts_full_cooldown"] = failed_probe["state"] == "OPEN" and failed_probe["open_until_ms"] == 2010 and failed_probe["consecutive_failures"] == 3 and before_second["retry_after_ms"] == 1
    checks["successful_probe_closes_and_clears_streak"] = recovered["state"] == "CLOSED" and recovered["consecutive_failures"] == 0 and recovered["open_until_ms"] is None
    normal = SequentialBreaker()
    a = _instant(normal, 0, False)[1]; z = _instant(normal, 1, True)[1]; a2 = _instant(normal, 2, False)[1]
    checks["normal_success_breaks_failure_streak"] = a["consecutive_failures"] == 1 and z["consecutive_failures"] == 0 and a2["state"] == "CLOSED" and a2["consecutive_failures"] == 1
    manual = SequentialBreaker(1, 1000)
    _instant(manual, 0, False)
    reset = manual.reset(1)
    new = manual.acquire(1)
    checks["manual_reset_clears_open_without_recycling_ticket"] = reset["state"] == "CLOSED" and reset["open_until_ms"] is None and new["allowed"] and new["ticket"] == 2
    checks["manual_reset_cannot_discard_pending_observation"] = _rejects_unchanged(manual, lambda: manual.reset(2))
    checks["wrong_stale_and_missing_completions_rejected"] = _rejects_unchanged(manual, lambda: manual.complete(2, 1, True)) and _rejects_unchanged(manual, lambda: manual.complete(2, None, True))
    checks["outcome_requires_actual_bool_without_mutation"] = all(_rejects_unchanged(manual, lambda bad=bad: manual.complete(2, new["ticket"], bad)) for bad in [1, 0, "success", None])
    checks["invalid_and_backwards_clock_is_not_committed"] = all(_rejects_unchanged(manual, lambda bad=bad: manual.acquire(bad)) for bad in [0, -1, True, 1.0, "2"]) and _rejects_unchanged(manual, lambda: manual.complete(0, new["ticket"], True))
    config_ok = True
    for threshold, cooldown in [(True, 1), (0, 1), (5, 1), (1.0, 1), (1, True), (1, 0), (1, 10001), (1, 1.0)]:
        try:
            SequentialBreaker(threshold, cooldown)
            config_ok = False
        except ValueError:
            pass
    checks["configuration_integer_and_bound_contract"] = config_ok
    same = SequentialBreaker(1, 1)
    permission = same.acquire(0)
    pending = same.acquire(0)
    same.complete(0, permission["ticket"], False)
    denied = same.acquire(0)
    exact_probe = same.acquire(1)
    checks["same_time_events_follow_explicit_call_order"] = pending["reason"] == "pending_observation" and denied["reason"] == "open" and exact_probe["reason"] == "probe"
    # This group compares 384 complete small traces, not 384 extra check groups.
    oracle_cases = 0
    oracle_ok = True
    times = [0, 1, 3, 5, 8, 13]
    for threshold, cooldown, outcomes in itertools.product([1, 2, 3], [2, 5], itertools.product([False, True], repeat=6)):
        actual = SequentialBreaker(threshold, cooldown)
        records = []
        for at, outcome in zip(times, outcomes):
            permit, final = _instant(actual, at, outcome)
            records.append((permit["allowed"], permit["reason"], final["state"], final["consecutive_failures"], final["open_until_ms"]))
        oracle_ok &= records == independent_reference(threshold, cooldown, times, outcomes)
        oracle_cases += 1
    checks["independent_outcome_history_interval_oracle"] = bool(oracle_ok and oracle_cases == 384)
    copied = b.view(); copied["state"] = "fabricated"
    completed_ticket = probe2["ticket"]
    checks["snapshot_and_completed_ticket_do_not_rewrite_state"] = b.view()["state"] == "CLOSED" and _rejects_unchanged(b, lambda: b.complete(2010, completed_ticket, False))
    assert all(type(ok) is bool and ok for ok in checks.values()), checks
    return {"python": platform.python_version(), "dependencies": {"standard_library_only": True},
            "example": {"threshold": 2, "cooldown_ms": 1000, "synthetic_trace": trace,
                        "final_state": b.view()}, "oracle_traces_inside_one_group": oracle_cases,
            "checks": checks, "limitations": ["No callable, service, network, sleep, retry or wall clock",
                "One outstanding fixture observation; sequential non-thread-safe model, not concurrent probe coordination",
                "Caller-supplied success/failure is not a measured health signal or availability guarantee"]}


if __name__ == "__main__":
    print(json.dumps(run_example(), ensure_ascii=False, indent=2))

در پاسخ دستی، شکست‌های صفر و ده، حالت را تا ۱۰۱۰ باز می‌کنند. درخواست پانصد، ۵۱۰ میلی‌ثانیه فاصلهٔ باقی‌مانده می‌بیند؛ درخواست ۱۰۰۹ فقط یک میلی‌ثانیه. آزمون ۱۰۱۰ شمارهٔ سوم را می‌گیرد؛ درخواست دومِ همان لحظه به علت مشاهدهٔ ناتمام رد می‌شود. شکستِ آزمون، مهلت تازهٔ ۲۰۱۰ و شمارندهٔ سه را می‌سازد.

در ۲۰۰۹ هنوز یک میلی‌ثانیه باقی است. آزمون ۲۰۱۰ شمارهٔ چهارم را می‌گیرد و موفقیتِ ساختگی آن، حالت بسته با شمارندهٔ صفر می‌دهد. عدد فاصلهٔ باقی‌مانده رزرو نوبت نیست؛ صرفاً فاصله تا مرزِ فعلی است. در دادهٔ نامعتبر، شمارهٔ ناسازگار یا نتیجهٔ غیرِبولی، حالت و ساعتِ ثبت‌شده تغییر نمی‌کنند.

هفده گروه بررسی اجرا شده‌اند. مرجع مستقل به‌جای کلاسِ حالت، تاریخچهٔ نتیجه‌های پذیرفته‌شده و بازه‌های مسدود را می‌خواند و طول دنبالهٔ شکست پس از آخرین موفقیت را حساب می‌کند. ۳۸۴ خط زمانیِ کوچک داخل یک گروه مقایسه می‌شوند؛ جدول دستی، شمارهٔ قدیمی، برابری زمان و بازنشانی دستی جدا بررسی شده‌اند. اجرای دوباره، گروه تازه نیست.

۵. مدل ترتیبی را تضمین هم‌زمانی یا SLA ندانیم

یک آزمون ترتیبی هماهنگی هم‌زمان درخواست‌ها را ثابت نمی‌کند
تصویر مفهومی: یک آزمون ترتیبی هماهنگی هم‌زمان درخواست‌ها را ثابت نمی‌کند؛ بدون مقیاس عددی و مستقل از نتایج اجرای کد.

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

{
  "python": "3.14.5",
  "dependencies": {
    "standard_library_only": true
  },
  "example": {
    "threshold": 2,
    "cooldown_ms": 1000,
    "synthetic_trace": [
      {
        "operation": "acquire",
        "at_ms": 0,
        "result": {
          "allowed": true,
          "reason": "normal",
          "ticket": 1,
          "retry_after_ms": 0,
          "state": "CLOSED",
          "consecutive_failures": 0,
          "open_until_ms": null,
          "pending_ticket": 1,
          "next_ticket": 2,
          "last_ms": 0
        }
      },
      {
        "operation": "complete",
        "at_ms": 0,
        "success": false,
        "result": {
          "state": "CLOSED",
          "consecutive_failures": 1,
          "open_until_ms": null,
          "pending_ticket": null,
          "next_ticket": 2,
          "last_ms": 0
        }
      },
      {
        "operation": "acquire",
        "at_ms": 10,
        "result": {
          "allowed": true,
          "reason": "normal",
          "ticket": 2,
          "retry_after_ms": 0,
          "state": "CLOSED",
          "consecutive_failures": 1,
          "open_until_ms": null,
          "pending_ticket": 2,
          "next_ticket": 3,
          "last_ms": 10
        }
      },
      {
        "operation": "complete",
        "at_ms": 10,
        "success": false,
        "result": {
          "state": "OPEN",
          "consecutive_failures": 2,
          "open_until_ms": 1010,
          "pending_ticket": null,
          "next_ticket": 3,
          "last_ms": 10
        }
      },
      {
        "operation": "acquire",
        "at_ms": 500,
        "result": {
          "allowed": false,
          "reason": "open",
          "ticket": null,
          "retry_after_ms": 510,
          "state": "OPEN",
          "consecutive_failures": 2,
          "open_until_ms": 1010,
          "pending_ticket": null,
          "next_ticket": 3,
          "last_ms": 500
        }
      },
      {
        "operation": "acquire",
        "at_ms": 1009,
        "result": {
          "allowed": false,
          "reason": "open",
          "ticket": null,
          "retry_after_ms": 1,
          "state": "OPEN",
          "consecutive_failures": 2,
          "open_until_ms": 1010,
          "pending_ticket": null,
          "next_ticket": 3,
          "last_ms": 1009
        }
      },
      {
        "operation": "acquire",
        "at_ms": 1010,
        "result": {
          "allowed": true,
          "reason": "probe",
          "ticket": 3,
          "retry_after_ms": 0,
          "state": "HALF_OPEN",
          "consecutive_failures": 2,
          "open_until_ms": 1010,
          "pending_ticket": 3,
          "next_ticket": 4,
          "last_ms": 1010
        }
      },
      {
        "operation": "acquire",
        "at_ms": 1010,
        "result": {
          "allowed": false,
          "reason": "pending_observation",
          "ticket": null,
          "retry_after_ms": null,
          "state": "HALF_OPEN",
          "consecutive_failures": 2,
          "open_until_ms": 1010,
          "pending_ticket": 3,
          "next_ticket": 4,
          "last_ms": 1010
        }
      },
      {
        "operation": "complete",
        "at_ms": 1010,
        "success": false,
        "result": {
          "state": "OPEN",
          "consecutive_failures": 3,
          "open_until_ms": 2010,
          "pending_ticket": null,
          "next_ticket": 4,
          "last_ms": 1010
        }
      },
      {
        "operation": "acquire",
        "at_ms": 2009,
        "result": {
          "allowed": false,
          "reason": "open",
          "ticket": null,
          "retry_after_ms": 1,
          "state": "OPEN",
          "consecutive_failures": 3,
          "open_until_ms": 2010,
          "pending_ticket": null,
          "next_ticket": 4,
          "last_ms": 2009
        }
      },
      {
        "operation": "acquire",
        "at_ms": 2010,
        "result": {
          "allowed": true,
          "reason": "probe",
          "ticket": 4,
          "retry_after_ms": 0,
          "state": "HALF_OPEN",
          "consecutive_failures": 3,
          "open_until_ms": 2010,
          "pending_ticket": 4,
          "next_ticket": 5,
          "last_ms": 2010
        }
      },
      {
        "operation": "complete",
        "at_ms": 2010,
        "success": true,
        "result": {
          "state": "CLOSED",
          "consecutive_failures": 0,
          "open_until_ms": null,
          "pending_ticket": null,
          "next_ticket": 5,
          "last_ms": 2010
        }
      }
    ],
    "final_state": {
      "state": "CLOSED",
      "consecutive_failures": 0,
      "open_until_ms": null,
      "pending_ticket": null,
      "next_ticket": 5,
      "last_ms": 2010
    }
  },
  "oracle_traces_inside_one_group": 384,
  "checks": {
    "first_failure_remains_closed": true,
    "threshold_failure_opens_from_completion_time": true,
    "blocked_attempt_does_not_extend_deadline": true,
    "deadline_before_and_exact_equality": true,
    "pending_probe_denies_another_permission": true,
    "failed_probe_restarts_full_cooldown": true,
    "successful_probe_closes_and_clears_streak": true,
    "normal_success_breaks_failure_streak": true,
    "manual_reset_clears_open_without_recycling_ticket": true,
    "manual_reset_cannot_discard_pending_observation": true,
    "wrong_stale_and_missing_completions_rejected": true,
    "outcome_requires_actual_bool_without_mutation": true,
    "invalid_and_backwards_clock_is_not_committed": true,
    "configuration_integer_and_bound_contract": true,
    "same_time_events_follow_explicit_call_order": true,
    "independent_outcome_history_interval_oracle": true,
    "snapshot_and_completed_ticket_do_not_rewrite_state": true
  },
  "limitations": [
    "No callable, service, network, sleep, retry or wall clock",
    "One outstanding fixture observation; sequential non-thread-safe model, not concurrent probe coordination",
    "Caller-supplied success/failure is not a measured health signal or availability guarantee"
  ]
}

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

منابع در ۲۵ مهر ۱۴۰۵ خوانده شدند. تاریخ مقالهٔ Fowler، شش مارس ۲۰۱۴ است؛ زمان دسترسی و تاریخ‌های ویرایش مستندات از آن جدا ثبت شده‌اند. نه دسترس‌پذیری، نه کاهش خطا و نه توافق سطح خدمت اندازه‌گیری شده است. موفقیتِ ورودی یک فرضِ دادهٔ نمونه است و گواه سلامت یک خدمت واقعی نیست.

پرسش اول: چرا ردِ پانصد، مهلت را به ۱۵۰۰ نبرد؟ پاسخ: فقط ثبتِ شکستِ مجاز، انتظار را آغاز می‌کند؛ درخواستِ مسدود نتیجه‌ای ندارد که شمارنده یا مهلت را عوض کند.

پرسش دوم: شکست آزمون در ۱۰۱۰ چه می‌دهد؟ پاسخ: حالت باز، شمارندهٔ سه و مهلت ۲۰۱۰؛ مدت انتظار کامل از زمان ثبت همان شکست شروع می‌شود.

پرسش سوم: اگر نتیجهٔ آزمون نرسد چه می‌شود؟ پاسخ: مشاهده ناتمام می‌ماند و درخواست تازه رد می‌شود؛ این مدل زمان‌سنجِ انقضای عملیات ندارد.

پرسش چهارم: آیا موفقیتِ ۲۰۱۰ سلامت سرویس را ثابت می‌کند؟ پاسخ: خیر؛ فقط برچسبِ ساختگی را به انتقال حالت تبدیل کرده‌ایم. سرویس، سنجهٔ سلامت و ارزیابی میدانیِ مستقلی اجرا نشده‌اند.

منابع و مطالعهٔ بیشتر

بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیل‌ها و پیشنهادهای اجرایی، جمع‌بندی نویسنده‌اند.