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

سه نام حالت داریم: 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، شش مارس ۲۰۱۴ است؛ زمان دسترسی و تاریخهای ویرایش مستندات از آن جدا ثبت شدهاند. نه دسترسپذیری، نه کاهش خطا و نه توافق سطح خدمت اندازهگیری شده است. موفقیتِ ورودی یک فرضِ دادهٔ نمونه است و گواه سلامت یک خدمت واقعی نیست.
پرسش اول: چرا ردِ پانصد، مهلت را به ۱۵۰۰ نبرد؟ پاسخ: فقط ثبتِ شکستِ مجاز، انتظار را آغاز میکند؛ درخواستِ مسدود نتیجهای ندارد که شمارنده یا مهلت را عوض کند.
پرسش دوم: شکست آزمون در ۱۰۱۰ چه میدهد؟ پاسخ: حالت باز، شمارندهٔ سه و مهلت ۲۰۱۰؛ مدت انتظار کامل از زمان ثبت همان شکست شروع میشود.
پرسش سوم: اگر نتیجهٔ آزمون نرسد چه میشود؟ پاسخ: مشاهده ناتمام میماند و درخواست تازه رد میشود؛ این مدل زمانسنجِ انقضای عملیات ندارد.
پرسش چهارم: آیا موفقیتِ ۲۰۱۰ سلامت سرویس را ثابت میکند؟ پاسخ: خیر؛ فقط برچسبِ ساختگی را به انتقال حالت تبدیل کردهایم. سرویس، سنجهٔ سلامت و ارزیابی میدانیِ مستقلی اجرا نشدهاند.
منابع و مطالعهٔ بیشتر
- Microsoft Azure Architecture؛ الگوی Circuit Breaker و ملاحظات آن
- Martin Fowler؛ توضیح اصلی Circuit Breaker و بازنشانی
- Python؛ Enum و نامهای نمادین حالت
بررسی منابع: ۲۵ مهر ۱۴۰۵. تحلیلها و پیشنهادهای اجرایی، جمعبندی نویسندهاند.

