11 تمرین ۱ — شبیهسازِ ابهامِ timeoutExercise 1 — the timeout-ambiguity simulator
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- یک سرویس شمارنده، یک کلاینت و در دمو حداکثر دو تلاش برای هر درخواست (اعداد آموزشی).
- حساب retry لایهای با ۳ لایه و ۳ تلاش برای هرکدام است (اعداد آموزشی) — شکل مهم است نه ثابتها.
- لاگ نتیجه سهتایی (شناسه درخواست، hashpayload و نتیجه) را بیشتر از پنجرهٔ حداکثر retry نگه میدارد (نگهداری آموزشی) — retry بعد از پنجره شکست صریح است نه حدس.
- One counter service, one client, at-most-two attempts per request in the demo (teaching numbers).
- Layered-retry math uses 3 layers x 3 attempts each (teaching numbers) — the shape matters, not the constants.
- The result log keeps (request ID, payload hash, result) past the maximum retry window (teaching retention) — retry after the window is fail-loud, not guesswork.
چرا این روش و این عددها؟Why this method and these numbers?
timeout فقط نبود پاسخ را نشان میدهد، نه اجرا نشدن درخواست؛ به همین دلیل retry همان intent باید ID قبلی را حمل کند. سه لایه با سه attempt میتوانند ۳×۳×۳=۲۷ فراخوان پاییندست بسازند، چون هر تلاش لایهٔ بالا تمام تلاشهای پایین را دوباره فعال میکند. deadline مشترک، retry budget و jitter بار را محدود میکنند. دو attempt در دمو برای دیدن duplicate کافی است؛ عدد production باید از نرخ خطای گذرا و ظرفیت باقیمانده بیاید.
Timeout proves no response arrived, not that execution did not happen; retry the same intent with the same ID. Three layers with three attempts can cause 3×3×3=27 downstream calls because each upper attempt repeats lower attempts. Shared deadlines, retry budgets and jitter bound load. Two demo attempts expose duplication; production counts depend on transient errors and spare capacity.
مدل ذهنیMental model
حل گامبهگامStep by step
- timeout یعنی «اجرا نشد» نیست اول ناشناخته را نام ببر: timeout با سه history میخواند — درخواست گم (هیچی اعمال نشده)، سرور مرده (شاید اعمال شده) و پاسخ گم (اعمالشدهٔ تأییدنشده). هیچ مدرک سمت کلاینت آنها را جدا نمیکند، پس طراحی باید زیر هر سه کار کند.
- شناسهٔ ثابت، نتیجهٔ ذخیرهشده به قصد یک شناسه بده و به سرور یک حافظه: کلاینت برای هر قصد یک UUID میسازد و در retryها نگهش میدارد؛ سرور (شناسه، hashpayload و نتیجه) را اتمیک درج-یا-lookup میکند و روی تکرار نتیجهٔ ذخیرهشده را برمیگرداند.
- گره payload، نگهداری نتایج شناسه را به payload گره بزن و بیشتر از پنجرهٔ retry نگهش دار: همان شناسه با payload متفاوت رد میشود (باگ کلاینت که بلند گرفته میشود) و lookupهای بعد از انقضای نگهداری شکست صریح میخورند بهجای اعمال کور.
- بودجه برای لایهها طوفان را کران کن نه فقط تلاش را: deadline هر تلاش دم را میبرد، backoff بهعلاوهٔ jitter گله را پخش میکند و بودجهٔ مشترک retry بار را shed میکند — در حساب آموزشی ۳ لایه × ۳ تلاش در اوج ۲۷ تلاش downstream میشوند.
- Timeout is not 'not executed' Name the unknown first: a timeout fits three histories — request lost (nothing applied), server dead (maybe applied), reply lost (applied, unconfirmed). No client-side evidence separates them, so the design must work under all three.
- Same ID, stored result Give the intent one ID and the server one memory: the client mints a UUID per intent and keeps it across retries; the server inserts-or-looks-up (ID, payload hash, result) atomically, returning the stored result on repeats.
- Bind payload, retain results Bind the ID to the payload and keep it past the retry window: the same ID with a different payload is rejected (client bug, caught loud), and lookups after retention expiry fail loud instead of applying blind.
- Budget the layers Bound the storm, not just the attempt: per-attempt deadlines cut tails, backoff plus jitter spreads the herd, and a shared retry budget sheds load — 3 layers x 3 attempts peak at 27 downstream tries in the teaching math.
طراحی مشخصConcrete design
شناسهٔ درخواست: یک UUID برای هر قصد caller که کلاینت قبل از اولین ارسال میسازد و در retryها نگه میدارد. جدول سرور (request_id UNIQUE, payload_hash, result, kept_until): هنگام رسیدن، درج-یا-lookup در یک گام اتمیک — شناسهٔ حاضر نتیجهٔ ذخیرهشده را برمیگرداند (یا روی ناسازگاری payload رد میکند) و شناسهٔ غایب یکبار اعمال و ذخیره میشود. نگهداری طولانیترین پنجرهٔ retry بهعلاوهٔ slack ساعت را پوشش میدهد؛ خواندن بعد از انقضا شکست صریح است. هر retry همان شناسه را دارد با deadline هر تلاش، backoff و jitter، و از بودجهٔ مشترک retry خرج میکند که هنگام کندی downstream بار را shed میکند.
Request ID: one UUID per caller intent, minted by the client before the first send and kept across retries. Server table (request_id UNIQUE, payload_hash, result, kept_until): on arrival, insert-or-lookup in one atomic step — a present ID returns its stored result (or rejects on payload mismatch), an absent ID applies once and stores. Retention covers the longest retry window plus clock slack; reads after expiry fail loud. Every retry carries the same ID, a per-attempt deadline, backoff with jitter, and charges a shared retry budget that sheds load when the downstream is slow.
مثال محاسبهشدهWorked example
حساب آموزشی: ۳ لایه و هرکدام تا ۳ تلاش، پس بدترین fan-out به downstream برای یک تلاش caller برابر ۳×۳×۳ = ۲۷ تلاش است. بودجهٔ مشترک retry به اندازهٔ آموزشی ۱۰٪ ترافیک یعنی ۲۸مین retry همزمان بهجای پیوستن به رمّه صبر میکند یا بلند شکست میخورد. backoff با jitter بازماندهها را پخش میکند: بدون آن هر ۲۷تایی همقدم میرسند؛ با آن در پنجرهٔ backoff پخش میشوند و downstream میان موجها نفس میکشد.
Teaching math: 3 layers, up to 3 attempts each, so the worst downstream fan-out is 3 x 3 x 3 = 27 attempts for one caller try. A shared retry budget of a teaching 10% of traffic means the 28th concurrent retry waits or fails loud instead of joining the stampede. Backoff with jitter spreads the survivors: without it all 27 arrive in lockstep; with it they spread across the backoff window and the downstream recovers between waves.
دانلود نمودار مستقلDownload standalone diagram
- بالا: کلاینت req-7 را با payload هفت میفرستد؛ سرویس اعمالش میکند و counter=1 را در لاگ نتیجه ثبت میکند — یک قصد caller و یک شناسه.
- پاسخ روی سیم گم میشود، پس کلاینت با نتیجهٔ UNKNOWNtimeout میگیرد — اعمال شده یا نه، سیم یکسان دیده میشود.
- کلاینت از بودجه خرج میکند، backoff بهعلاوهٔ jitter صبر میکند و همان req-7 را با همان payload تکرار میکند — هرگز شناسهٔ تازه.
- سرور req-7 را lookup میکند، counter=1 را پیدا میکند و نتیجهٔ ذخیرهشده را بدون اثر دوم برمیگرداند؛ استفادهٔ دوبارهٔ شناسه با payload تازه رد میشود.
- Top: the Client sends req-7 with payload P; the Service applies it and records counter=1 in the Result Log — one caller intent, one ID.
- The reply is lost on the wire, so the client times out with outcome UNKNOWN — applied or not, the wire looks the same.
- The client charges the budget, waits backoff plus jitter, and retries the SAME req-7 with the same payload — never a fresh ID.
- The server looks req-7 up, finds counter=1, and returns the stored result with no second effect; reusing the ID with a new payload is rejected.
نقطهٔ تصمیم: نقطهٔ تصمیم پیکان lookup-قبل-از-اعمال است: هر رسیدن، چه اول چه retry، قبل از هر اثر از lookup شناسه میگذرد. بدون lookup، امنیتی نیست.
رد خرابی: پیکان چین پاسخ را دنبال کن: اثر اعمال شده (counter=1 در فروشگاه) ولی کلاینت timeout میگیرد. حالا یک retry سادهلوحانه با شناسهٔ تازه را در همان مسیر دنبال کن: فروشگاه شناسهٔ ناآشنا میبیند و دوباره اعمال میکند — counter=2. خرابی روی سیم نامرئی است؛ فقط شمارش اثر آن را لو میدهد.
تضمین نشاندادهشده: شکل نشان میدهد retry با lookup نتیجه در یک اثر جمع میشود و ناسازگاری payload رد میشود. طوفان retry میان لایهها را کران نمیکند — آن کار نمودار اسمبل است.
تضمین نشانندادهشده: دربارهٔ مدت نگهداری نتایج، سرنوشت بعد از انقضا و ضرب تلاشها میان لایهها چیزی نمیگوید — همه بیرون این شکلاند.
Decision point: The decision point is the lookup-before-apply arrow: every arrival, first or retry, passes the request-ID lookup before any effect. No lookup, no safety.
Failure trace: Trace the dashed reply arrow: the effect is applied (counter=1 in the store) yet the client times out. Now trace a naive retry with a fresh ID down the same path: the store sees an unknown ID and applies again — counter=2. The failure is invisible on the wire; only the effect count tells it.
Guarantee shown: The figure shows the retry collapsing into one effect via the result lookup, and the payload-mismatch rejection. It does not bound retry storms across layers — that is the assembly diagram's job.
Not shown: It says nothing about how long results are kept, what happens after retention expires, or how layers multiply attempts — all outside this figure.
خودآزماییSelf-check
پاسخ گم شده ولی اثر اعمال شده. چرا retry با شناسهٔ ثابت دو بار شارژ نمیکند؟
پاسخ: چون سرور قبل از اعمال شناسه را lookup میکند: req-7 تکراری به counter=1 ذخیرهشده میخورد و همان را برمیگرداند. شناسهٔ تازه نو بهنظر میرسد، پس بار دوم اعمال میشود.
The reply was lost but the effect applied. Why does the same-ID retry not double-charge?
Answer: Because the server looks the ID up before applying: the retried req-7 hits the stored counter=1 and returns it. A fresh ID looks new, so it applies a second time.
دانلود نمودار مستقلDownload standalone diagram
- سؤال اول — Idempotent؟ بله به Retry Same ID میرسد زیر نظر Budget Gate (با deadline و backoff و jitter و سقف ۲۷).
- نه به سؤال دوم میرسد — Queryable؟ بله به Read Back Result میرسد: قبل از دست زدن به چیزی از لاگ نتیجه بپرس.
- دوباره نه به سومی میرسد — Needs Guard؟ بله به Guarded Write فنسدار میرسد؛ نه در Fail Explicitly تمام میشود.
- هر سه برگ بله در یک اثر تمام میشوند (dedup یا نتیجهٔ ذخیره یا fencing)؛ برگ شکست در خطای صادقانه تمام میشود، هرگز حدس.
- First question — Idempotent? Yes drops to Retry Same ID, governed by the Budget Gate (deadline, backoff plus jitter, 27 max).
- No leads to the second question — Queryable? Yes drops to Read Back Result: ask the result log before touching anything.
- No again leads to the third — Needs Guard? Yes drops to the fenced Guarded Write; no ends at Fail Explicitly.
- All three yes-leaves end in one effect (dedup, stored result, or fencing); the fail leaf ends in an honest error, never a guess.
نقطهٔ تصمیم: نکته ترتیب تصمیم است: idempotency قبل از خواندن وضعیت، خواندن قبل از محافظت، محافظت قبل از شکست. جلو زدن (retry بدون شناسه یا محافظت بدون خواندن) همانجاست که corruption وارد میشود.
رد خرابی: یک downstream کند بدون بودجه را دنبال کن: هر کلاینت همزمان retry میکند، سه لایه هر تلاش را سهبرابر میکنند و ۲۷ تلاش جایی مینشیند که یکی کافی بود — گلهٔ رعدآسا. برگ Budget Gate تنها پیکانی است که جلویش را میگیرد: تلاشها تمام میشوند، انتظارها پخش میشوند و deadline دم را میبرد.
تضمین نشاندادهشده: شکل چهار پاسخ را مرتب میکند و نشان میدهد بودجه retryها را اداره میکند. ثابتها (۳×۳ و پایهٔ backoff) را اثبات نمیکند — آنها اعداد آموزشیاند که برای هر وابستگی اندازه میگیری.
تضمین نشانندادهشده: دربارهٔ اینکه نوشتهٔ محافظتشده چه چیزی را بررسی میکند (آن تمرین fencing است) یا نگهداری نتایج چیزی نمیگوید.
Decision point: The decision order is the point: idempotency before read-back, read-back before guarding, guarding before failing. Skipping ahead (retry without an ID, guard without a read) is where corruption enters.
Failure trace: Trace a slow downstream with no budget: every client retries at once, three layers triple each attempt, and 27 tries land where one sufficed — the thundering herd. The Budget Gate leaf is the only arrow that stops it: attempts run out, waits spread out, the deadline cuts the tail.
Guarantee shown: The figure orders the four responses and shows the budget governing retries. It does not prove the constants (3x3, the backoff base) — those are teaching numbers you measure per dependency.
Not shown: It does not cover what the guarded write checks (that is the fencing exercise) or how long results are retained.
خودآزماییSelf-check
سه لایه هرکدام تا سه بار retry میکنند. بدترین fan-out به downstream چقدر است و کدام دستگیره سقفش میزند؟
پاسخ: ۲۷: هر ۳ لایه یک تلاش را تا ۳ تلاش میکنند، پس ۳×۳×۳ تلاش downstream. بودجه تلاشها را سقف میزند، deadline دم را میبرد و jitter گله را پخش میکند.
Three layers each retry up to three times. What is the worst downstream fan-out, and which knob caps it?
Answer: 27: each of the 3 layers turns 1 attempt into up to 3, so 3×3×3 downstream tries. The budget caps attempts, the deadline cuts the tail, jitter spreads the herd.
گزینههای جایگزینAlternatives
- اول خواندن وضعیت وقتی نتیجه قابل استعلام است («req-7 را اعمال کردی؟»): یک lookup میتواند ناشناخته را بدون هیچ سیاست retry حل کند. وقتی خواندن ارزان و لاگ نتیجه با شناسه درخواست ایندکس شده، ارزانتر از idempotency کامل است — ولی وقتی عملیات حالت خواندنی ندارد بیفایده است.
- شکست صریح وقتی عملیات نه idempotent است نه قابل استعلام: خطای صریح برگردان و بگذار انسان یا لایه بالاتر مغایرت را حل کند. قیمتش یک شکست visible است و سودش صفر corruption — وقتی شارژ دوباره بدتر از درخواست ردشده است، تریدآف درست همین است.
- Read-back-first when the outcome is queryable ('did you apply req-7?'): one lookup can settle the unknown without any retry policy. Cheaper than full idempotency when reads are cheap and the result log is indexed by request ID — but useless when the operation has no readable state.
- Fail-loud when the operation is neither idempotent nor queryable: return an explicit error and let a human or a higher layer reconcile. Costs a visible failure, buys zero corruption — the right trade when a double charge is worse than a rejected request.
سناریوهای خرابیFailure scenarios
- طوفان retry روی downstream کند: بدون بودجه هر کلاینت همزمان میکوبد و retryهای لایهای چندبرابر میشوند (تا ۲۷ برابر). نگهبان: بودجهٔ مشترک retry، backoff بهعلاوهٔ jitter، deadline هر تلاش و shed کردن بار وقتی بودجه خالی شد.
- گم شدن ورودی لاگ نتیجه قبل از پایان نگهداری (خرابی دیسک یا انقضای زود): retry بعد نو بهنظر میرسد و دو بار اعمال میشود. نگهبان: ذخیرهٔ بادوام و تکثیرشدهٔ نتیجه و شکست صریح بعد از پنجرهٔ نگهداری بهجای حدس.
- قاطی payload: دو قصد متفاوت caller یک شناسه را شریک میشوند (باگ کلاینت). بررسی binding دومین payload را رد میکند — بلند و امن و قابل debug — بهجای اعمال بیصدای قصد اشتباه.
- Retry storm on a slow downstream: without a budget every client hammers at once and layered retries multiply (up to 27x). Guard with a shared retry budget, backoff plus jitter, per-attempt deadlines, and load shedding when the budget drains.
- Result-log entry lost before retention ends (disk loss, early expiry): a retry then looks new and double-applies. Guard with durable, replicated result storage and fail-loud after the retention window instead of guessing.
- Payload confusion: two different caller intents share one ID (client bug). The binding check rejects the second payload — loud, safe, and debuggable — instead of silently applying the wrong intent.
کد قابل اجراRunnable code
اجراشده و تأییدشده در همین محیطExecuted and verified in this environment
این کد آموزشی و سادهسازیشده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچکس اینشکلی کد پروداکشن نمیزند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.
import java.util.*;
// Teaching demo: after a timeout the client knows NOTHING - the request may
// be lost, the server may have crashed, or the reply may be lost. Retry with
// the SAME request ID collapses repeats into one effect; a fresh ID doubles it.
public class TimeoutDemo {
// What really happened on the server for one client request.
enum Truth { LOST_REQUEST, CRASHED_BEFORE_APPLY, APPLIED_REPLY_LOST }
static class Server {
int effects = 0; // real-world effects applied
Map<String, String> results = new HashMap<>(); // request ID -> stored result
Map<String, String> payloadOf = new HashMap<>(); // request ID -> bound payload
// Returns the result string; null means "client never sees it".
String apply(String reqId, String payload, boolean replyLost) {
if (results.containsKey(reqId)) {
if (!payloadOf.get(reqId).equals(payload)) return "REJECTED: id reused with new payload";
return results.get(reqId) + " (duplicate suppressed)";
}
payloadOf.put(reqId, payload);
effects++;
String r = "ok counter=" + effects;
results.put(reqId, r);
return replyLost ? null : r;
}
}
// One client request with ONE retry. naive=false reuses the request ID.
static String run(Truth truth, boolean naive) {
Server s = new Server();
String id = "req-7";
String first = null;
switch (truth) {
case LOST_REQUEST -> first = null; // never arrived
case CRASHED_BEFORE_APPLY -> first = null; // died doing nothing
case APPLIED_REPLY_LOST -> first = s.apply(id, "pay-100", true);
}
boolean timeout = (first == null); // client only sees silence
String retryId = naive ? "req-7-again" : id; // naive = fresh ID
String second = s.apply(retryId, "pay-100", false);
return "timeout=" + timeout + " outcome=UNKNOWN retry=" + second + " effects=" + s.effects;
}
public static void main(String[] a) {
for (Truth t : Truth.values()) {
System.out.println(t + " naive-retry -> " + run(t, true));
System.out.println(t + " same-id-retry -> " + run(t, false));
}
int layers = 3, attempts = 3; // teaching numbers
int worst = (int) Math.pow(attempts, layers); // 3^3
System.out.println("layered worst case = " + attempts + "^" + layers + " = " + worst + " downstream attempts");
Server s = new Server();
s.apply("req-7", "pay-100", false);
System.out.println("payload binding -> " + s.apply("req-7", "pay-999", false));
System.out.println("OK: unknown stays unknown; same ID keeps one effect");
}
}
اجرا: javac TimeoutDemo.java && java TimeoutDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: شش خط سناریو: retry سادهلوحانه فقط در حالت APPLIED_REPLY_LOST دو اثر میسازد (counter=2 و effects=2)؛ retry با شناسهٔ ثابت در هر سه حالت counter=1 و effects=1 نگه میدارد. بعد ۳^۳ = ۲۷، خط REJECTED ناسازگاری payload و خط OK. (خروجی واقعی همین محیط)
Run: javac TimeoutDemo.java && java TimeoutDemo — stdlib Java only, instant.
Expected output: Six scenario lines: naive retry double-applies only in APPLIED_REPLY_LOST (counter=2, effects=2); same-ID retry keeps counter=1 and effects=1 in all three truths. Then 3^3 = 27, the payload-mismatch REJECTED line, and the OK line. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): سه لایه هر کدام سه بار retry کنند، حداکثر چند تلاش downstream داریم؟Follow-up (Senior): Three layers each make up to three attempts. What is the maximum number of downstream attempts?
۲۷. هر لایه یک تلاش ورودی را تا سه تلاش downstream میکند و لایهها ضربی روی هم مینشینند: ۳×۳×۳. این بدترین حالت بدون بودجه و بدون deadline است که دم را ببرد — عدد آموزشی فرض میکند هر لایه تلاشهایش را تمام میکند. در پروداکشن با بودجهٔ مشترک retry (کسری از ترافیک)، deadline هر تلاش و backoff jitterدار سقفش میزنی تا ۲۷تایی هرگز همقدم نرسند.
27. Each layer turns one incoming attempt into up to three downstream attempts, and the layers stack multiplicatively: 3 x 3 x 3. That is the worst case with no budget and no deadline cutting tails — the teaching number assumes every layer exhausts its attempts. In production you cap it with a shared retry budget (a fraction of traffic), per-attempt deadlines, and jittered backoff so the 27 never arrive in lockstep.
پیگیری (استف): downstream کند است و هر کلاینت retry میکند. اول چه چیزی میشکند و کدام دستگیره را میچرخانی؟Follow-up (Staff): The downstream is slow and every client retries. What breaks first, and which knob do you turn?
اول بودجه و بعد deadline — بهعلاوهٔ jitter. اول بودجهٔ مشترک retry وقتی downstream از قبل مریض است تلاش تازه را متوقف میکند (این تنها دستگیرهای است که بار سرتاسری را میبیند). بعد deadline هر تلاش، تلاش آویزان را میبرد تا یک وابستگی کند نتواند همهٔ workerها را نگه دارد. jitter بازماندهها را پخش میکند تا همقدم نرسند و shed کردن بار اضافه را با خطای صریح میاندازد بهجای صف کردنش. بدون بودجه، backoff بهتنهایی فقط همان رمّهٔ ۲۷برابری را عقب میاندازد.
The budget and the deadline, in that order — plus jitter. First the shared retry budget stops new attempts when the downstream is already sick (this is the only knob that sees global load). Then per-attempt deadlines cut each hanging try so one slow dependency cannot hold every worker. Jitter spreads the survivors so they do not arrive in lockstep, and load shedding drops the excess with an explicit error instead of queueing it. Without the budget, backoff alone just delays the same 27x stampede.
پیگیری (استف): در overload چرا retry و rate limit را با هم طراحی میکنی؟Follow-up (Staff): Why design retries and rate limiting together?
retry ورودی تازه تولید میکند ولی ظرفیت سرویس را بیشتر نمیکند. در token bucket، refill r درخواست/ثانیه از ظرفیت پایدارِ اندازهگیریشده با حاشیه میآید و burst b از کار اضافی قابلتحمل در deadline صف: اگر ظرفیت آزاد c و انتظار مجاز T باشد، b≤c×T یک کران آموزشی است. صف را کراندار نگه دار، 429/503 و Retry-After بده، retry را در یک لایه با deadline و jitter محدود کن. ظرفیت و fairness هر tenant را جدا بسنج.
Retries add arrivals, not capacity. Choose token-bucket refill r from measured sustainable capacity with margin and burst b from spare capacity c times the tolerated queue wait T: b≤c×T is a teaching bound. Bound queues, return 429/503 with Retry-After and limit retries to one layer with deadlines and jitter. Measure capacity and per-tenant fairness.
اشتباهات رایجCommon mistakes
- خواندن timeout بهمعنای «اجرا نشد» و ارسال کورکورانهٔ دوباره: در حالت پاسخگمشده اثر از قبل هست، پس ارسال دوباره دوتایش میکند. timeout یعنی ناشناخته — همیشه.
- ساختن شناسهٔ تازه برای هر تلاش: سرور نمیتواند retry را به اصلی وصل کند، پس dedup هرگز عمل نمیکند و هر retry یک شارژ تازه است. شناسه مال قصد caller است نه مال تلاش.
- retry بیکران در هر لایه: سه سیاست معصوم «۳ بار retry» روی هم مینشینند و ۲۷ تلاش downstream میشوند و بدون jitter همقدم میرسند. تلاشها را سرتاسری با بودجه کران کن، نه فقط محلی با شمارش.
- Reading a timeout as 'it did not execute' and blindly resending: in the lost-reply case the effect already exists, so the resend doubles it. Timeout means unknown — always.
- Minting a fresh request ID per attempt: the server cannot link the retry to the original, so dedup never fires and every retry is a new charge. The ID belongs to the caller intent, not to the attempt.
- Unbounded retries at every layer: three innocent-looking 'retry 3 times' policies stack into 27 downstream attempts, and without jitter they arrive in lockstep. Bound attempts globally with a budget, not just locally with a count.
منابع:Sources: AWS · safe retries with idempotent APIs · AWS · timeouts, backoff and jitter
22 تمرین ۲ — clock skew و عدمقطعیت اندازهگیریExercise 2 — clock skew and measurement uncertainty
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- اعداد دمو MOCK آموزشیاند: skew جلو بودن B بهاندازهٔ ۲۵۰ms، RTT نمونه ۴۰ms، گام NTP بهاندازهٔ ۵۰۰ms- و ۱۲۰ms کار واقعی.
- دو ماشین یعنی دو ساعت مستقل (VM/host جدا)؛ یک host با دو کانتینر یعنی یک ساعت.
- رجیستر LWW مقداری را نگه میدارد که بزرگترین timestamp دیواری را دارد و مساویها دلبخواهی شکسته میشوند.
- Demo numbers are teaching MOCKS: +250ms B-ahead skew, 40ms sample RTT, -500ms NTP step, 120ms of real work.
- Two hosts means two independent clocks (separate VMs/hosts); one host with two containers means one clock.
- LWW register keeps the value with the greatest wall timestamp, ties broken arbitrarily.
چرا این روش و این عددها؟Why this method and these numbers?
RTT/2 برآورد تأخیر یکطرفه تحت فرض تقارن است، نه اندازهگیری دقیق آن. اختلاف مسیر رفت و برگشت offset برآوردشده را جابهجا میکند؛ timestamp exchange یک بازهٔ ممکن برای offset میدهد و باید پردازش و drift طی نمونهگیری حساب شوند. ۲۵۰ms جابهجایی و ۵۰۰ms step برای واضحشدن اثر انتخاب شدهاند؛ هر skew بزرگتر از فاصلهٔ واقعی دو write میتواند ترتیب LWW را برعکس کند. monotonic برای مدت است چون با اصلاح ساعت دیواری عقب نمیپرد.
RTT/2 estimates one-way delay under symmetry; it is not a measured one-way delay. Asymmetry shifts offset estimates, so timestamp exchange should report a possible interval with processing and drift accounted for. The 250ms skew and 500ms step make the effect visible; skew exceeding the true write separation can reverse LWW order. Monotonic clocks measure local duration without wall-clock steps.
مدل ذهنیMental model
حل گامبهگامStep by step
- هر ساعت یک کار ساعتها را با کار جدا کن: monotonic برای چقدر طول کشید در یک ماشین — duration و timeout و انتظار TTL؛ دیواری برای ساعت چند است برای انسان و لاگ. گام NTP در دمو ۱۲۰ms کار را به duration دیواری ۳۸۰ms- تبدیل میکند ولی monotonic دقیق میماند.
- دو ماشین بهعلاوهٔ عدمقطعیت skew واقعی را روی دو host/VM مستقل بسنج: timestamp ردوبدل کن، RTT را نصف کن برای عدمقطعیت (RTT برابر ۴۰ms میدهد ۲۰ms+-) و اختلاف خام بهعلاوهٔ عدمقطعیت بهعلاوهٔ روش را گزارش بده. کانتینرهای یک host ساعت مشترک دارند — آن آزمایش از روی ساختار ~۰ اندازه میگیرد.
- آشکارا mock کن یا اصلاً نکن وقتی دو ماشین نداری، آشکارا mock کن: جابهجایی اعلامشده تزریق کن (۲۵۰+ روی B)، به هر عدد جابهجاشده برچسب MOCK بزن و حساب عدمقطعیت را یکسان نگه دار. mock سازوکار را یاد میدهد؛ فقط برچسب است که نمیگذارد اندازهگیری دروغین شود.
- stamp دیواری هرگز داوری نمیکند هرگز روی دیواری مرتب نکن: با ۲۵۰ms جلو بودن B، نوشتهٔ حقیقت-t برای B (با stamp برابر t+250) بر نوشتهٔ حقیقت-t+100 برای A (با stamp برابر t-400) میچربد — LWW نوشتهٔ زودتر را نگه میدارد و دیرتر را بیصدا میاندازد. last-write میانماشینی علیت یا انتظار عدمقطعیت میخواهد، هرگز مقایسهٔ stamp.
- One job per clock Split the clocks by job: monotonic (CLOCK_MONOTONIC) for how-long on one host — durations, timeouts, TTL waits; wall (CLOCK_REALTIME) for what-time for humans and logs. The demo's NTP step turns 120ms of work into a -380ms wall duration while monotonic stays exact.
- Two hosts plus uncertainty Measure real skew on two independent hosts or VMs: exchange timestamps, halve the RTT for the uncertainty (40ms RTT gives +-20ms), and report raw diff plus uncertainty plus method. Containers on one host share the clock — that experiment measures ~0 by construction.
- Mock openly or not at all When you cannot use two hosts, mock openly: inject a stated shift (+250ms on B), label every shifted number MOCK, and keep the uncertainty math identical. A mock teaches the mechanism; only the label keeps it from becoming a false measurement.
- Wall stamps never arbitrate Never order on wall: with B 250ms ahead, B's truth-t write (stamp t+250) beats A's truth-t+100 write (stamp t-400) — LWW keeps the earlier write and drops the later one silently. Cross-host last-write needs causality or an uncertainty wait, never a stamp compare.
مثال محاسبهشدهWorked example
اعداد آموزشی از اجرای دمو: جلو بودن B بهاندازهٔ MOCK برابر ۲۵۰ms؛ یک نمونه روی RTT ساختگی ۴۰ms گزارش میدهد rawDiff=250ms با uncertainty برابر ۲۰ms+- — skew واقعی جایی در ۲۳۰ تا ۲۷۰ms است و نمونهٔ دوم میتواند موجه ۴۰ms اختلاف داشته باشد. گام MOCK برابر NTP بهاندازهٔ ۵۰۰ms- وسط سنجش، ۱۲۰ms کار واقعی را به duration دیواری ۳۸۰ms- تبدیل میکند: زمان منفی، امضای ساعت stepخورده. بعد LWWstamp برابر t+250 (حقیقت t) را با stamp برابر t-400 (حقیقت t+100) مقایسه میکند و نوشتهٔ زودتر را نگه میدارد.
Teaching numbers from the demo run: B ahead by a MOCK 250ms; one sample over a MOCK 40ms RTT reports rawDiff=250ms with uncertainty=+-20ms — the true skew sits somewhere in 230..270ms, and a second sample could legitimately disagree by 40ms. The MOCK NTP step of -500ms mid-measurement turns 120ms of real work into a wall duration of -380ms: negative time, the signature of a stepped clock. LWW then compares stamp t+250 (truth t) against stamp t-400 (truth t+100) and keeps the earlier write.
دانلود SVG مستقلDownload standalone SVG
- تایملاین بالا: دیواری ماشین A وسط سطر با زیگزاگ نارنجی NTP بهاندازهٔ ۵۰۰ms عقب میرود.
- تایملاین پایین: دیواری ماشین B کل مسیر ۲۵۰ms جلوتر است — همان skew ساختگی که روی خود سطر برچسب دارد.
- لوزیها: B در حقیقت t مینویسد (stamp برابر t+250) و A دیرتر در حقیقت t+100 (stamp برابر t-400) — خطچینها ثابت میکنند واقعاً کی دیرتر بوده.
- جعبهٔ رأی بهعلاوهٔ پنل نمونه: LWW همان B را نگه میدارد و نمونهٔ skew با RTT برابر ۴۰ms عدمقطعیت ۲۰ms+- را گزارش میکند.
- Top timeline: host A's wall equals truth until the orange NTP zigzag steps it back 500ms mid-row.
- Bottom timeline: host B's wall runs 250ms ahead the whole way — the MOCK skew, labelled on the row itself.
- The diamonds: B writes at truth t (stamp t+250), A writes later at truth t+100 (stamp t-400) — the dashed lines prove who was really later.
- Verdict plus sample panel: LWW keeps B, and the skew sample reports +-20ms uncertainty from its 40ms RTT.
نقطهٔ تصمیم: نقطهٔ تصمیم خطچینهای حقیقتاند: حقیقت یکسان و stamp متفاوت. هر وقت دو stamp از دو ماشین اختلاف داشتند، به خطهای حقیقت (علیت) اعتماد کن، هرگز به stampها.
رد خرابی: دو لوزی را دنبال کن: B در حقیقت t با stamp برابر t+250 مینویسد و A دیرتر در حقیقت t+100 با stamp برابر t-400 مینویسد (ساعتش عقب رفته). جعبهٔ رأی ۲۵۰ را با ۴۰۰- مقایسه میکند و B را نگه میدارد — نوشتهٔ دیرتر رفته و هیچ خطایی بالا نیامده. آن دفن بیصدا همان حالت خرابی LWW است.
تضمین نشاندادهشده: شکل skew و گام و دفن و عدمقطعیت نمونه را کنار هم نشان میدهد. skew واقعی هیچ ماشینی را اثبات نمیکند — هر عدد جابهجاشده برچسب MOCK دارد.
تضمین نشانندادهشده: نمیگوید timeout تو باید از کدام ساعت استفاده کند — آن قاعدهٔ نمودار اسمبل است.
Decision point: The decision point is the dashed truth-lines: same truth, different stamps. Whenever two stamps from two hosts disagree, believe the truth-lines (causality), never the stamps.
Failure trace: Trace the two diamonds: B writes at truth t with stamp t+250, A writes later at truth t+100 with stamp t-400 (its clock stepped back). The verdict box compares 250 against -400 and keeps B — the later write is gone, and no error is raised. That silent burial is the LWW failure mode.
Guarantee shown: The figure shows the skew, the step, the burial, and the sampling uncertainty side by side. It does not prove any real host's skew — every shifted number wears its MOCK label.
Not shown: It does not tell you which clock your timeout should use — that is the assembly diagram's rule.
خودآزماییSelf-check
با اینکه A در حقیقت دیرتر نوشته ولی LWW همان B را نگه میدارد. کدام دو عدد تصمیم میگیرند و چه چیزی ثابت میکند A دیرتر بوده؟
پاسخ: چون LWW stampهای دیواری را مقایسه میکند نه حقیقت را: stamp برابر t+250 برای B از stamp برابر t-400 برای A میبرد با اینکه A در حقیقت ۱۰۰ms دیرتر نوشته. خطچینهای حقیقت همان مدرکاند.
A wrote later in truth, yet LWW keeps B. Which two numbers decide it, and what proves A was later?
Answer: Because LWW compares wall stamps, not truth: B's stamp (t+250) beats A's stamp (t-400) even though A wrote 100ms later in truth. The dashed truth-lines are the evidence.
دانلود نمودار مستقلDownload standalone diagram
- سؤال Duration؟ بله به برگ Monotonic میرسد: span و timeout و انتظار TTL با CLOCK_MONOTONIC کار میکنند — مقایسهٔ میانماشینی کنارش همان خطر است.
- سؤال Stamp؟ بله به برگ دیواری فقط برای نمایش و ثبت میرسد — با خطر عقبرفتن NTP که زیرش آویزان است.
- سؤال Order؟ بله به منطقی-یا-بازهای (لمپورت و انتظار TrueTime) میرسد — مرتبسازی دیواری بهجایش به دفن LWW ختم میشود.
- سؤال نمونهبرداری؟ دو ماشین مستقل با عدمقطعیت یا MOCK صریحبرچسبدار — بعد قاعده هر چهار جواب را جمع میکند.
- Duration? Yes takes the Monotonic leaf: spans, timeouts and TTL waits run on CLOCK_MONOTONIC — the cross-host compare beside it is the hazard.
- Stamp? Yes takes the Wall leaf for display and record only — with the NTP step-back hazard hanging beneath it.
- Order? Yes takes Logical-or-Interval (Lamport, TrueTime wait) — wall ordering instead ends in the LWW burial.
- Sampling? Two independent hosts with uncertainty, or explicitly labelled MOCK — then the Rule collects all four answers.
نقطهٔ تصمیم: نکته ترتیب سؤال است: اول duration (با monotonic)، دوم stamp (با دیواری و فقط نمایشی) و سوم ترتیب (با منطقی یا بازهای — هرگز دیواری). پرسیدن به ترتیب دیگر همان راهی است که زمان دیواری به تصمیمها میخزد.
رد خرابی: هر سه برگ قرمز خطر را دنبال کن: monotonic میانماشینی دو صفر متفاوت را مقایسه میکند؛ گام NTPspanهای دیواریسنج را منفی میکند؛ LWW با stamp دیواری نوشتهٔ دیرتر را دفن میکند. سه سوءاستفاده و سه corruption بیصدا و صفر خطای بالا آمده.
تضمین نشاندادهشده: شکل هر نیاز را به ساعتش میسپارد و سه سوءاستفاده را نام میبرد. سیاست NTP یا epsilon تو را تعیین نمیکند — آنها برای هر ناوگان اندازهگیری میشوند.
تضمین نشانندادهشده: هیچ عدد skew واقعی را اثبات نمیکند — گام تایملاین فقط اعداد ساختگی صادقانهبرچسبدار نشان میدهد.
Decision point: The decision order is the point: duration first (monotonic), stamp second (wall, display only), order third (logical or interval — never wall). Asking in another order is how wall time sneaks into decisions.
Failure trace: Trace each red hazard leaf: cross-host monotonic compares two different zeroes; the NTP step pushes wall-measured spans negative; wall-stamped LWW buries the later write. Three misuses, three silent corruptions, zero errors raised.
Guarantee shown: The figure assigns every need to its clock and names the three misuses. It does not set your NTP policy or your epsilon — those are measured per fleet.
Not shown: It does not prove any real skew number — the timeline step shows mocked numbers only, honestly labelled.
خودآزماییSelf-check
وسط timeout ساعت دیواری با NTP عقب میرود. timeout باید از کدام ساعت استفاده کند و چرا؟
پاسخ: Monotonic: چون گام دیواری میتواند انتظار باقیمانده را منفی یا کشآمده کند و زود شلیک کند یا آویزان بماند. monotonic در ماشین خودش فقط جلو میرود، پس انتظار صادقانه است.
NTP steps the wall clock backwards mid-timeout. Which clock must the timeout use, and why?
Answer: Monotonic: a wall step can push the remaining wait negative or stretch it, firing early or hanging. Monotonic only moves forward on its own host, so the wait is honest.
گزینههای جایگزینAlternatives
- انتظار عدمقطعیت بهسبک TrueTime: وقتی کران عدمقطعیت ساعت epsilon را داری، نویسنده epsilon بعد از timestamp صبر میکند بعد commit میکند و زمان دیواری را به ترتیب امن تبدیل میکند به قیمت latency. درست وقتی ترتیب میانماشینی روی stamp دیواری اجتنابناپذیر است — برای timeout و duration که monotonic رایگان است زیادهروی است.
- ساعت منطقی برای ترتیب: timestamp لمپورت یا بردار نسخه «کی آخر نوشت» را از علیت میفهمد نه از خوانش دیواری. درست هر وقتی که نویسندهها روی ماشینهاییاند که به ساعتشان اعتماد نداری — که میان ماشینها همیشه همین است.
- TrueTime-style uncertainty waits: when you have a bounded clock uncertainty epsilon, a writer waits epsilon past its timestamp before committing, turning wall time into safe order at the cost of latency. Right when cross-host order on wall stamps is unavoidable — overkill for timeouts and durations, where monotonic is free.
- Logical clocks for order: Lamport timestamps or version vectors decide 'who wrote last' from causality, not from wall readings. Right whenever writers live on machines whose clocks you do not trust — which, across hosts, is always.
سناریوهای خرابیFailure scenarios
- NTP ساعت دیواری را عقب میبرد: هر duration و timeout و TTL دیواریسنج همان لحظه دروغ میگوید (دمو برای ۱۲۰ms کار، ۳۸۰ms- نشان میدهد). نگهبان: همهٔ spanها را با ساعت monotonic بسنج؛ دیواری فقط برای stamp است.
- مقایسهٔ monotonic میان ماشینها: monoA=999 در برابر monoB=5 قاطع بهنظر میرسد و هیچ معنایی ندارد — صفرهای متفاوت و نرخهای متفاوت. نگهبان: هرگز مقایسهشان نکن؛ ترتیب میان ماشینها ساعت منطقی یا بازهٔ عدمقطعیت میخواهد.
- «آزمایش skew» تکماشینی: دو کانتینر روی یک host ساعت زیرین مشترک دارند (مگر namespace زمانی جداشان کند)، پس اختلاف اندازهگیریشده ~۰ است و دربارهٔ skew واقعی هیچچیز را ثابت نمیکند. نگهبان: دو host/VM مستقل یا برچسب MOCK برای جابهجایی و گزارش عدمقطعیت مبتنی بر RTT.
- LWW با stamp دیواری میان نویسندهها: نوشتهٔ زودتر ساعت جلوتر بر نوشتهٔ واقعاً دیرتر میبرد، بیصدا. نگهبان: هرگز روی stamp دیواری مرتب نکن — علیت per-key یا ساعت منطقی یا انتظار عدمقطعیت.
- NTP steps the wall clock backwards: every wall-measured duration, timeout and TTL in flight lies at that moment (the demo shows -380ms for 120ms of work). Guard by measuring all spans on the monotonic clock; wall is for stamps only.
- Cross-host monotonic compare: monoA=999 against monoB=5 looks decisive and means nothing — different zeroes, different rates. Guard by never comparing them; order across hosts needs logical clocks or uncertainty intervals.
- Single-host 'skew experiment': two containers on one host share the underlying clock (unless time namespaces split them), so the measured diff is ~0 and proves nothing about real skew. Guard with two independent hosts/VMs, or label the shift MOCK and report the RTT-based uncertainty.
- Wall-stamped LWW across writers: the ahead clock's earlier write wins over the later real write, silently. Guard by never ordering on wall stamps — use per-key causality, logical clocks, or an uncertainty wait.
کد قابل اجراRunnable code
اجراشده و تأییدشده در همین محیطExecuted and verified in this environment
این کد آموزشی و سادهسازیشده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچکس اینشکلی کد پروداکشن نمیزند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.
import java.util.*;
// Teaching demo with EXPLICITLY MOCKED clocks (labelled MOCK - not a real
// two-host measurement). Host B's wall clock runs 250ms ahead. Shows:
// wall breaks durations when NTP steps it; monotonic is safe for durations
// but meaningless across hosts; LWW lets the wall-ahead write bury a later one.
public class SkewDemo {
static class MockWall { long offset; long now(long truth) { return truth + offset; } }
static class Mono { long t; long now() { return t; } void advance(long d) { t += d; } }
public static void main(String[] a) {
MockWall wallA = new MockWall(), wallB = new MockWall();
wallB.offset = 250; // MOCK skew: B ahead 250ms
long truth = 1_000_000;
long sampleA = wallA.now(truth), sampleB = wallB.now(truth);
long rtt = 40; // MOCK round-trip of the sample
System.out.println("MOCK wallA=" + sampleA + " wallB=" + sampleB
+ " rawDiff=" + (sampleB - sampleA) + "ms rtt=" + rtt + "ms uncertainty=+-" + (rtt / 2) + "ms");
Mono mono = new Mono();
mono.advance(120); // real work takes 120ms
long wallStart = wallA.now(truth);
wallA.offset -= 500; // MOCK NTP step: wall jumps back
long wallDur = wallA.now(truth + 120) - wallStart; // wall-measured duration
System.out.println("MOCK NTP step -500ms: wall duration=" + wallDur + "ms (lies) mono duration="
+ mono.now() + "ms (true) -> timeouts/TTL must use monotonic");
Map<String, String> reg = new HashMap<>(); // LWW register
Map<String, Long> ts = new HashMap<>();
// B writes at its wall time (truth t, wall t+250); A writes LATER (truth t+100) at wall t+100.
reg.put("k", "from-B"); ts.put("k", wallB.now(truth));
long tsA = wallA.now(truth + 100); // wallA offset is now -500
if (tsA > ts.get("k")) { reg.put("k", "from-A"); ts.put("k", tsA); }
System.out.println("LWW keeps=" + reg.get("k") + " (B's earlier write buries A's later write)");
Mono mA = new Mono(), mB = new Mono();
mA.advance(999); mB.advance(5);
System.out.println("monoA=" + mA.now() + " monoB=" + mB.now()
+ " -> cross-host compare is meaningless (different zeroes)");
System.out.println("OK: real skew needs two independent hosts; mock is labelled mock");
}
}
اجرا: javac SkewDemo.java && java SkewDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: پنج خط: نمونهٔ MOCK (با rawDiff=250ms و uncertainty برابر ۲۰ms+-)، گام NTP (با duration دیواری ۳۸۰ms- دروغگو و mono درست ۱۲۰ms)، ماندن from-B در LWW، مقایسهٔ بیمعنی monoA=999 با monoB=5 و خط OK. (خروجی واقعی همین محیط)
Run: javac SkewDemo.java && java SkewDemo — stdlib Java only, instant.
Expected output: Five lines: the MOCK sample (rawDiff=250ms, uncertainty=+-20ms), the NTP step (wall duration=-380ms lying, mono 120ms true), LWW keeping from-B, the meaningless monoA=999 vs monoB=5 compare, and the OK line. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): NTP ساعت را step کند؛ timeout و TTL برنامه کدام clock را باید استفاده کنند؟Follow-up (Senior): NTP steps the wall clock. Which clock should application timeouts and TTL durations use?
برای هر دو monotonic یعنی CLOCK_MONOTONIC. timeout میسنجد «چقدر صبر کردهام» و TTL میسنجد «این چقدر دیگر معتبر است»: هر دو duration در یک ماشیناند و فقط ساعت monotonic از گام NTP بدون پرش جان سالم بهدر میبرد. زمان دیواری فقط وقتی وارد میشود که لحظهٔ مطلق را برای انسان یا رکوردهای میانسیستمی stamp میزنی («باطلشده در 14:03:11Z»)، هرگز برای سنجش یا انتظار. همان duration دیواری ۳۸۰ms- برای ۱۲۰ms کار واقعی در دمو کل استدلال است.
Monotonic — CLOCK_MONOTONIC — for both. A timeout measures 'how long have I waited', and a TTL measures 'how much longer is this valid': both are durations on one host, and only the monotonic clock survives an NTP step without jumping. Wall time enters only when you stamp an absolute moment for humans or for cross-system records ('revoked at 14:03:11Z'), never for measuring or waiting. The demo's -380ms wall duration for 120ms of real work is the whole argument.
پیگیری (استف): میخواهی عدد skew را به تیم گزارش بدهی. گزارش چه چیزی باید داشته باشد تا صادقانه باشد؟Follow-up (Staff): You report a skew number to the team. What must the report contain so it is honest?
سه جزء برچسبدار: اختلاف خام (در دمو ۲۵۰ms)، عدمقطعیت نمونهبرداری (۲۰ms+- از RTT برابر ۴۰ms — پاسخ ممکن است در هر دو مسیر تأخیر خورده باشد) و برچسب روش: REAL روی دو host/VM مستقل یا MOCK با جابهجایی تزریقشدهٔ اعلامشده. گزارش بده «250ms با 20ms+- (MOCK با ۲۵۰+ تزریقشده روی B و RTT برابر ۴۰ms)» — هرگز «۲۵۰ms» لخت. عدد لخت پنهان میکند دنیا را اندازه گرفتی یا نمایش را صحنه کردی؛ همان برچسب است که باعث میشود مهندس بعدی به آزمایش اعتماد یا آن را تکرار کند.
Three labelled parts: the raw diff (250ms in the demo), the sampling uncertainty (+-20ms from the 40ms RTT — the reply could have been delayed on either leg), and the method tag: REAL on two independent hosts/VMs, or MOCK with the injected shift stated. Report '250ms +-20ms (MOCK, +250 injected on B, RTT 40ms)' — never a bare '250ms'. A bare number hides whether you measured the world or staged the play; the tag is what makes the next engineer trust or redo the experiment.
اشتباهات رایجCommon mistakes
- اندازهگیری «skew» با دو کانتینر روی یک host: ساعت زیرین مشترک و اختلاف ~۰ و صفر اطلاعات دربارهٔ واگرایی واقعی. استقلال یعنی ساعتهای کرنل جدا، نه namespace جدا.
- مقایسهٔ خوانشهای monotonic میان ماشینها (monoA با monoB): هرکدام از بوت خودش شروع و با نرخ خودش میزند — مقایسه عددبازی است. monotonic اکیداً تکماشینی است.
- مرتب کردن نوشتهها با timestamp دیواری (LWW روی دیواری): سریعترین دروغگو میبرد — نوشتهٔ کهنهٔ ساعت جلوتر بدون خطا بر نوشتهٔ دیرتر صادق میچربد. stampهای دیواری ثبت میکنند، داوری نمیکنند.
- Measuring 'skew' with two containers on one host: same underlying clock, ~0 diff, zero information about real divergence. Independence means separate kernels Clocks, not separate namespaces.
- Comparing monotonic readings across hosts (monoA vs monoB): each starts at its own boot and ticks its own rate — the comparison is numerology. Monotonic is strictly single-host.
- Ordering writes by wall timestamps (LWW on wall): the fastest liar wins — an ahead clock's stale write beats a later honest one with no error. Wall stamps record, they do not arbitrate.
منابع:Sources: Linux · time namespaces and realtime · Atlassian · engineering interview handbook
33 تمرین ۳ — fencing token عملیExercise 3 — fencing tokens in practice
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- epochهای ۷ و بعد ۸ با یک نگهدارندهٔ pauseشدهٔ A و یک جانشین B (سناریوی آموزشی از brief).
- lock serviceepochهای بادوام و صعودی صادر میکند؛ storage اتمیک مقایسه و نوشتن میکند (در دمو یک گام synchronized).
- op_id برای هر عملیات منطقی یکتاست و در تمام retryهای همان عملیات ثابت میماند؛ dedup تا وقتی بازتحویل معتبر ممکن است حفظ میشود.
- Epochs 7 then 8, one paused holder A and one successor B (teaching scenario from the brief).
- The lock service issues durable, monotonic epochs; storage compares and writes atomically (single synchronized step in the demo).
- An op_id uniquely identifies a logical operation and remains unchanged across its retries; retain dedup while valid redelivery remains possible.
چرا این روش و این عددها؟Why this method and these numbers?
عدد epoch فقط ترتیب مالکیت است؛ ۷ و ۸ را میتوان با هر دو عدد افزایشی جایگزین کرد. شرط token<highest رد میکند تا مالک تازه بتواند چند write همepoch انجام دهد؛ شرط سخت token>highest برای هر write، عملیات دوم همان مالک را اشتباه رد میکند. op_id برای هویت همان عملیات و retry آن است و باید در retry ثابت بماند. ذخیرهٔ highest و dedup کنار داده ضروری است چون restart حافظهٔ فرایند را پاک میکند.
Epoch numbers order ownership; any increasing pair can replace 7 and 8. Reject token<highest so the current owner can perform multiple same-epoch writes; requiring token>highest for every write wrongly rejects its second operation. Keep op_id stable across retries of one operation. Persist highest and dedup with data because restart erases process memory.
مدل ذهنیMental model
حل گامبهگامStep by step
- انقضا خوابآلود را متوقف نمیکند ببین چرا انقضا امنیت نیست: lease هفت تمام میشود و Bepoch هشت را میگیرد ولی Aی pauseشده هرگز نمیفهمد — نوشتهٔ epoch-7 او بعداً با اعتماد کامل میرسد. هر نگهبانی که در ذهن A زندگی کند (تایمرها و ساعتهای lease) نگهبانی خوابآلود است.
- رد epoch کهنه در storage رأی را به storage منتقل کن: هر نوشته (توکن و شناسهٔ op) را دارد؛ storage اتمیک توکن کمتر از بزرگترین دیدهشده را رد میکند و بالاترین تازه را ثبت میکند و فقط بعد اعمال میکند. نویسندهٔ pauseشده کنار داده خلعسلاح میشود نه کنار ساعت خودش.
- epoch برای نوبت و op برای تلاش دو هویت را جدا کن: epoch میگوید نوبت کیست (یک epoch زنده برای هر منبع) و شناسهٔ عملیات میگوید این کدام تلاش است. همepoch با op تازه دومین نوشتهٔ مجاز است؛ همepoch با op دیدهشده تکرار برای جذب است.
- دوام همان نرده است رأی را persist کن: بزرگترین دیدهشده و مجموعهٔ op اعمالشده روی ذخیرهٔ بادوام با داده زندگی میکنند و قبل از تأیید نوشته میشوند. restartای که فراموششان کند epochهای کهنه را میپذیرد — همان خط HAZARD در دمو — پس فقط حافظه باگ fencing است.
- Expiry cannot stop a sleeper See why expiry is not safety: A's lease ends, B is granted epoch 8, but paused A never learns this — its epoch-7 write arrives later in full confidence. Any guard that lives in A's head (timers, lease clocks) cannot guard a sleeper.
- Reject stale epochs in storage Move the verdict into storage: every write carries (token, opId); storage atomically rejects token below highest-seen, records the new highest, and only then applies. The paused writer is disarmed at the data, not at its own clock.
- Epoch for turn, op for attempt Split the two identities: the epoch says whose turn it is (one live epoch per resource), the operation ID says which attempt this is. Same epoch plus new op is a second legitimate write; same epoch plus seen op is a retry to absorb.
- Durability is the fence Persist the verdict: highest-seen and the applied-op set live on durable storage with the data, written before the ack. A restart that forgets them accepts stale epochs — the demo's HAZARD line — so memory alone is a fencing bug.
طراحی مشخصConcrete design
lock service: تابع issue() برای هر اعطای lease یک epoch بادوام و اکیداً صعودی برمیگرداند (یکی برای هر epoch نه هر نوشته) و شمارنده را قبل از جواب persist میکند. مسیر نوشتن storage در یک گام اتمیک: اگر token از highest کمتر بود: رد کن؛ highest را برابر token کن؛ اگر opId دیده شده: dedup؛ وگرنه اعمال کن. بزرگترین epoch دیدهشده و مجموعهٔ opهای اعمالشده هر دو روی ذخیرهٔ بادوام و تکثیرشده با داده زندگی میکنند — فقط حافظه با restart شکاف مغزی را دوباره باز میکند. قاعدهٔ تخصیص token: دقیقاً یک epoch زنده برای هر منبع؛ اعطای تازه همیشه از همهٔ قبلیها بزرگتر است، حتی بعد از failover خود lock service.
Lock service: issue() returns a durable, strictly monotonic epoch per lease grant (one per epoch, not per write) and persists the counter before answering. Storage write path, one atomic step: if token < highest: reject; highest = token; if opId seen: dedup; else apply. The highest-seen epoch and the applied-op set both live on durable storage, replicated with the data — memory alone reopens the split brain on restart. Token allocation rule: exactly one live epoch per resource; a new grant always exceeds every earlier one, even across lock-service failover.
دانلود نمودار مستقلDownload standalone diagram
- باند lease هفت و pause: Aepoch هفت را از lock service میگیرد و بعد میایستد — pause در GC و حالت آموزشی.
- باند lease هشت و نوشتن: B بعد از انقضا epoch هشت را میگیرد و leader=B را با op-b1 مینویسد و storage مقدار highest=8 را ثبت میکند.
- باند بیداری کهنه و مرگ: نوشتهٔ epoch-7 برای A دیر میرسد و storage اتمیک ردش میکند — ۷ زیر ۸ است.
- باند نوشتههای همepoch: op-b2 برای B در epoch هشت پذیرفته میشود (op تازه و مجاز) و تکرارش بدون اثر دوم dedup میشود.
- Lease 7 pauses: A takes epoch 7 from the lock service, then stops — GC pause, teaching case.
- Lease 8 writes: B takes epoch 8 after expiry, writes leader=B with op-b1, and storage records highest=8.
- Stale wakes, dies: A's epoch-7 write arrives late and storage rejects it atomically — 7 below 8.
- Same-epoch writes: B's op-b2 at epoch 8 is accepted (new op, legitimate), and its retry is deduped with no second effect.
نقطهٔ تصمیم: نقطهٔ تصمیم بررسی اتمیک داخل storage است: توکن در برابر بزرگترین دیدهشده، کنار داده و در یک گام. بررسی هر جای دیگر (سمت کلاینت یا ساعت lease) با نویسندهٔ pauseشده مسابقه میدهد و میبازد.
رد خرابی: نوشتهٔ کهنهٔ چیندار را دنبال کن: A توکن ۷ را میچسباند و storage جواب REJECT میدهد — نویسندهٔ pauseشده بدون اینکه کسی خبرش کند خلعسلاح میشود. حالا همان پیکان را بدون بررسی تصور کن (خط no-fence در کد): storage مقدار leader=A را روی leader=B اعمال میکند و کلاستر دو حقیقت دارد. بررسی تنها فرق دو history است.
تضمین نشاندادهشده: شکل رد کهنه و پذیرش همepoch مجاز و جذب تکراری را نشان میدهد. نشان نمیدهد وقتی خود highest-seen گم شود چه میشود — آن حالت amnesia در اسمبل است.
تضمین نشانندادهشده: فرض میکند lock serviceepochهای صعودی صادق صادر میکند؛ صادرکنندهٔ دوشاخه (دو lock service با شکاف مغزی در بالا) بیرون این شکل است.
Decision point: The decision point is the atomic check inside storage: token against highest-seen, beside the data, in one step. A check anywhere else (client-side, lease-clock-side) races the paused writer and loses.
Failure trace: Trace the dashed stale write: A attaches token 7 and storage answers REJECT — the paused writer is disarmed without anyone telling it. Now picture the same arrow without the check (the no-fence line in the code): storage applies leader=A over leader=B, and the cluster has two truths. The check is the only difference between the two histories.
Guarantee shown: The figure shows the stale reject, the legitimate same-epoch accept, and the duplicate absorb. It does not show what happens when the highest-seen itself is lost — that is the assembly's amnesia state.
Not shown: It assumes the lock service issues honest monotonic epochs; a forked issuer (two lock services, split brain upstairs) is outside this figure.
خودآزماییSelf-check
چرا op-b2 برای B پذیرفته میشود ولی تکرارش نه؟ بین دو رسیدن چه فرقی است؟
پاسخ: چون epoch یکسان است (۸) ولی شناسهٔ عملیات فرق دارد: op-b2 قصد تازهای است که سرور هرگز ندیده، پس اعمال میشود؛ تکرار شناسهٔ دیدهشده دارد، پس جذب میشود. epoch میگوید نوبت کیست و شناسهٔ op میگوید کدام تلاش.
B's op-b2 is accepted but its retry is not. What differs between the two arrivals?
Answer: Same epoch (8) but different operation IDs: op-b2 is a new intent the server has never seen, so it applies; the retry carries a seen ID, so it absorbs. Epoch decides whose turn, op ID decides which attempt.
دانلود نمودار مستقلDownload standalone diagram
- ریل اصلی: No Leaseepoch برابر N اعطا میکند (Held) و نگهدارنده با N مینویسد (Writing) و storage یک اثر را تأیید میکند (Confirmed).
- میانبر pause: نگهدارنده pause میشود (نگهدارندهٔ pauseشده یعنی زامبی) و کهنه بیدار میشود و مینویسد — یا انقضا اول N+1 را به جانشین اعطا میکند.
- دو خروجی از Writing: توکن کمتر از highest در Stale Rejected مینشیند؛ تکرار همepoch و همop در Duplicate Absorbed.
- تله: restart مقدار highest-seen را پاک میکند و در Highest Forgotten مینشیند، جایی که نوشتهٔ کهنه میبرد — دوام همان قرارداد است.
- Main rail: No Lease grants epoch N (Held), the holder writes with N (Writing), and storage confirms one effect (Confirmed).
- Pause detour: the holder pauses (Paused Holder, the zombie) and wakes writing stale — or expiry re-grants N+1 to the successor first.
- Two exits from Writing: a below-highest token lands in Stale Rejected; a same-epoch same-op retry lands in Duplicate Absorbed.
- The trap: restart wipes the highest-seen and lands in Highest Forgotten, where the stale write wins — durability is the contract.
نقطهٔ تصمیم: نقطههای تصمیم دو خروجی Writingاند: کمتر از highest به Stale Rejected میرود و همepoch و همop به Duplicate Absorbed. رد در همان ستون نوشته است — خروجیها در ستون حالتی مینشینند که ترک میکنند.
رد خرابی: پیکان Confirmed به Amnesia را دنبال کن: یک restart مقدار highest-seen بادوام را پاک میکند و حالت پایانی Amnesia میگوید «کهنه میبرد: شکاف مغزی» — نوشتهٔ epoch-7 که توالی ردش کرده بود حالا اعمال میشود. خرابی نه خطای شبکه میخواهد نه باگ دوم: فراموشی کافی است.
تضمین نشاندادهشده: شکل هر سرنوشت lease را بهعلاوهٔ خطر دوام در یک ماشین نشان میدهد. صادرکنندهٔ دوشاخهٔ توکن را نشان نمیدهد — در کل شکل یک lock service صادقصعودی فرض شده.
تضمین نشانندادهشده: دربارهٔ اینکه خود lock service چطور زنده میماند (termهای consensus) یا حالت dedup چقدر باید زنده بماند چیزی نمیگوید.
Decision point: The decision points are the two exits from Writing: below-highest goes to Stale Rejected, same-epoch-same-op goes to Duplicate Absorbed. Same column as the write for the reject — exits sit in the column of the state they leave.
Failure trace: Trace the Confirmed-to-Amnesia arrow: a restart wipes the durable highest-seen, and the terminal Amnesia state reads 'stale wins: split brain' — the epoch-7 write the sequence rejected now applies. The failure needs no network fault and no second bug: forgetting is enough.
Guarantee shown: The figure shows every lease outcome plus the durability hazard in one machine. It does not show a forked token issuer — one lock service, honestly monotonic, is assumed throughout.
Not shown: It does not cover how the lock service itself survives (consensus terms) or how long dedup state must live.
خودآزماییSelf-check
بزرگترین epoch دیدهشده کجا باید زندگی کند و کدام تکخطا میشکندش؟
پاسخ: بزرگترین دیدهشده باید روی ذخیرهٔ بادوام و تکثیرشده با داده زندگی کند — فقط حافظه با restart فراموش میکند و فراموشی بدون هیچ خطای دیگر شکاف مغزی را دوباره باز میکند.
Where must the highest-seen epoch live, and what single fault breaks it?
Answer: Highest-seen must live on durable storage, replicated with the data — memory alone forgets on restart, and forgetting reopens the split brain with no other fault needed.
گزینههای جایگزینAlternatives
- انقضای lease بهتنهایی («lease هفت تمام شد پس A نمیتواند بنویسد»): فقط وقتی کار میکند که هر شرکتکننده به ساعت lease احترام بگذارد — فرایند pauseشده اصلاً پیام را نگرفته. ارزانتر است (لولهکشی token ندارد) ولی به همان نودی اعتماد میکند که بنابر فرض unreachable است. فقط جایی که pauseها کراندار و اندازهگیریشدهاند، هرگز برای pauseهای GC یا VM.
- fencing تکنویسنده با consensus (term در Raft/ZAB بهعنوان epoch): همان شکل با صادرکنندهٔ قویتر — epoch از term لیدر منتخب میآید نه شمارندهٔ lock service. درست وقتی خود lock service باید خرابی را تحمل کند؛ سنگینتر از شمارنده و همچنان با بررسی storage اجرا میشود.
- Lease expiry alone ('A's lease ended, so A cannot write'): works only if every participant obeys the lease clock — a paused process never got the memo. Cheaper (no token plumbing), but it trusts the very node that is, by hypothesis, unreachable. Use only where pauses are bounded and measured, never across GC or VM pauses.
- Single-writer fencing via consensus (Raft/ZAB term as the epoch): same shape, stronger issuer — the epoch comes from an elected leader term instead of a lock service counter. Right when the lock service itself must survive failures; heavier than a counter, and still enforced at the storage check.
سناریوهای خرابیFailure scenarios
- fencing فقط با انقضای lease (خط no-fence): نوشتهٔ کهنهٔ A مقدار leader=B را با leader=A بازنویسی میکند — بدون خطا و با دو حقیقت. نگهبان: بررسی توکن سمت storage؛ انقضا مالکیت را جابهجا میکند و فقط بررسی اجرایش میکند.
- فراموشی restart: مقدار highest-seen داخل حافظه با crash فراموش میشود و نوشتهٔ کهنهٔ بعدی اعمال میشود — همان خط HAZARD در دمو. نگهبان: persist کردن highest-seen (و مجموعهٔ op) با هر پذیرش، قبل از تأیید.
- تکراری همepoch بدون dedup در op: op-b2 تکرارشدهٔ B دو بار اعمال میشد (اینجا نوشتن دوبارهٔ x=1 بیضرر است و جای دیگر شارژ دوباره). نگهبان: مجموعهٔ op اعمالشده بهعنوان دغدغهای جدا از مقایسهٔ epoch.
- صادرکنندهٔ دوشاخهٔ توکن: دو lock service (یا failoverای که شمارنده را صفر میکند) یک epoch را دو بار میدهند و fencing مساوی را با مساوی مقایسه میکند. نگهبان: یک صادرکنندهٔ واحد برای هر منبع — term در consensus یا شمارندهٔ فنسدار — تا epochها هرگز دوشاخه نشوند.
- Lease-expiry-only fencing (the no-fence line): A's stale write overwrites leader=B with leader=A — no error, two truths. Guard with the storage-side token check; expiry moves ownership, only the check enforces it.
- Restart amnesia: highest-seen kept in memory is forgotten on crash, and the next stale write applies — the HAZARD line in the demo. Guard by persisting highest-seen (and the op set) on every accept, before acknowledging.
- Same-epoch duplicate without op dedup: B's retried op-b2 would apply twice (x=1 written twice — harmless here, a double charge elsewhere). Guard with the applied-op set as a separate concern from the epoch compare.
- Forked token issuer: two lock services (or a failover that resets the counter) hand out the same epoch twice, and fencing compares equal against equal. Guard with a single issuer per resource — consensus term or fenced counter — so epochs never fork.
کد قابل اجراRunnable code
اجراشده و تأییدشده در همین محیطExecuted and verified in this environment
این کد آموزشی و سادهسازیشده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچکس اینشکلی کد پروداکشن نمیزند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.
import java.util.*;
// Teaching demo: fencing tokens are ownership EPOCHS, not per-write IDs.
// Storage atomically rejects stale epochs; legitimate same-epoch writes are
// told apart by operation ID (dedup), not by the epoch. Forgetting the
// highest token across a restart reopens the split brain.
public class FencingDemo {
static class LockService {
int epoch = 6; // teaching: A gets 7, B gets 8
int issue() { return ++epoch; } // durable, monotonic
}
static class Storage {
int highest = 0; // durable highest-seen epoch
Map<String, String> data = new HashMap<>();
Set<String> appliedOps = new HashSet<>(); // operation dedup, separate concern
synchronized String write(int token, String opId, String k, String v) {
if (token < highest) return "REJECT stale-epoch token=" + token + " highest=" + highest;
highest = token;
if (!appliedOps.add(opId)) return "DEDUP op=" + opId + " (same-epoch repeat, no second effect)";
data.put(k, v);
return "ACCEPT token=" + token + " op=" + opId + " " + k + "=" + v;
}
}
public static void main(String[] a) {
LockService lock = new LockService();
Storage store = new Storage();
int tokenA = lock.issue(); // A holds epoch 7
System.out.println("A lease epoch=" + tokenA + " then A pauses (GC pause, teaching)");
int tokenB = lock.issue(); // B holds epoch 8
System.out.println("B lease epoch=" + tokenB);
System.out.println("B write -> " + store.write(tokenB, "op-b1", "leader", "B"));
System.out.println("A wakes, stale write -> " + store.write(tokenA, "op-a1", "leader", "A"));
System.out.println("B second write same epoch -> " + store.write(tokenB, "op-b2", "x", "1"));
System.out.println("B retry of op-b2 -> " + store.write(tokenB, "op-b2", "x", "1"));
System.out.println("final leader=" + store.data.get("leader") + " x=" + store.data.get("x"));
Storage noFence = new Storage() {
@Override synchronized String write(int token, String opId, String k, String v) {
data.put(k, v); // lease-expiry only: NO token check
return "APPLIED without check " + k + "=" + v;
}
};
noFence.write(2, "op-b1", "leader", "B");
System.out.println("no-fence A overwrite -> " + noFence.write(1, "op-a1", "leader", "A"));
Storage restarted = new Storage(); // amnesia: highest forgotten
restarted.write(tokenB, "op-b1", "leader", "B");
restarted.highest = 0; // crash lost durable state
System.out.println("restart-amnesia A write -> " + restarted.write(tokenA, "op-a9", "leader", "A")
+ " HAZARD: stale epoch accepted");
System.out.println("OK: epoch rejects, op-id dedups, durability required");
}
}
اجرا: javac FencingDemo.java && java FencingDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: نه خط: نوشتهٔ epoch-8 برای B با ACCEPT، نوشتهٔ epoch-7 برای A با REJECT کهنه، دومین نوشتهٔ همepoch برای B با ACCEPT، تکرارش با DEDUP و leader نهایی=B؛ بعد بازنویسی بدون-fence (با leader=A) و HAZARD فراموشی restart (پذیرش ۷ کهنه)؛ خط OK. (خروجی واقعی همین محیط)
Run: javac FencingDemo.java && java FencingDemo — stdlib Java only, instant.
Expected output: Nine lines: B's epoch-8 write ACCEPTs, A's epoch-7 write REJECTs as stale, B's second same-epoch write ACCEPTs, its retry DEDUPs, final leader=B; then the no-fence overwrite (leader=A) and the restart-amnesia HAZARD (stale 7 accepted); OK line. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (استف): storage restart شود و highest token را فراموش کند، چه خطایی ممکن است؟Follow-up (Staff): Storage restarts and forgets the highest fencing token. What failure becomes possible?
نوشتهٔ epochکهنه دوباره میتواند اعمال شود — شکاف مغزی بدون هیچ خطای دیگر دوباره باز میشود. مشخصاً: storage با highest=0 ریاستارت میشود ولی مقدار epoch-8 برای B داخل داده نشسته؛ Aی pauseشده با epoch هفت بیدار میشود و ۷ دیگر زیر هیچ highest بهیادماندهای نیست، پس روی B بازنویسی میکند. خط HAZARD در دمو دقیقاً همین است: ۷ کهنه پذیرفته میشود و leader به A برمیگردد. نگهبان دوام است: highest-seen (و مجموعهٔ op) را روی دیسک و تکثیرشده با داده persist کن، قبل از تأیید هر نوشته — فقط حافظه باگ fencing است.
A stale-epoch write can apply again — the split brain reopens with no other fault. Concretely: storage restarts with highest=0 while B's epoch-8 value sits in the data; paused A wakes with epoch 7, and 7 is no longer below any remembered highest, so it overwrites B. The demo's HAZARD line is exactly this: stale 7 accepted, leader flips to A. The guard is durability: persist highest-seen (and the op set) on disk, replicated with the data, before acknowledging any write — memory alone is a fencing bug.
پیگیری (سینیور): دو نوشتهٔ مجاز epoch هشت را شریکاند و بعد یکی retry میشود. چطور دو اثر را قبول میکنی ولی سهتا نه؟Follow-up (Senior): Two legitimate writes share epoch 8, then one is retried. How do you accept two but not three effects?
با شناسهٔ عملیات، هویت جدا. هر دو epoch هشت را دارند، پس بررسی epoch هر دو را قبول میکند — درست، چون هر دو زیر epoch زنده میرسند. بعد مجموعهٔ op تصمیم میگیرد: op-b2 تازه است، پس اعمال میشود (دومین نوشتهٔ مجاز epoch)؛ تکرار دوباره op-b2 را دارد و مجموعه از قبل داردش، پس بدون اثر دوم جذب میشود. epoch جواب میدهد «نوبت کیست» و شناسهٔ op جواب میدهد «کدام تلاش» — قاطی کردنشان یا نوشتههای مجاز را رد میکند یا retryها را دو بار اعمال میکند.
By operation ID, the separate identity. Both carry epoch 8, so the epoch check passes both — correctly, because both arrive under the live epoch. Then the op set decides: op-b2 is new, so it applies (second legitimate write of the epoch); the retry carries op-b2 again, the set already holds it, so it absorbs with no second effect. Epoch answers 'whose turn', op ID answers 'which attempt' — conflating them either rejects legitimate writes or double-applies retries.
اشتباهات رایجCommon mistakes
- اعتماد به انقضای lease برای متوقف کردن نویسندهٔ pauseشده: انقضا خوانش ساعت است و فرایند pauseشده نمیتواند ساعت بخواند. نوشتهٔ کهنه بعد از انقضا با اعتماد کامل میرسد — فقط بررسی storage جلویش را میگیرد.
- ساختن یک توکن برای هر نوشته بهجای هر epoch: آنوقت «کهنه» معنایی ندارد — هر توکن تازه است و مقایسه همهچیز را قبول میکند و نویسندهٔ pauseشده رد میشود. توکن باید نام نسل مالکیت باشد که همهٔ نوشتههایش شریکاند.
- نگه داشتن highest-seen فقط در حافظه: تا اولین restart کار میکند و بعد هر epoch قدیمی دوباره جوان است. دوام اینجا بهینهسازی نیست — خود نرده است.
- Trusting lease expiry to stop a paused writer: expiry is a clock reading, and the paused process cannot read clocks. The stale write arrives after expiry with full confidence — only the storage check stops it.
- Minting one token per write instead of per epoch: then 'stale' has no meaning — every token is fresh, the compare passes everything, and the paused writer sails through. The token must name the ownership generation, shared by all its writes.
- Keeping highest-seen in memory only: it works until the first restart, then every old epoch is young again. Durability is not an optimization here — it is the fence.
منابع:Sources: Kleppmann · distributed locking and fencing · Atlassian · engineering interview handbook
44 تمرین ۴ — safety/livenessExercise 4 — safety/liveness
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- یک مصرفکنندهٔ صف و شناسههای رشتهای پیام و امکان redelivery بعد از هر crash یا پارتیشن (transport از نوع at-least-once).
- اثرها در دمو شمارندهٔ هر پیاماند (جانشین آموزشی اثرهای واقعی اپلیکیشن).
- deadlineهای liveness کرانهای آموزشیاند («بالاخره» همیشه با فرض اعلامشدهٔ زمان و شبکه و ظرفیت میآید).
- One queue consumer, string message IDs, redelivery possible after any crash or partition (at-least-once transport).
- Effects are per-message counters in the demo (teaching stand-ins for real application effects).
- Liveness deadlines are teaching bounds ('eventually' always ships with a stated time, network and capacity assumption).
چرا این روش و این عددها؟Why this method and these numbers?
safety یعنی هیچ اجرای مجاز به حالت ممنوع نرسد؛ یک counterexample آن را رد میکند. liveness یعنی با فرض پیشرفت شبکه، پردازنده و ظرفیت، کار بالاخره تمام شود؛ timeout یک آزمایش محدود نمیتواند آن را برای همهٔ اجراها ثابت کند. deadline عملیاتی هدف محصول است و با eventually در مدل نظری یکسان نیست. at-most-once میتواند duplicate را منع کند ولی با دورریختن پیام، پیشرفت کسبوکاری را از بین ببرد.
Safety excludes forbidden states in every permitted execution; one counterexample refutes it. Liveness requires eventual completion under network, processor and capacity progress assumptions; a finite timeout test cannot prove it universally. An operational deadline differs from theoretical eventually. At-most-once can prevent duplicates while discarded messages destroy business progress.
مدل ذهنیMental model
حل گامبهگامStep by step
- اثرها نه تحویلها اثرها را بشمار نه تحویلها را: سیم ممکن است تکراری و گم و جابهجا کند — اثر اپلیکیشن برای هر شناسهٔ پیام تنها چیزی است که کسبوکار حس میکند. «یکبار اعمال برای هر شناسه» همان جملهٔ safety است؛ بقیه transport است.
- dedup که زنده میماند safety را با dedup زندهمانده اجرا کن: بررسی-بعد-اعمال در یک گام اتمیک و persist قبل از تأیید، تا restartای که redelivery را میسازد نتواند خاطرهٔ اثر اول را پاک کند. dedup همان نرده است و مجموعه میخهایش.
- liveness با فرضها liveness را با سه فرض نامدار کران کن: پارتیشن ظرف H خوب میشود و ظرفیت مصرفکننده بالای رسیدنها میماند و هر پیام معتبر ظرف T اثر میکند. آنوقت پارتیشن دائمی دقیقاً همان فرضی را میشکند که نام میبرد — liveness شکسته و safety دستنخورده.
- چهار خانه و مستقل استقلال را با ماتریس چهارخانه اثبات کن: تحویل دوباره هر دو را نگه میدارد و پارتیشن فقط liveness را میشکند و گمشدن dedup فقط safety را و reject-all با liveness مرده safety را بهصورت vacuous نگه میدارد. هر خاصیتی که نتواند تنها بشکند دو خاصیت است با یک نام.
- Effects, not deliveries Count effects, not deliveries: the wire may duplicate, lose, or reorder — the application effect per message ID is the only thing the business feels. 'Applied once per ID' is the safety sentence; everything else is transport.
- Dedup that survives Enforce safety with surviving dedup: check-then-apply under one atomic step, persisted before the ack, so the restart that triggers redelivery cannot wipe the memory of the first effect. Dedup is the fence; the set is its posts.
- Liveness with assumptions Bound liveness with three named assumptions: the partition heals within H, consumer capacity stays above arrivals, and every valid message effects within T. A permanent partition then breaks exactly the assumption it names — liveness violated, safety untouched.
- Four cells, independent Prove independence with the four-cell matrix: redelivery holds both, partition breaks liveness only, dedup-loss breaks safety only, reject-all holds safety vacuously with liveness dead. Any property that cannot break alone is two properties wearing one name.
طراحی مشخصConcrete design
حالت مصرفکننده با دو ساختار: dedup: Set[messageID] (دیدهشده یعنی اثرکرده) و effects: Map[messageID, count] که در یک گام اتمیک بهروز میشوند — بررسی-بعد-اعمال زیر همان قفل و persist قبل از تأیید. قرارداد تحویل: transport از نوع at-least-once با redelivery بیکران؛ مصرفکننده همه را جذب میکند. قرارداد liveness که برای هر صف اعلام میشود: پیامهای معتبر ظرف T اثر میکنند بهشرط اینکه پارتیشن ظرف H خوب شود و ظرفیت مصرفکننده بالای نرخ رسیدن بماند — کرانی با سه فرض نامدار، نه یک «بالاخره» لخت.
Consumer state, two structures: dedup: Set[messageID] (seen means effected) and effects: Map[messageID, count], updated in one atomic step — check-then-apply under the same lock, persisted before the ack. Delivery contract: at-least-once transport with unbounded redelivery; the consumer absorbs all of it. Liveness contract, stated per queue: valid messages take effect within T provided the partition heals within H and consumer capacity stays above arrival rate — a bound with three named assumptions, not a bare 'eventually'.
مثال محاسبهشدهWorked example
ماتریس آموزشی از اجرای دمو: تحویل دوبارهٔ m1 با dedup میدهد effects={m1=1, m2=1} — SAFETY میماند و LIVENESS میماند. پارتیشن دائمی (m3 هرگز نمیرسد) — SAFETY روی هیچچیز-دوبار-اعمالنشده میماند و LIVENESS روی هرگز-اثرنکرده میشکند. پاک شدن dedup در restart میدهد effects={m1=2} — SAFETY میشکند و LIVENESS میماند. reject-all میدهد effects={} — SAFETY بهصورت vacuous میماند و LIVENESS میشکند. چهار خانه و چهار رأی و صفر سرنوشت مشترک.
Teaching matrix from the demo run: redelivery of m1 with dedup gives effects={m1=1, m2=1} — SAFETY HOLDS, LIVENESS HOLDS. A permanent partition (m3 never arrives) — SAFETY HOLDS on nothing-applied-twice, LIVENESS VIOLATED on never-effected. Dedup wiped across restart gives effects={m1=2} — SAFETY VIOLATED, LIVENESS HOLDS. Reject-all gives effects={} — SAFETY HOLDS vacuously, LIVENESS VIOLATED. Four cells, four verdicts, zero shared fate.
دانلود نمودار مستقلDownload standalone diagram
- ریل اصلی: Sent وارد Transit میشود؛ اولین تحویل با ثبت dedup به Applied Once میرسد — مسیر خوش.
- میانبر retry: یک timeout در Backing Off پارک میکند (نتیجهٔ نامشخص) و retry شلیک میکند و پیام بهصورت Redelivered برمیگردد.
- میانبر پارتیشن: لینک به Partitioned میمیرد؛ خوب شدن به Redelivered برمیگردد و هرگز خوب نشدن به Starved میلغزد — liveness شکسته و safety دستنخورده.
- سه تسویه از Redelivered: op تازه یکبار اعمال میشود و op دیدهشده جذب میشود و dedup گمشده دوتایی میکند — بهعلاوهٔ Dropped برای سرویس reject-all.
- Main rail: Sent enters Transit; first delivery reaches Applied Once with the dedup recorded — the happy path.
- Retry detour: a timeout parks in Backing Off (unknown outcome), the retry fires, and the message returns as Redelivered.
- Partition detour: the link dies into Partitioned; healing returns to Redelivered, never healing slides to Starved — liveness broken, safety untouched.
- Three settlements from Redelivered: op-new applies once, op-seen absorbs, dedup-lost doubles — plus Dropped for the reject-all service.
نقطهٔ تصمیم: نقطههای تصمیم خروجیهای Redeliveredاند: op تازه به Applied Once میرود و op دیدهشده به Absorbed و dedup گمشده به Double Effect. رسیدن یکسان و سه تسویه — مجموعهٔ dedup کل فرق است.
رد خرابی: پیکانهای برچسب امنیتی را دنبال کن: از Redelivered به Double Effect (dedup در restart گم شده)، از Partitioned به Starved (برش هرگز خوب نمیشود) و از Sent به Dropped (هر پیام رد میشود). سه پیکان و سه راه اشتباه تسویه شدن — و فقط اولی به safety دست میزند.
تضمین نشاندادهشده: شکل هر سرنوشت تحویل را نشان میدهد و اینکه هر تسویهٔ اشتباه کدام خاصیت را میشکند. کرانهای زمانی را تعیین نمیکند — آنها در قرارداد liveness و برای هر صف زندگی میکنند.
تضمین نشانندادهشده: نمیگوید سیستم تو هفتهٔ پیش کدام تسویهٔ اشتباه را دیده — چهار probe (اسمبل) را اجرا کن تا بفهمی.
Decision point: The decision points are the exits from Redelivered: op-new goes to Applied Once, op-seen to Absorbed, dedup-lost to Double Effect. Same arrival, three settlements — the dedup set is the whole difference.
Failure trace: Trace the security-labelled arrows: Redelivered to Double Effect (dedup lost across the restart), Partitioned to Starved (the cut never heals), Sent to Dropped (every message refused). Three arrows, three ways to settle wrong — and only the first touches safety.
Guarantee shown: The figure shows every delivery outcome and which property each wrong settlement breaks. It does not set the time bounds — those live in the liveness contract, per queue.
Not shown: It does not say which wrong settlement your system saw last week — run the four probes (the assembly) to find out.
خودآزماییSelf-check
پیامی در Starved نشسته. کدام خاصیت میماند و کدام شکسته و کدام فرض گم است؟
پاسخ: Starved: چون پیام معتبر است و هیچچیز دو بار اعمال نشده (safety میماند) ولی چون پارتیشن هرگز خوب نمیشود اثر نمیکند (liveness شکسته). قطعهٔ گمشده فرض خوب شدن است.
A message sits in Starved. Which property holds, which is violated, and what assumption is missing?
Answer: Starved: the message is valid and nothing applied twice (safety holds), but it never takes effect because the partition never heals (liveness violated). The missing piece is the healing assumption.
دانلود نمودار مستقلDownload standalone diagram
- سؤال خوب شدن پارتیشن؟ نه به Liveness Down میرسد: پیام معتبر هرگز اثر نمیکند و هیچچیز دو بار اعمال نمیشود — فرضهای لازم: خوب شدن بهعلاوهٔ ظرفیت.
- سؤال طوفان تحویل دوباره؟ جذبشده به Both Hold میرسد: همان شناسه بارها هنوز یکبار اثر میکند — کرانها هنوز اعلام میشوند.
- سؤال زنده ماندن dedup؟ نه به Safety Down میرسد: مجموعهٔ پاکشده دو بار اعمال میکند — حالت dedup را persist کن.
- سؤال رد همهچیز؟ به Safety Vacuous میرسد و به Rule میرود: صفر اثر و صفر progress — برای progress پذیرش بگیر.
- Partition Heals? No drops Liveness Down: the valid message never effects, nothing applies twice — assumptions needed: heal plus capacity.
- Redelivery Storm? Absorbed drops Both Hold: the same ID many times still effects once — bounds still stated.
- Dedup Survives? No drops Safety Down: the wiped set double-applies — persist the dedup state.
- Reject Everything? drops Safety Vacuous and walks to the Rule: zero effects, zero progress — admit to progress.
نقطهٔ تصمیم: نقطهٔ تصمیم ترتیب probe است: اول پارتیشن (فقط liveness)، دوم طوفان (هر دو میمانند)، سوم dedup (فقط safety) و آخر reject-all (vacuous). به ترتیب دیگر اجراشان کن الگوی استقلال هنوز میماند — ترتیب آموزشی است و رأیها ساختاریاند.
رد خرابی: probeها را چپبهراست اجرا کن: پارتیشن هرگزخوبنشونده رأی Liveness Down میدهد با safety دستنخورده؛ طوفان رأی Both Hold میدهد؛ dedup پاکشده رأی Safety Down میدهد با progress سالم؛ reject-all رأی Safety Vacuous میدهد با liveness مرده. هر probe حداکثر یکچیز را میشکند — همان استقلال یافته است.
تضمین نشاندادهشده: شکل هر چهار ادعای استقلال را با ساختار اثبات میکند — برای هر ادعا یک probe. اعداد T و H و ظرفیت تو را تعیین نمیکند — آنها اندازهگیریهای هر صفاند.
تضمین نشانندادهشده: نویسندههای بیزانسی یا consensus را پوشش نمیدهد — این drill فقط خطاهای crash و شبکه را فرض میکند.
Decision point: The decision point is the probe order: partition first (liveness alone), storm second (both hold), dedup third (safety alone), reject-all last (vacuous). Run them in another order and the independence pattern still holds — the order is pedagogical, the verdicts are structural.
Failure trace: Run the probes left to right: the never-healing partition verdicts Liveness Down with safety untouched; the storm verdicts Both Hold; the wiped dedup verdicts Safety Down with progress intact; the reject-all verdicts Safety Vacuous with liveness dead. Each probe breaks at most one thing — that independence is the finding.
Guarantee shown: The figure proves the four independence claims by construction — one probe per claim. It does not set your T, H, or capacity numbers — those are per-queue measurements.
Not shown: It does not cover Byzantine writers or consensus — this drill assumes crash and network faults only.
خودآزماییSelf-check
داشبورد safety سبز و progress صفر نشان میدهد. کدام probe شلیک کرده و سبز اینجا چه معنایی دارد؟
پاسخ: Safety: چون هیچچیز دو بار اعمال نشده خاصیت safety میماند — بهصورت vacuous روی صفر اثر. liveness مرده چون هیچ پیام معتبری هرگز اثر نمیکند. چراغ سبز safety با صفر progress امضای outage از نوع reject-all است.
The dashboard shows safety green and progress zero. Which probe fired, and what does green mean here?
Answer: Safety: nothing applied twice, so the safety property holds — vacuously, on zero effects. Liveness is dead because no valid message ever effects. A green safety light with zero progress is the signature of a reject-all outage.
گزینههای جایگزینAlternatives
- تحویل at-most-once بهعنوان گزارهٔ safety («هر پیام حداکثر یکبار تحویل میشود»): روی سیم سادهتر دیده میشود ولی پیام گمشده بعد گمشدن داده با چراغ سبز safety است — شکستهای progress را داخل چراغ سبز پنهان میکند. مگر وقتی transport واقعاً نمیتواند redeliver کند، گزارهٔ مبتنی بر اثر را ترجیح بده.
- تأیید سرتاسری بهعلاوهٔ بازپخش اپراتوری برای liveness: بهجای قول progress خودکار، قول تشخیص بده (هر پیام تأییدنشده visible است) بهعلاوهٔ مسیر بازپخش runbookشده. درست وقتی redelivery خودکار ناامن است (اثرهای non-idempotent) — آنوقت liveness به کران human-in-the-loop صادقانهاعلامشده تبدیل میشود.
- At-most-once delivery as the safety statement ('each message delivered at most once'): simpler to observe on the wire, but a lost message is then a safety-preserving data loss — it hides progress failures inside a green light. Prefer the effect-based statement unless the transport truly cannot redeliver.
- End-to-end acknowledgments plus operator replay for liveness: instead of promising automatic progress, promise detection (every un-acked message is visible) plus a runbooked replay path. Right when automatic redelivery is unsafe (non-idempotent effects) — liveness becomes a human-in-the-loop bound, stated honestly.
سناریوهای خرابیFailure scenarios
- پارتیشن دائمی: پیام معتبر هرگز نمیرسد — liveness شکسته و safety دستنخورده. نگهبان: فرض اعلامشدهٔ خوب شدن بهعلاوهٔ بافر و بازپخش هنگام خوب شدن؛ بدون فرض، ادعای liveness آرزوست.
- گم شدن حالت dedup در restart: تحویل دوبارهای که خوب شدن میآورد دو بار اعمال میکند — safety شکسته ولی liveness میماند. نگهبان: persist کردن مجموعهٔ dedup قبل از تأیید، تکثیرشده با اثرها.
- outage از نوع reject-all (پیکربندی مسموم یا گیت بسته): هر پیام رد میشود — safety بهصورت vacuous سبز و liveness مرده و داشبورد با حذف دروغ میگوید. نگهبان: معیار progress (اثرکرده در دقیقه) کنار هر چراغ safety و پیج وقتی progress صفر شد.
- تحویل at-most-once که بهعنوان «بدون تکراری و بدون گمشدن» فروخته شود: پیام انداختهشده در معیار safety موفق شمرده میشود ولی اثر کسبوکاری هرگز رخ نمیدهد. نگهبان: اثرها را اعلام کن نه تحویلها را — پیامهای اثرکرده را بشمار و بقیه را redeliver کن و بگذار liveness مالک شکاف باشد.
- Permanent partition: the valid message never arrives — liveness violated, safety untouched. Guard with the stated healing assumption plus buffering and replay on heal; without the assumption the liveness claim is a wish.
- Dedup state lost across restart: the redelivery that healing brings double-applies — safety violated while liveness holds. Guard by persisting the dedup set before acking, replicated with the effects.
- Reject-all outage (poison config, closed gate): every message refused — safety vacuously green, liveness dead, and the dashboard lies by omission. Guard with a progress metric (effected-per-minute) beside every safety light, paging when progress hits zero.
- At-most-once delivery sold as 'no duplicates, no loss': a dropped message is counted as success by the safety metric while the business effect never happens. Guard by stating effects, not deliveries — count effected messages, redeliver the rest, and let liveness own the gap.
کد قابل اجراRunnable code
اجراشده و تأییدشده در همین محیطExecuted and verified in this environment
این کد آموزشی و سادهسازیشده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچکس اینشکلی کد پروداکشن نمیزند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.
import java.util.*;
// Teaching demo: safety = "no message takes effect twice" (nothing bad);
// liveness = "every valid message eventually takes effect under recovery"
// (something good, needs stated time/network assumptions). Each can break
// without the other.
public class SafetyLivenessDemo {
static class Consumer {
Set<String> dedup = new HashSet<>(); // dedup state (must survive restart)
Map<String, Integer> effects = new HashMap<>(); // application effects per message
void deliver(String id) {
if (!dedup.add(id)) return; // redelivery: no second effect
effects.merge(id, 1, Integer::sum);
}
boolean safetyHolds() { return effects.values().stream().allMatch(c -> c <= 1); }
}
public static void main(String[] a) {
// 1. redelivery with dedup: both hold
Consumer c1 = new Consumer();
c1.deliver("m1"); c1.deliver("m1"); c1.deliver("m2"); // m1 redelivered
System.out.println("redelivery+dedup: effects=" + c1.effects
+ " SAFETY=" + (c1.safetyHolds() ? "HOLDS" : "VIOLATED") + " LIVENESS=HOLDS");
// 2. permanent partition: m3 never arrives
Consumer c2 = new Consumer();
c2.deliver("m1");
boolean m3arrived = false; // partition never heals
System.out.println("permanent-partition: m3 arrived=" + m3arrived
+ " SAFETY=HOLDS (nothing applied twice) LIVENESS=VIOLATED (m3 never effected)");
// 3. dedup state lost across restart: redelivery double-applies
Consumer c3 = new Consumer();
c3.deliver("m1");
c3.dedup.clear(); // crash lost dedup state
c3.deliver("m1"); // redelivery after restart
System.out.println("dedup-lost: effects=" + c3.effects
+ " SAFETY=" + (c3.safetyHolds() ? "HOLDS" : "VIOLATED") + " LIVENESS=HOLDS (m1 did take effect)");
// 4. reject-all service: drops every message
Consumer c4 = new Consumer();
System.out.println("reject-all: effects=" + c4.effects
+ " SAFETY=HOLDS vacuously (zero effects) LIVENESS=VIOLATED (nothing progresses)");
System.out.println("OK: safety and liveness fail independently; liveness needs recovery assumptions");
}
}
اجرا: javac SafetyLivenessDemo.java && java SafetyLivenessDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: چهار خط سناریو: redelivery+dedup (هر دو HOLDS)، پارتیشن دائمی (safety میماند و liveness میشکند)، dedup گمشده (safety میشکند با effects={m1=2} و liveness میماند) و reject-all (safety بهصورت vacuous میماند و liveness میشکند)؛ بهعلاوهٔ خط OK. (خروجی واقعی همین محیط)
Run: javac SafetyLivenessDemo.java && java SafetyLivenessDemo — stdlib Java only, instant.
Expected output: Four scenario lines: redelivery+dedup (both HOLDS), permanent-partition (safety HOLDS, liveness VIOLATED), dedup-lost (safety VIOLATED with effects={m1=2}, liveness HOLDS), reject-all (safety HOLDS vacuously, liveness VIOLATED); plus the OK line. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): سیستم همهٔ پیامها را رد کند؛ کدام خاصیت ظاهراً سالم میماند؟Follow-up (Senior): The service rejects every message. Which property can appear healthy?
Safety — و دقیقاً به همین دلیل جواب جالب است. با صفر اثر، صفر تکراری هست، پس «هیچ پیامی دو بار اثر نمیکند» بهصورت vacuous میماند؛ چراغ داشبورد سبز میماند. liveness («هر پیام معتبر بالاخره اثر میکند») مرده: هیچچیز پیش نمیرود. درس: چراغ safety بدون معیار progress دروغ با حذف است — همیشه جفتشان کن و روی صفر شدن اثرکرده-در-دقیقه پیج بده، نه فقط روی safety.
Safety — and that is exactly why the answer is interesting. With zero effects there are zero duplications, so 'no message takes effect twice' holds vacuously; the dashboard light stays green. Liveness ('every valid message eventually takes effect') is dead: nothing progresses. The lesson: a safety light without a progress metric is a lie by omission — always pair them, and page on effected-per-minute hitting zero, not on safety alone.
پیگیری (استف): نشان دادی liveness تنها میشکند. آیا safety میتواند بشکند ولی progress ادامه داشته باشد؟Follow-up (Staff): You showed liveness breaking alone. Can safety break while progress continues?
بله — سناریوی dedup گمشده دقیقاً برعکس است: m1 تحویل میشود و crash مجموعهٔ dedup را پاک میکند و تحویل دوباره m1 را بار دوم اعمال میکند (effects={m1=2}). safety شکسته — یک پیام و دو اثر — ولی liveness میماند چون m1 اثر کرد (دو بار). پس استقلال هر دو طرف میرود: progress بدون safety (اعمال دوباره) و safety بدون progress (پارتیشن و reject-all). هر بازبینی طراحی باید هر دو ضدمثال را بخواهد، نه فقط مد روز را.
Yes — the dedup-lost scenario is exactly the reverse: m1 is delivered, the crash wipes the dedup set, the redelivery applies m1 a second time (effects={m1=2}). Safety is violated — one message, two effects — while liveness holds, because m1 did take effect (twice). So the independence runs both ways: progress without safety (double-apply) and safety without progress (partition, reject-all). Any design review must ask for both counterexamples, not just the fashionable one.
اشتباهات رایجCommon mistakes
- اعلام safety روی تحویلها («حداکثر یکبار») و تمام دانستنش: پیام گمشده تحویل at-most-once را نگه میدارد ولی اثر کسبوکاری ناپدید میشود. safety باید روی اثرها اعلام شود وگرنه گمشدن داخل تضمین پنهان میشود.
- قول liveness بدون فرضهای بازیابی («هر پیام بالاخره تحویل میشود»): بالاخره زمان نیست و پارتیشن تأخیر نیست و دیسک پر شبکه نیست. liveness بیقید آرزوست — خوب شدن و ظرفیت و deadline را نام ببر یا ادعا را پس بگیر.
- خواندن سبز safety بهمعنای سلامت در outage از نوع reject-all: صفر اثر یعنی صفر تکراری، پس چراغ سبز میماند ولی کسبوکار ایستاده. هر داشبورد safety دوقلوی progress خودش را میخواهد.
- Stating safety on deliveries ('at most once') and calling it done: a lost message preserves at-most-once delivery while the business effect vanishes. Safety must be stated on effects, or loss hides inside the guarantee.
- Promising liveness without recovery assumptions ('every message is eventually delivered'): eventually is not a time, a partition is not a delay, and a full disk is not a network. Unconditional liveness is a wish — name heal, capacity, and deadline or withdraw the claim.
- Reading safety-green as healthy during a reject-all outage: zero effects means zero duplications, so the light stays green while the business stands still. Every safety dashboard needs its progress twin.
منابع:Sources: Jepsen · consistency models · Kafka 4.1 · design and delivery semantics