11 تمرین ۱ — ساعتِ Lamport عملیExercise 1 — a working Lamport clock
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- سه پروسه A و B و C با شناسه یکتا؛ tie-break فقط یکتایی میخواهد نه هیچچیز دیگر.
- سناریوی آموزشی از نمودار ۵ فصل: A و C هرکدام یکبار میفرستند و B هر دو را میگیرد بعد جواب میدهد.
- پیامها میتوانند تأخیر داشته باشند ولی قاعده دریافت per-delivery اعمال میشود؛ هیچ ساعت دیواری هیچجا نیست.
- Three processes A, B, C with unique IDs; the tie-break needs uniqueness, nothing else.
- Teaching scenario from chapter diagram 5: A and C post once each, B merges both then replies.
- Messages can be delayed but the receive rule is applied per delivery; no wall clock is used anywhere.
چرا این روش و این عددها؟Why this method and these numbers?
قاعدهٔ receive=max(local,received)+1 باعث میشود پیامِ دریافتشده بعد از ارسالش timestamp بزرگتری داشته باشد؛ جمع یک برای جداشدن رویداد دریافت از قبلی است. دو رویداد بیرابطه ممکن است timestamp برابر یا مرتب داشته باشند، پس عکس رابطهٔ happened-before برقرار نیست. process_id فقط tie-break قطعی میدهد؛ از آن علتومعلول نمیسازیم. سه process کمترین سناریوی روشن برای ارسال مستقل و دریافت مشترک را فراهم میکنند.
Receive=max(local,received)+1 gives reception a timestamp greater than its send; adding one distinguishes the receive event. Unrelated events may have equal or ordered timestamps, so the converse of happened-before fails. Process IDs provide deterministic tie-breaking, not causality. Three processes make independent sends and a shared receiver easy to inspect.
مدل ذهنیMental model
حل گامبهگامStep by step
- اول شمارش یک شمارنده برای هر پروسه از صفر. هر رخداد محلی و هر ارسال +1 میزند — ساعت فقط جلو میرود، یک قدم per-event.
- پذیرش روی دریافت روی دریافت max بگیر. سناریوی کتاب را برو: B از A میگیرد max(0,1)=1 و از C میگیرد max(1,1)=1، بعد ارسالِ جواب ۱+۱=۲ میزند. هیچ دریافتی ساعت را عقب نمیبرد.
- مرتبسازی با tie-break مهرها را بهصورت (counter, processID) مرتب کن با tie-break شناسه: (1,A) < (1,C) < (2,B). ترتیب کلی که به هر زنجیره پیام احترام میگذارد — پستهای همزمان A و C فقط ترتیب شناسه میگیرند.
- نام بردن ناتوانی محدودیت را با تله نام ببر: x=(5,A) و y=(3,C) بدون پیام — مهرهای مرتب و صفر علّیت. تشخیص همزمانی ساعت برداری میخواهد: یک ورودی per-process و حالت و بایت مهر O(n).
- Count first One counter per process, starting at 0. Every local event and every send ticks it +1 — the clock only moves forward, one step per event.
- Adopt on receive On receive, adopt max(local, seen). Walk the book's scenario: B takes max(0,1)=1 from A, max(1,1)=1 from C, then its reply-send ticks 1+1=2. No receive ever moves a clock backwards.
- Sort with tie-break Sort stamps as (counter, processID) with the process ID breaking ties: (1,A) < (1,C) < (2,B). A total order that respects every message chain — concurrent posts A and C simply take ID order.
- Name what it cannot do Name the limit with the trap: x=(5,A) and y=(3,C) with no messages — ordered stamps, zero causality. Concurrency detection needs vector clocks: one entry per process, O(n) state and stamp bytes.
طراحی مشخصConcrete design
یک counter (عدد ۶۴بیتی) بهعلاوه یک processID تغییرناپذیر برای هر پروسه. پاکت پیام (counter, processID) را حمل میکند؛ harness هر ارسال/دریافت را با مهرش ثبت میکند. مسیر خواندن: مرتبسازی مهرهای ثبتشده با (counter, processID). مسیر نوشتن: اعمال دو قاعده در هر رخداد؛ بدون هماهنگی و بدون RPC اضافه.
One counter (u64) plus one immutable processID per process. Message envelope carries (counter, processID); the harness logs every send/receive with its stamp. Read path: sort logged stamps by (counter, processID). Write path: apply the two rules at each event; no coordination, no extra RPC.
مثال محاسبهشدهWorked example
گام آموزشی (نمودار ۵ فصل): A میفرستد (1,A)؛ C میفرستد (1,C)؛ B نگه میدارد max(0,1)=1 بعد max(1,1)=1؛ ارسالِ جواب B میشود (2,B)؛ A میگیرد max(1,2)=2. شمارندههای پایانی: A=2 و B=2 و C=1. مهرهای مرتب: (1,A) < (1,C) < (2,B).
Teaching walk (chapter diagram 5): A sends (1,A); C sends (1,C); B holds max(0,1)=1, then max(1,1)=1; B's reply-send ticks to (2,B); A adopts max(1,2)=2. Final counters: A=2, B=2, C=1. Sorted stamps: (1,A) < (1,C) < (2,B).
دانلود نمودار مستقلDownload standalone diagram
- A میفرستد: شمارنده صفر به یک و پیام (1,A) را میبرد. C میفرستد: شمارنده صفر به یک و پیام (1,C) را میبرد.
- B هر دو را میگیرد: max(0,1)=1 بعد max(1,1)=1. جواب B یک رخداد ارسال است: ۱+۱=۲ با مهر (2,B)؛ و A میگیرد: max(1,2)=2.
- باند تله: x روی A با مهر (5,A) و y روی C با مهر (3,C) و هیچ پیامی — (3,C) < (5,A) هیچچیز را ثابت نمیکند چون x و y هرگز هم را ندیدند.
- A posts: counter 0 to 1, message carries (1,A). C posts: counter 0 to 1, message carries (1,C).
- B receives both: max(0,1) = 1, then max(1,1) = 1. B's reply is a send event: 1 + 1 = 2, carrying (2,B); A adopts max(1,2) = 2.
- Trap band: x on A stamped (5,A) and y on C stamped (3,C) with no messages at all — (3,C) < (5,A) proves nothing, because x and y never met.
نقطهٔ تصمیم: نقطه تصمیم شمارنده B بعد از هر دریافت است: max را بگیر و هرگز عقب نرو. بقیه فقط مرتبسازی است.
رد خرابی: نپذیرفتن مهر دیدهشده را دنبال کن: اگر B مهر دیدهشده را نادیده بگیرد و روی صفر بماند، جوابش (1,B) میشود — کوچکتر از (1,C) که دیده بود و ساعت به عقب برمیگردد. قاعده دریافت دقیقاً برای ممنوع کردن همین است.
تضمین نشاندادهشده: شکل نشان میدهد دو قاعده مهرهای کتاب را میسازند و بند تله ترتیب بدون علّیت را نشان میدهد. زمان دیواری نشان نمیدهد — چون هیچجا نیست.
تضمین نشانندادهشده: تشخیص همزمانی و رفتار با گم شدن پیام بیرون این شکلاند — اسمبلی قیمت ساعت برداری را برای اولی نام میبرد.
Decision point: The decision point is B's counter after each receive: adopt the max, never go backwards. Everything else is sorting.
Failure trace: Trace a lost adoption: if B ignored the seen stamp and kept 0, its reply would carry (1,B) — smaller than the (1,C) it already saw, and the clock would move backwards. The receive rule exists exactly to forbid that.
Guarantee shown: The figure shows the two rules producing the book's stamps and the trap segment showing order without causality. It does not show wall-clock time — there is none.
Not shown: Concurrency detection and message-loss handling are outside this figure — the assembly names the vector-clock price for the first.
خودآزماییSelf-check
بعد از گرفتن (1,A) و بعد (1,C)، مقدار B چیست و چرا جواب (2,B) است؟
پاسخ: B بعد از دریافت اول ۱ است (max(0,1)) و بعد از دومی هنوز ۱ (max(1,1))؛ ارسالِ جواب به ۲ میرسد پس (2,B). آن +1 که بعضی ارائهها روی دریافت میگذارند همان افزایشی است که روی ارسال بعد حساب میشود — ترتیب هر دو جور یکی است.
After receiving (1,A) then (1,C), what does B hold, and why is the reply (2,B)?
Answer: B holds 1 after the first receive (max(0,1)) and still 1 after the second (max(1,1)); the reply-send ticks to 2, hence (2,B). The +1 some presentations put on receive is the same increment charged on the next send — the order is identical either way.
دانلود SVG مستقلDownload standalone SVG
- بالا: (1,A) و (1,C) و (2,B) بهترتیب — ترتیب کلی که tie-break میسازد.
- وسط: x=(5,A) و y=(3,C) که هرکدام «بدون پیام ورودی/خروجی» دارند و میانشان «هیچ مسیر هیچطرف».
- پایین: جعبه زرد قاعده — max را بگیر و عقب نرو — و قیمت حقیقت واقعی همزمانی: ساعت برداری.
- Top: (1,A), (1,C), (2,B) in order — the total order the tie-break produces.
- Middle: x = (5,A) and y = (3,C), each boxed with 'no message in or out', and 'no path either way' between them.
- Bottom: the yellow rule box — adopt max, never backwards — and the price of real concurrency truth: vector clocks.
نقطهٔ تصمیم: نقطه تصمیم برچسب وسط است: «هیچ مسیری هیچطرف». مهر بدون پیام بحث را تمام میکند.
رد خرابی: جعبههای تله را دنبال کن: x با مهر (5,A) و y با مهر (3,C) و هیچ پیکیانی میانشان. هرکس بخواند «(3,C) < (5,A) پس y علّت x است» پیامی را اختراع کرده که هرگز نبوده.
تضمین نشاندادهشده: شکل تبادل مرتب و تله بدون پیام را کنار هم نشان میدهد، بهعلاوه قاعده و قیمتش. حتی یک جفت همزمان را تشخیص نمیدهد — نکته همین است.
تضمین نشانندادهشده: گم شدن و تکراری پیام و برخورد شناسه پروسه بیرون این شکلاند — مال سناریوهای خرابیاند.
Decision point: The decision point is the center label: 'no path either way'. Stamps without messages end the argument.
Failure trace: Trace the trap boxes: x stamped (5,A), y stamped (3,C), no arrows between them. Anyone reading '(3,C) < (5,A), so y caused x' has invented a message that never existed.
Guarantee shown: The figure shows the sorted exchange and the message-free trap side by side, plus the rule and its price. It does not detect a single concurrent pair — that is the point.
Not shown: Message loss, duplication and process-ID collision are outside this figure — they belong to the failure scenarios.
خودآزماییSelf-check
آیا (3,C) < (5,A) یعنی y قبل از x رخ داده؟
پاسخ: نه. مهرها نمایش را مرتب میکنند؛ فقط زنجیره پیام علّتها را مرتب میکند. x و y تا پیامی وصلشان کند همزماناند و مهر Lamport این را نمیگوید.
Does (3,C) < (5,A) mean y happened before x?
Answer: No. The stamps order the display; only a message chain orders causes. x and y are concurrent until a message connects them, and Lamport stamps cannot tell you that.
گزینههای جایگزینAlternatives
- ساعت برداری (یک شمارنده per-process): هم علیّت و هم همزمانی را تشخیص میدهد، به قیمت حالت O(n) در هر گره و O(n) بایت در هر پیام. فقط وقتی بپرداز که محصول باید «همزمان» را بفهمد — ویرایش مشترک، تحویل علّی، فراداده CRDT — نه برای مرتبسازی ساده.
- Vector clocks (one counter per process): detect causality AND concurrency, at O(n) state per node and O(n) bytes per message. Pay for it only when the product must know 'concurrent' — collaborative editing, causal delivery, CRDT metadata — not for plain ordering.
سناریوهای خرابیFailure scenarios
- گم شدن یا تکراری پیام: پذیرش idempotent است (max دو بار همان یکبار است) ولی پیام گمشده گیرنده را جلو نمیبرد — ترتیب معتبر میماند فقط درشتتر. هرگز مهری که نگرفتی حدس نزن.
- برخورد شناسه پروسه: دو پروسه با یک شناسه tie-break را میشکنند و دو رخداد مهر یکسان میگیرند. نگهبان: شناسه از یک رجیستری (یا همان oracle تمرین ۳) و crash روی ثبت تکراری.
- سرریز شمارنده: شمارنده ۶۴بیتی با نرخ آموزشی عملاً تمامنشدنی است؛ شمارنده کوچک میچرخد و ترتیب را میشکند. نگهبان: ۶۴بیت ثابت و assert — هرگز بیصدا نچرخ.
- Lost or duplicated message: adoption is idempotent (max twice is max once), but a lost message simply never advances the receiver — the order stays valid, only coarser. Never 'fill in' a stamp you did not receive.
- Process-ID collision: two processes sharing one ID break the tie-break and can stamp two events identically. Guard: assign IDs from one registry (or reuse the oracle of exercise 3) and crash on duplicate registration.
- Counter overflow: a u64 counter ticking per event lasts effectively forever at teaching rates; a tiny counter wraps and breaks the order. Guard: fix u64 and assert — never silently wrap.
کد قابل اجرا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: Lamport clocks on 3 processes (chapter diagram 5).
// send/local -> c+=1, stamp (c,pid); receive(m) -> adopt max(c,m.ts).
public class LamportDemo {
record Ts(long c, String pid) implements Comparable<Ts> {
public int compareTo(Ts o) {
int r = Long.compare(c, o.c);
return r != 0 ? r : pid.compareTo(o.pid);
}
}
static class Proc {
final String id; long c = 0;
Proc(String id) { this.id = id; }
Ts send() { c++; return new Ts(c, id); }
void receive(Ts m) { c = Math.max(c, m.c()); }
}
public static void main(String[] a) {
Proc A = new Proc("A"), B = new Proc("B"), C = new Proc("C");
Ts mA = A.send(), mC = C.send();
B.receive(mA); B.receive(mC);
Ts mB = B.send(); A.receive(mB);
System.out.println("stamps: A=" + mA + " C=" + mC + " B-reply=" + mB);
List<Ts> order = new ArrayList<>(List.of(mB, mC, mA));
Collections.sort(order);
System.out.println("total order = " + order);
Ts x = new Ts(1, "A"), y = new Ts(1, "C");
System.out.println("L(x)=" + x + " < L(y)=" + y + " but no causal path");
if (!order.equals(List.of(new Ts(1, "A"), new Ts(1, "C"), new Ts(2, "B"))))
throw new AssertionError();
System.out.println("OK: receive adopts max; total order <> causal order");
}
}
اجرا: javac LamportDemo.java && java LamportDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: مهرها: A=(1,A) و C=(1,C) و B=(2,B)؛ ترتیب کلی دقیقاً (1,A) < (1,C) < (2,B)؛ L(x) < L(y) بدون مسیر علّی؛ OK. (خروجی واقعی همین محیط)
Run: javac LamportDemo.java && java LamportDemo — stdlib Java only, instant.
Expected output: stamps A=(1,A), C=(1,C), B-reply=(2,B); sorted total order exactly (1,A) < (1,C) < (2,B); L(x) < L(y) with no causal path; OK. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): دو رخداد بدون هیچ پیام چه timestampهایی میتوانند داشته باشند؟Follow-up (Senior): What timestamps can two independent events with no messages have?
هر جفتی — مثلاً (5,A) و (3,C) یا (1,A) و (100,C). بدون پیام هیچ قیدی نیست: هر شمارنده فقط خودش را دیده. همین آزادی دقیقاً چرایی ناتوانی مهر Lamport در تشخیص همزمانی است: مهرهای مرتب بدون زنجیره پیام هیچچیز را ثابت نمیکنند.
Any pair at all — for example (5,A) and (3,C), or (1,A) and (100,C). With no messages there is no constraint: each counter only ever saw itself. That freedom is exactly why Lamport stamps cannot detect concurrency: equal-or-ordered stamps without a message chain prove nothing.
پیگیری (سینیور): ساعت برداری با هزینه <span class="ltr">O(n)</span> کی به <span class="ltr">Lamport</span> میارزد؟Follow-up (Senior): When is a vector clock worth its O(n) cost over Lamport?
وقتی محصول باید همزمانی را «بداند» نه فقط مرتبش کند: ویرایش مشترک (ویرایشهای همزمان ادغام و بعدیهای علّی اعمال)، انتشار علّی، فراداده CRDT. هزینه: O(n) شمارنده در هر گره و O(n) بایت مهر در هر پیام، بهعلاوه منطق ادغام در هر دریافت. پیشفرض Lamport؛ ارتقا فقط با سؤال همزمانی نامدار.
When the product must KNOW concurrency, not just order it: collaborative editing (concurrent edits merge, causally-after edits apply), causal broadcast, CRDT metadata. Cost: O(n) counters per node and O(n) stamp bytes per message, plus join logic on every receive. Default to Lamport; upgrade only on a named concurrency question.
اشتباهات رایجCommon mistakes
- خواندن L(a) < L(b) بهمعنای «a علّت b است». ترتیب مهر ترتیب نمایش است؛ علّیت زنجیره پیام از a به b میخواهد.
- ادعای تشخیص همزمانی با Lamport («مهر برابر یعنی همزمان»). دو رخداد بدون پیام هر مهری میتوانند داشته باشند — برابری هم چیزی را ثابت نمیکند.
- دست بردن به همگامسازی ساعت دیواری. Lamport اصلاً ساعت همگام نمیخواهد — نکته همین است؛ افزودن NTP اینجا هیچچیز را حل نمیکند و فقط حالت خرابی skew میافزاید.
- Reading L(a) < L(b) as 'a caused b'. The stamp order is display order; causality needs a message chain from a to b.
- Claiming Lamport detects concurrency ('equal stamps mean concurrent'). Two message-free events may hold any stamps — equality proves nothing either.
- Reaching for wall-clock synchronization. Lamport needs no synchronized clocks at all — that is the point; adding NTP solves nothing here and adds skew failure modes.
منابع:Sources: Lamport · Time, Clocks, and Ordering · 1978 · Atlassian · engineering interview handbook
22 تمرین ۲ — quorum و نقض خطیپذیریExercise 2 — quorums and a linearizability violation
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- سه replica به نام n1 و n2 و n3 با membership ثابت؛ حین سناریو rebalancing نیست.
- تکنویسنده با نسخههای مرتب v1 (کهنه) v2 (تازه)؛ نوشتن هدف w=3 دارد ولی فقط n1 نسخه v2 را ذخیره کرده — جزئی و هرگز تأییدشده نیست.
- خواندنها با r=2 بیشینه نسخه دیدهشده را برمیگردانند؛ خواندن خواننده B بعد از برگشتن خواندن A شروع میشود.
- Three replicas n1/n2/n3, fixed membership; no rebalancing during the scenario.
- Single writer, ordered versions v1(old) < v2(new); the write targets w=3 but only n1 has stored v2 — partial, never acknowledged.
- Reads take r=2 and return the max version they meet; reader B's read starts after reader A's read returned.
چرا این روش و این عددها؟Why this method and these numbers?
read1 مقدار write ناتمام را میتواند ببیند، ولی اگر آن مقدار را برگرداند، read2 بعدی دیگر نمیتواند به قبلش برگردد. write-back پیش از پاسخ، مقدار دیدهشده را روی اکثریت میگذارد؛ هر اکثریت بعدی با آن اشتراک دارد. پس برای n=3 دو عضو کافی است؛ درخواست ACK هر سه در مثال فقط پنجرهٔ write ناتمام را واضح میکند. اثبات به monotonic نسخه، membership ثابت و قواعد writer وابسته است، نه صرف جمع r+w.
Read1 may observe an incomplete write, but once it returns that value a later Read2 cannot go backwards. Before-return write-back places the observed version on a majority; every later majority intersects it. With n=3, two replicas suffice; targeting all three writes merely exposes the incomplete-write window. The proof also needs monotonic versions, fixed membership and a defined writer protocol.
مدل ذهنیMental model
حل گامبهگامStep by step
- جزئی یعنی تأییدنشده هدف را از تأییدشده جدا کن: نوشتن w=3 را نشانه گرفت ولی فقط n1 نسخه v2 را ذخیره کرد. نامش جزئی — هیچ quorumای مدیون رؤیتپذیریاش نیست.
- بازپخش نقض جفت ناقض را اجرا کن: خواندن اول از n1 و n2 برمیگرداند max(v2,v1)=v2 (همزمان، مجاز)؛ خواندن دوم از n2 و n3 بعد از برگشتن اول شروع میشود و میگیرد max(v1,v1)=v1. تازه-بعد-کهنه با نامساوی سالم.
- برگرداندن قبل از بازگشت درست ABD را اعمال کن: خواننده v2 را به n1 و n2 — یک quorum — قبل از برگشتن برمیگرداند. خواندن دوباره از n2 و n3 حالا v2 را میبیند. تعمیر قبل از بازگشت تضمین است؛ تعمیر بعدش آرایش.
- نام بردن فرضها فرضهای مانده را صادقانه بگو: اینجا تکنویسنده (چندنویسنده query timestamp قبل از نوشتن میخواهد)، membership ثابت (rebalancing quorum مشترک میخواهد) و نسخههای مرتب با ساخت، هرگز با ساعت دیواری.
- Partial is not acked Separate targeted from acknowledged: the write aimed at w=3 but only n1 stored v2. Name it partial — no quorum owes it visibility.
- Replay the violation Run the violating pair: read1 from {n1,n2} returns max(v2,v1)=v2 (concurrent, allowed); read2 from {n2,n3} starts after read1 returned and gets max(v1,v1)=v1. New-then-old with the inequality intact.
- Write back before return Apply the ABD fix: the reader writes v2 back to {n1,n2} — a quorum — BEFORE returning. The re-read from {n2,n3} now meets v2. Repair before the return is the guarantee; repair after it is cosmetics.
- Name the assumptions State the remaining assumptions honestly: single writer here (multi-writer needs a pre-write timestamp query), fixed membership (rebalancing needs joint quorums), and versions ordered by construction, never by wall clock.
طراحی مشخصConcrete design
هر replica جفت (version, value) را با نسخههای کاملاً مرتب نگه میدارد. مسیر نوشتن (تکنویسنده): مهر v+1 بزن، به هر سه بفرست و منتظر w تأیید بمان — اینجا فقط یکی میرسد پس نوشتن در پرواز میماند. مسیر خواندن: از r=2 بپرس، بیشینه را بردار، به r=2 برگردان، بعد برگرد. چندنویسنده فاز query میافزاید: قبل از مهر زدن بالاترین timestamp را از quorum بخوان.
Each replica stores (version, value) with totally ordered versions. Write path (single writer): stamp v+1, send to all three, wait for w ACKs — here only one lands, so the write stays in flight. Read path: query r=2, pick max version, write it back to r=2, then return. Multi-writer adds a query phase: read the highest timestamp from a quorum before stamping the write.
مثال محاسبهشدهWorked example
اعداد آموزشی: replicaها نگه میدارند (v2=new) روی n1 و (v1=old) روی n2 و n3. خواندن اول از n1 و n2 میشود max(2,1)=v2. خواندن دوم از n2 و n3 میشود max(1,1)=v1 — نقض. write-back نسخه v2 را روی n2 میگذارد؛ خواندن دوباره میشود max(2,1)=v2 — درست شد. هزینه درست (آموزشی): یک round-trip اضافه quorum روی هر خواندن.
Teaching numbers: replicas hold (v2=new) on n1, (v1=old) on n2/n3. Read1 {n1,n2} = max(2,1) = v2. Read2 {n2,n3} = max(1,1) = v1 — violation. Write-back puts v2 on n2; re-read {n2,n3} = max(2,1) = v2 — fixed. Cost of the fix (teaching): one extra quorum round-trip on every read.
دانلود نمودار مستقلDownload standalone diagram
- باند بالا: نوشتن v2=new فقط روی n1 مینشیند؛ پیکانهای n2 و n3 چیندار و در پروازند و هرگز ذخیره نشدهاند — جزئی، نه تأییدشده.
- باند وسط: خواننده A از n1 و n2 میخواند و v2 برمیگرداند؛ خواننده B بعد از آن بازگشت شروع میکند و از n2 و n3 میخواند و v1 STALE برمیگرداند — زمان به عقب برگشت.
- باند پایین: نسخه بیشینه v2 قبل از برگشتن خواندن به quorum برمیگردد؛ خواندن دوباره هرجا نگاه کند v2 را میبیند.
- Top band: write v2=new lands on n1 only; the n2/n3 arrows are dashed in flight, never stored — partial, not acknowledged.
- Middle band: reader A reads {n1,n2} and returns v2; reader B starts after that return, reads {n2,n3} and returns v1 STALE — time moved backwards.
- Bottom band: the max version v2 goes back out to a quorum before the read returns; the re-read meets v2 everywhere it looks.
نقطهٔ تصمیم: نقطه تصمیم پیکان write-back است: نسخه بیشینه به quorum قبل از برگشتن خواندن. هرچه بعدش است همگرا میشود.
رد خرابی: باند وسط را دنبال کن: خط زندگی خواننده B بعد از پیکان بازگشت A شروع میشود ولی مقدار برگشتیاش v1 از v2 دیدهشده کهنهتر است. برچسب صورتی STALE دقیقاً روی همان قدم عقب است.
تضمین نشاندادهشده: شکل نوشتن جزئی و جفت ناقض و تعمیر write-back را بهترتیب نشان میدهد. نویسندههای همزمان و تغییر membership را نشان نمیدهد — کارت سوم هر دو را بهعنوان فرض اضافه نام میبرد.
تضمین نشانندادهشده: ماندگاری نوشتن در پرواز وعده داده نشده: v2 روی یک replica ممکن است با crash گم شود. خطیپذیری اینجا درباره تازگی خواندن است نه بقای نوشتن.
Decision point: The decision point is the write-back arrow: max version out to a quorum before the read returns. Everything after it converges.
Failure trace: Trace the middle band: reader B's lifeline starts after reader A's return arrow, yet its return value v1 is older than A's v2. The pink STALE label sits exactly on that backward step.
Guarantee shown: The figure shows the partial write, the violating pair, and the write-back repair in order. It does not show concurrent writers or membership change — the third card names both as extra assumptions.
Not shown: Durability of the in-flight write is not promised: v2 on one replica may still be lost on crashes. Linearizability here is about read freshness, not write survival.
خودآزماییSelf-check
چرا با اینکه w + r > n همهجا برقرار است خواندن کهنه هنوز ممکن است؟
پاسخ: چون نوشتن هرگز کامل نشد: فقط یک replica نسخه v2 را ذخیره کرد پس هیچ quorumای موظف به حملش نبود. w + r > n همپوشانی مجموعهها را وعده میدهد نه رؤیتپذیری نوشتن جزئی را — همپوشانی با replicaای که v2 را ندیده v1 برمیگرداند: برای مجموعهها قانونی و برای تازگی غلط.
w + r > n holds throughout. Why is the stale read still possible?
Answer: Because the write never completed: only one replica stored v2, so no quorum was obliged to carry it. w + r > n promises that quorums overlap, not that a partial write is visible — overlap with a replica that never saw v2 returns v1, legally for sets, wrongly for freshness.
دانلود SVG مستقلDownload standalone SVG
- چپ بالا: t0 — n1 سبز با v2 و n2 و n3 آبی با v1. زیرنویسش علنی میگوید: جزئی و هرگز تأییدشده بهعنوان w=3 نیست.
- پیکان وسط: برگرداندن v2 به quorum قبل از بازگشت. چپ پایین: t1 — n2 سبز شد.
- راست: خواندن اول v2 برمیگرداند (مجاز، همزمان) و خواندن دوم v1 STALE (نقض، دیرتر شروع شده) و خواندن دوباره v2 (درست شد).
- Left top: t0 — n1 green with v2, n2/n3 blue with v1. The caption says it plainly: partial, never acknowledged as w=3.
- Middle arrow: write-back v2 to the quorum, before return. Left bottom: t1 — n2 turned green.
- Right: read1 returns v2 (allowed, concurrent), read2 returns v1 STALE (violation, started later), re-read returns v2 (fixed).
نقطهٔ تصمیم: نقطه تصمیم جعبه زرد قاعده است: تعمیر باید قبل از بازگشت بنشیند نه بعدش. تعمیر پسزمینه هم صادقانه دیر برچسب خورده.
رد خرابی: سطر صورتی نتیجه را دنبال کن: {n2:v1, n3:v1} در خواندنی که بعد از برگشتن v2 شروع شده. بعد سطر سبز زیرش را دنبال کن: همان quorum بعد از پیکان write-back نسخه v2 برمیگرداند. تنها فرق دو سطر write-back است.
تضمین نشاندادهشده: شکل نسخهها و نتیجهها و قاعده تعمیر را با هم نشان میدهد. نه سقف مدت نوشتن در پرواز را میگوید نه بقایش را وعده میدهد.
تضمین نشانندادهشده: CAS خطیپذیر و تخصیص timestamp چندنویسنده بیرون این شکلاند — CAS طبق فصل consensus میخواهد، نه اینجا.
Decision point: The decision point is the yellow rule box: repair must land before the return, not after. Background repair is honestly labeled too late.
Failure trace: Trace the rose outcome row: {n2:v1, n3:v1} at a read that started after v2 was already returned. Then trace the green row below it: the same quorum after the write-back arrow returns v2. The only change between the rows is the write-back.
Guarantee shown: The figure shows versions, outcomes and the repair rule together. It does not bound how long the in-flight write takes, nor promise its survival.
Not shown: Linearizable CAS and multi-writer timestamp allocation are outside this figure — CAS needs consensus, stated in the chapter, not here.
خودآزماییSelf-check
آیا read-repair پسزمینه میتواند جای write-back را بگیرد؟
پاسخ: نه. تعمیر باید قبل از برگشتن خواندن به صداکننده تمام شود. کار پسزمینهای که بعداً میدود نمیتواند مقداری که قبلاً برگشته را عطفبماسبق خطیپذیر کند — صداکننده نقض را قبلاً دیده.
Can background read-repair replace the write-back?
Answer: No. The repair must complete before the read returns to its caller. A background task that runs later cannot retroactively linearize a value already returned — the caller already observed the violation.
گزینههای جایگزینAlternatives
- خواندن خطیپذیر رهبرمحور (خواندن از رهبر یا خواندن با تأیید رهبر): استدلالش سادهتر ولی هر خواندن هزینه latency رهبر را میدهد و رهبر گلوگاه است. وقتی رقابت کم است و سادگی به توان خواندن میارزد انتخابش کن؛ وقتی خواندن باید مقیاس بخورد خواندن رجیستری quorum را.
- Leader-based linearizable reads (read from the leader, or leader-confirmed reads): simpler to reason about, but every read pays leader latency and the leader is a bottleneck. Choose it when contention is low and simplicity beats read throughput; choose quorum register reads when reads must scale out.
سناریوهای خرابیFailure scenarios
- مرگ خواننده بعد از دیدن v2 ولی قبل از write-back: اگر صداکننده پاسخی نگرفت خواندن هرگز کامل نشد — یال تقدم نیست و write-back خواننده بعد هنوز v2 را پخش میکند. اگر v2 را برگرداند بعد وسط write-back مرد، خواندن کاملشده تقدم را گذاشت و وظیفه به خواننده بعد میرسد که هر بیشینهای ببیند برمیگرداند. کامل یعنی پاسخ به صداکننده رسید، نه اینکه خواننده مقدار را درون خود دید.
- نویسندههای همزمان بدون فاز query: دو نویسنده که مستقل مهر v2 میزنند مقدار را دوشاخه میکنند — یک نسخه با دو داده. نگهبان: اول بالاترین timestamp را از quorum بپرس (چندنویسنده ABD) تا نسخهها per-writer یکتا بمانند.
- تغییر membership وسط سناریو: w + r > n روی دو مجموعه عضو متفاوت هیچ همپوشانی ندارد. نگهبان: quorumهای مشترک — مجموعههای قدیم و جدید تا پایان تغییر باید هم را همپوشانی کنند.
- Reader crashes after observing v2 but before write-back: if the caller never got a response, the read never completed — no precedence edge, and the next reader's write-back still propagates v2. If it returned v2 then died mid-write-back, the completed read DID set precedence, and the duty passes to the next reader, which writes back whatever max it meets. Completed means the response reached the caller, not that the reader saw the value internally.
- Concurrent writers without a query phase: two writers stamping v2 independently fork the value — same version, different data. Guard: query the highest timestamp from a quorum first (ABD multi-writer), so versions stay unique per writer.
- Membership change mid-scenario: w + r > n computed over two different member sets overlaps nothing. Guard: joint quorums — old and new member sets must overlap each other until the change completes.
کد قابل اجرا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: versions on 3 replicas, an in-flight write on n1 only,
// new-then-old r=2 reads, then the ABD fix: write-back before return.
public class QuorumDemo {
record Ver(long v, String val) {}
public static void main(String[] a) {
Map<String, Ver> store = new LinkedHashMap<>(Map.of(
"n1", new Ver(2, "new"), "n2", new Ver(1, "old"), "n3", new Ver(1, "old")));
Ver r1 = maxOf(store, List.of("n1", "n2"));
Ver r2 = maxOf(store, List.of("n2", "n3"));
System.out.println("read1 {n1,n2} -> v" + r1.v() + "=" + r1.val());
System.out.println("read2 {n2,n3} -> v" + r2.v() + "=" + r2.val());
System.out.println("violation (new-then-old) = " + (r1.v() > r2.v()));
store.put("n2", r1); // write-back of v2 to a quorum BEFORE return
Ver r2b = maxOf(store, List.of("n2", "n3"));
System.out.println("after write-back, read2 -> v" + r2b.v() + "=" + r2b.val());
if (!(r1.v() == 2 && r2.v() == 1 && r2b.v() == 2)) throw new AssertionError();
System.out.println("OK: versions ordered, write-back-before-return fixes it");
}
static Ver maxOf(Map<String, Ver> s, List<String> q) {
return q.stream().map(s::get).max(Comparator.comparingLong(Ver::v)).orElseThrow();
}
}
اجرا: javac QuorumDemo.java && java QuorumDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: خواندن اول v2=new و دومی v1=old (نقض true) و بعد از write-back دومی v2=new؛ OK. (خروجی واقعی همین محیط)
Run: javac QuorumDemo.java && java QuorumDemo — stdlib Java only, instant.
Expected output: read1 returns v2=new, read2 returns v1=old (violation true), after write-back read2 returns v2=new; OK. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (استف): اگر یک reader بعد از خواندن جدید و قبل از write-back بمیرد، کدام read باید completed حساب شود؟Follow-up (Staff): A reader crashes after observing the new value but before write-back. Should that read count as completed?
اگر reader پیش از write-back و پیش از پاسخ بمیرد، read کامل نشده است و میتوان آن را در history کاملشده حذف کرد؛ read بعدی که مقدار قدیم میدهد لزوماً نقض نیست. اگر پاسخ v2 قبلاً به caller رسیده باشد، read کامل است و بازگشت v1 در read بعدی نقض است. انتقال وظیفه به خوانندهٔ بعد تضمین نمیدهد، چون ممکن است اصلاً v2 را نبیند؛ پروتکل درست پاسخ را تا پایان write-back quorum نگه میدارد.
If the reader crashes before write-back and before replying, the read is pending and may be omitted from a completed history; a later old read is not necessarily a violation. If v2 already reached the caller, the completed read constrains later reads and returning v1 violates linearizability. Passing repair duty to the next reader is insufficient: it may never see v2. A correct protocol withholds the reply until quorum write-back completes.
پیگیری (سینیور): چرا <span class="ltr">w + r > n</span> بهتنهایی خواندن تازه را تضمین نمیکند؟Follow-up (Senior): Why doesn't w + r > n alone guarantee fresh reads?
چون حساب روی مجموعههاست و تازگی سه پیشفرض دیگر میخواهد: نوشتن کامل شده باشد (هر w ذخیره، نه یکی)، نسخهها مرتب باشند تا خواننده مقایسه کند، و بیشینه قبل از برگشتن خواندن برگردد. همپوشانی quorum بدون این پیشفرضها پلی است که شاید بار کهنه ببرد — همان مجموعهها و همان جمع با بار کهنه.
Because it is arithmetic over sets, and freshness needs three more premises: the write completed (all w stored, not one), versions are ordered so readers can compare, and the max is written back before the read returns. Quorum overlap without those premises is a bridge that may carry the old value — same sets, same sum, stale cargo.
اشتباهات رایجCommon mistakes
- نامیدن w=3 هدف با w=3 تأییدشده. ارسال به سه مساوی ذخیره روی سه نیست؛ فقط ACKها حساباند و اینجا یکی است.
- ادعای خطیپذیر شدن خواندن با read-repair پسزمینه. تعمیر بعد از بازگشت آرایش تاریخ است — صداکننده نقض را قبلاً دیده. بگو «تعمیر eventual» و هرگز «خطیپذیر».
- مقایسه نسخهها با timestamp ساعت دیواری (LWW). ساعتهای دیواری skew دارند؛ مهرهای کج مقدارها را غلط و بیصدا مرتب میکنند. نسخههای اینجا منطقیاند و با ساخت مرتباند.
- Calling the targeted w=3 an acknowledged w=3. Sent-to-three is not stored-on-three; only ACKs count, and here there is one.
- Claiming background read-repair makes reads linearizable. Repair after the return is history cosmetics — the caller already saw the violation. Say 'eventual repair', never 'linearizable'.
- Comparing versions by wall-clock timestamps (LWW). Wall clocks skew; skewed stamps order values wrongly and silently. Versions here are logical and ordered by construction.
منابع:Sources: Attiya, Bar-Noy, Dolev · atomic registers · 1995 · Jepsen · consistency models
33 تمرین ۳ — مولدِ شناسهی oracle با failoverExercise 3 — an oracle ID generator with failover
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- یک allocator oracle دستههای ۵تایی (اندازه آموزشی) تحویل میدهد؛ high-water mark بادوام و اشارهگر تحویل ناپایدار است.
- crash همه حالت ناپایدار را میبرد و هیچ بادوامی را نه؛ standby (اگر باشد) از همان mark بادوام شروع میکند.
- یکتایی و بدون-gap و ترتیب تخصیص و ترتیب مشاهده real-time چهار تضمین جدا هستند — قبل از انتخاب بگو محصول کدام را میخواهد.
- One oracle allocator serves batches of 5 (teaching size); the high-water mark is durable, the handout pointer is volatile.
- Crash loses all volatile state and nothing durable; the standby (if any) starts from the same durable mark.
- Uniqueness, gap-free, allocation order and real-time observation order are four separate guarantees — define which the product needs before choosing.
چرا این روش و این عددها؟Why this method and these numbers?
پایدارکردن high-water mark یعنی همهٔ شناسههای زیر آن رزروشدهاند، حتی اگر هنوز تحویل نشده باشند؛ restart بعد از آن، تکرار را به gap تبدیل میکند. هزینهٔ write بادوام با دستهٔ b تقریباً بر b شناسه تقسیم میشود ولی تا b−1 شناسه در crash میتواند جا بیفتد. b=5 برای دیدن دستی این بدهبستان است. یکتایی با بازهٔ جدا قابل شارد است؛ ترتیب پاسخ جهانی نیازمند کنترل مسیر صدور است و از fence روی ذخیرهٔ mark بهتنهایی نتیجه نمیشود.
Persisted high-water marks reserve every ID below them, including unissued ones; restarting above the mark exchanges duplication for gaps. A batch of b amortizes a durable write across b IDs but can lose up to b−1 unissued IDs on crash. Five makes the trade-off inspectable. Disjoint ranges support sharded uniqueness; global response order needs issuance-path control beyond fencing mark storage.
مدل ذهنیMental model
حل گامبهگامStep by step
- اول mark بعد تحویل رزرو را قبل از تحویل پایدار کن: دسته ۱ تا ۵ را بادوام رزرو کن بعد ۱و۲و۳ را از RAM تحویل بده. ترتیب mark-first کل ترفند است.
- کشتن وسط دسته وسط دسته crash کن و از mark بالا بیا: ۴و۵ هرگز صادر نشدند پس صدورهای بعد ۶و۷ هستند. gap میآید و تکراری هرگز — issued=[1,2,3,6,7].
- جدا کردن تضمینها چهار قرارداد را جدا کن: یکتایی، gap-free، ترتیب تخصیص و ترتیب real-time پاسخها. طرح دستهای همین پاسخ عمداً gap دارد؛ بازههای جدا فقط یکتایی را میدهند. برای ترتیب جهانی، همهٔ مسیرهای تحویل باید پروتکل ترتیب واحد را رعایت کنند.
- fence کردن failover standby همهٔ بازههای رزروشده را از mark بادوام رد میکند و فقط بازهٔ تازه میگیرد؛ epoch قدیمی در reserve رد میشود. بازهٔ قبلی را دوباره واگذار نکن. رهبر قدیمی ممکن است از RAM شناسهٔ یکتا ولی کوچکتر بدهد؛ اگر ترتیب جهانی لازم است، مسیر پاسخ هم باید مالک فعلی را بررسی کند.
- Mark first, serve second Persist the reservation before serving: reserve batch 1..5 durably, then hand out 1,2,3 from RAM. The mark-first order is the whole trick.
- Crash the batch Crash mid-batch and recover from the mark: 4,5 were never issued, so next issues are 6,7. Gaps appear, duplicates never do — issued=[1,2,3,6,7].
- Split the guarantees Separate uniqueness, gap-free allocation, allocation order and real-time response order. This batched design deliberately allows gaps; disjoint ranges provide uniqueness. Global ordering requires every issuance path to follow one ordering protocol.
- Fence the failover The standby skips all durably reserved ranges and reserves new ones under an epoch fence. Never reassign an old range. An old leader may still issue unique but smaller RAM IDs; if global order is required, validate ownership on the response path too.
طراحی مشخصConcrete design
اجزا: یک allocator (اشارهگر تحویل ناپایدار + کش دسته)، یک فروشگاه بادوام برای high-water mark و یک standby که لاگ mark را بازپخش میکند. API: reserve(batch) اول mark را پایدار میکند و هنوز هیچچیز قابل تحویل برنمیگرداند؛ nextID() از RAM تحویل میدهد. مسیر خواندن: تحویل از حافظه بدون I/O. مسیر نوشتن: اول mark بعد تحویل — crash میانشان فقط شناسه جا میاندازد. کلید پارتیشن: هیچی — oracle یک تصمیمگیر منطقی است؛ با دستهبندی مقیاس بده نه با تکه کردن mark.
Components: one allocator (volatile handout pointer + batch cache), one durable store for the high-water mark, one standby replaying the mark log. API: reserve(batch) persists the mark and returns nothing servable yet; nextID() serves from RAM. Read path: serve from memory, no I/O. Write path: mark-first, then serve — a crash between them only skips IDs. Partition key: none — the oracle is one logical decider; scale it by batching, not by splitting the mark.
مثال محاسبهشدهWorked example
اجرای آموزشی: دسته ۱ تا ۵ رزرو شد (hwm=5)؛ صدور ۱و۲و۳؛ crash؛ بازیابی از hwm=5؛ صدور ۶و۷. issued=[1,2,3,6,7]: gapها ۴و۵ و تکراری هیچی. دموی شارد: A صادر میکند A-1 و A-2 و B صادر میکند B-1 و B-2 — هر چهار یکتا ولی A-2 ممکن است در real-time قبل از B-1 باشد.
Teaching run: batch 1..5 reserved (hwm=5); issues 1,2,3; crash; recover from hwm=5; issues 6,7. Issued=[1,2,3,6,7]: gaps {4,5}, duplicates none. Shard demo: A issues A-1,A-2 while B issues B-1,B-2 — all four unique, yet A-2 may predate B-1 in real time.
دانلود نمودار مستقلDownload standalone diagram
- درخواست بعد رزرو: mark دسته ۱ تا ۵ قبل از قابلتحویل شدن شناسه ۱ پایدار میشود.
- تحویل در RAM: ۱و۲و۳ بیرون میروند و ۴و۵ هرگز — crash وسط دسته مینشیند.
- بازیابی از mark و fence کردن epoch قدیم با token بادوام، بعد راستیآزمایی: ۱و۲و۳و۶و۷ — gap مجاز و تکراری هرگز.
- Request, then reserve: the batch mark 1..5 is persisted before ID 1 is servable.
- Hand out in RAM: 1,2,3 leave; 4,5 never do — the crash lands mid-batch.
- Recover from the mark, fence the old epoch with a durable token, then verify: 1,2,3,6,7 — gaps allowed, duplicates never.
نقطهٔ تصمیم: نقطه تصمیم گره رزرو است: mark قبل از قابلتحویل شدن هرچیز بادوام است. هر پیکان بعدش اجازه crash دارد.
رد خرابی: گره crash را دنبال کن: ۴و۵ هرگز بیرون نرفتند ولی mark قبلاً پوشششان داده — gap قیمت صادقانه mark-first است. بعد failover بدون fence را دنبال کن: allocator زندهشده ۱و۲و۳ را کنار ۱و۲و۳ standby دوباره صادر میکند — تکراری، همان یک نتیجهای که طرح ممنوع میکند.
تضمین نشاندادهشده: شکل خط لوله mark-first و failover fenced را نشان میدهد. گزینههای UUID و بازه-شاردی را بهعنوان شاخه نشان نمیدهد — اسمبلی taxonomy هر سه را مقایسه میکند.
تضمین نشانندادهشده: ترتیب real-time سراسری میان شاردها را هیچچیز اینجا وعده نمیدهد — taxonomy آن خانه را صادقانه علامت میزند.
Decision point: The decision point is the reserve node: the mark is durable before anything is servable. Every arrow after it is allowed to crash.
Failure trace: Trace the crash node: 4 and 5 never left, yet the mark already covers them — the gap is the honest price of mark-first. Then trace an unfenced failover: a revived allocator re-issuing 1,2,3 beside the standby's 1,2,3 — duplicates, the one outcome the design forbids.
Guarantee shown: The figure shows the mark-first pipeline and the fenced failover. It does not show UUID or shard-range alternatives as branches — the taxonomy assembly compares all three.
Not shown: Real-time global ordering across shards is not promised by anything here — the taxonomy marks that cell honestly.
خودآزماییSelf-check
چرا mark باید قبل از بیرون رفتن اولین شناسه پایدار شود نه بعدش؟
پاسخ: پایدار کردن بعد از صدور میگذارد crash رد شناسههای تحویلشده را گم کند پس بازیابی دوباره صادرشان میکند: تکراری. mark-first ریسک را برعکس میکند: crash فقط میتواند شناسههای هرگزبیروننرفته را جا بیندازد که gap است، هرگز تکراری.
Why must the mark persist before the first ID leaves, not after?
Answer: Persisting after issuing lets a crash lose track of handed-out IDs, so recovery re-issues them: duplicates. Mark-first inverts the risk: a crash can only skip IDs that never left, which is a gap, never a duplicate.
دانلود SVG مستقلDownload standalone SVG
- سطر یکتایی: سراسر سبز — oracle و UUID و بازههای جدا همه تضمینش میکنند. shard کردن یکتایی هرگز مسئله نبود.
- بدون-gap و ترتیب تخصیص: فقط تصمیمگیر واحد داردشان؛ UUID و بازههای شارد صورتی میشوند.
- ترتیب real-time: فقط oracle fenced زرد است (مشروط) و بقیه صورتی. ترتیب هماهنگی میخواهد — همیشه.
- Uniqueness row: green across — oracle, UUID and disjoint ranges all guarantee it. Sharding uniqueness was never the problem.
- Gap-free and allocation order: only the single decider holds them; UUID and shard ranges go rose.
- Real-time order: only the fenced oracle is yellow (conditional); the rest rose. Ordering needs coordination — always.
نقطهٔ تصمیم: نقطه تصمیم ستونی است که محصول واقعاً میخواهد: یک ستون را پایین بخوان و فقط همان را بپرداز. بیشتر محصولها فقط سطر اول را میخواهند.
رد خرابی: ادعای «oracle هرگز shard نمیشود» را روی سطر یکتایی دنبال کن: هر سه خانه سبزند. ادعا همانجا میمیرد. بعد نیاز مرتبسازی real-time را روی سطر پایین دنبال کن: فقط خانه oracle fenced زرد است و بقیه صورتی — shard کردن یکتایی هرگز ترتیب نخرید.
تضمین نشاندادهشده: شکل سه طرح را روی چهار تضمین در یک جدول مقایسه میکند. نه اندازه دسته را تعیین میکند نه timeout failover را — آنها اعداد استقرارند نه تضمین.
تضمین نشانندادهشده: طرحهای همگامسازی ساعت (سبک TrueTime) بیرون این جدولاند — آنها ترتیب real-time را با سختافزار میخرند، قیمتی دیگر.
Decision point: The decision point is the column the product actually needs: read down one column and pay only for it. Most products need the first row only.
Failure trace: Trace the 'oracle can never shard' claim across the uniqueness row: all three cells are green. The claim dies right there. Then trace a real-time sorting requirement down the bottom row: only the fenced oracle cell is yellow, the rest rose — sharding uniqueness never bought ordering.
Guarantee shown: The figure compares the three schemes on four guarantees in one table. It does not size batches or set failover timeouts — those are deployment numbers, not guarantees.
Not shown: Clock-synchronization schemes (TrueTime-style) are outside this table — they buy real-time order with hardware, a different price.
خودآزماییSelf-check
آیا بازههای جدای شارد ترتیب real-time سراسری میدهند؟
پاسخ: نه — فقط یکتایی. A-2 ممکن است در زمان دیواری قبل از B-100 صادر شود ولی کوچکتر مرتب شود؛ بازههای جدا حداکثر ترتیب per-shard میدهند. ترتیب real-time سراسری ستون oracle fenced را میخواهد.
Do disjoint shard ranges give global real-time order?
Answer: No — only uniqueness. A-2 may be issued before B-100 in wall-clock time yet sort smaller; disjoint ranges give per-shard order at most. Global real-time order needs the fenced oracle column.
گزینههای جایگزینAlternatives
- UUID تصادفی (بدون هماهنگی): یکتایی بدون هیچ allocator یا داستان failover، به قیمت هیچ ترتیبی و کلیدهای پهنتر. وقتی ترتیب هرگز خوانده نمیشود انتخابش کن — شناسه درخواست و span ردیابی — هرگز وقتی چیزی با شناسه مرتب میشود.
- شناسه پیشوند-شاردی (A-* و B-* از بازههای جدا): یکتا و بدون هماهنگی با ترتیب per-shard، ولی بدون ترتیب real-time سراسری. وقتی شاردها مالک سطرهایشاناند و مرتبسازی میانشاردی فقط نمایشی است انتخابش کن.
- UUIDs (random, no coordination): uniqueness without any allocator or failover story, at the cost of no order at all and wider keys. Choose when order is never read — request IDs, tracing spans — never when something sorts by ID.
- Shard-prefixed IDs (A-*, B-* from disjoint ranges): unique and coordination-free with per-shard order, but no global real-time order. Choose when shards own their rows and cross-shard sorting is display-only.
سناریوهای خرابیFailure scenarios
- failover بدون fencing: allocator قدیم زنده میشود و ۱و۲و۳ را کنار ۱و۲و۳ standby دوباره صادر میکند — تکراری، همان یک نتیجه ممنوع. نگهبان: standby یک token epoch تازه میگیرد و storage epoch کوچکتر را بادوام رد میکند — قاعده fencing فصل ۹، اعمالشده روی epochهای allocator نه tokenهای per-write.
- پایدار کردن mark بعد از تحویل: crash میان تحویل و پایداری رد شناسههای صادرشده را گم میکند — بازیابی دوباره صادرشان میکند. نگهبان: ترتیب mark-first در کد که با تست کشتن وسط دسته assert میشود (همان دمو).
- دو allocator روی بازههای همپوشان (shardبندی غلط): جدایی فرض شده ولی هرگز اعمالنشده میان شاردها تکراری میدهد. نگهبان: بازهها از یک نسخه کانفیگ تخصیص یابند و هر صادرکننده پیشوند شاردش را مهر بزند تا همپوشانی قابل تشخیص باشد نه بیصدا.
- Failover without fencing: the old allocator revives and re-issues 1,2,3 beside the standby's 1,2,3 — duplicates, the one forbidden outcome. Guard: the standby takes a fresh epoch token and storage durably rejects the smaller epoch — the fencing rule from chapter 9, applied to allocator epochs, not per-write tokens.
- Mark persisted after serving: a crash between serving and persisting loses track of issued IDs — recovery re-issues them. Guard: mark-first ordering in code, asserted by a test that kills mid-batch (the demo).
- Two allocators on overlapping ranges (mis-sharding): disjointness assumed but never enforced gives duplicates across shards. Guard: ranges assigned from one config version, and each issuer stamps its shard prefix so overlap is detectable, not silent.
کد قابل اجرا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: oracle allocator. Persist the batch high-water mark
// BEFORE issuing; crash mid-batch loses only the unissued tail.
public class OracleDemo {
static class Allocator {
long persistedHwm = 0, next = 1;
final long batch = 5;
final List<Long> issued = new ArrayList<>();
void reserve() { persistedHwm += batch; }
long issue() {
if (next > persistedHwm) reserve();
issued.add(next);
return next++;
}
void crash() { next = persistedHwm + 1; } // volatile lost, mark survives
}
public static void main(String[] a) {
Allocator al = new Allocator();
System.out.println("issue " + al.issue() + ", " + al.issue() + ", " + al.issue());
al.crash();
System.out.println("CRASH -> recover from hwm=" + al.persistedHwm);
System.out.println("next: " + al.issue() + ", " + al.issue());
Set<Long> uniq = new HashSet<>(al.issued);
System.out.println("issued=" + al.issued + " dups=" + (uniq.size() != al.issued.size()));
if (uniq.size() != al.issued.size()) throw new AssertionError("duplicate ID!");
System.out.println("OK: no duplicates, gaps allowed");
}
}
اجرا: javac OracleDemo.java && java OracleDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: صدور ۱و۲و۳ بعد crash و بازیابی از hwm=5 و صدور ۶و۷؛ issued=[1,2,3,6,7] بدون تکراری؛ OK. (خروجی واقعی همین محیط)
Run: javac OracleDemo.java && java OracleDemo — stdlib Java only, instant.
Expected output: issues 1,2,3 then crash, recovery from hwm=5, next issues 6,7; issued=[1,2,3,6,7], no duplicates; OK. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): دو node بازههای جدا دارند؛ آیا IDهای صادرشده به ترتیب زمان جهانی افزایش مییابند؟Follow-up (Senior): Two nodes issue IDs from disjoint ranges. Do the IDs increase in global real-time order?
نه. بازههای جدا فقط یکتایی و ترتیب per-shard را تضمین میکنند. A-2 میتواند ظهر صادر شود و B-100 ساعت ۱۱:۵۹ ولی بعدش مرتب شود — ترتیب صدور دیواری و ترتیب عددی میان شاردها ربطی ندارند. اگر محصول سراسری با شناسه در real-time مرتب میکند، oracle fenced میخواهد نه بازه.
No. Disjoint ranges guarantee uniqueness and per-shard order only. A-2 can be issued at noon and B-100 at 11:59 yet sort after it — wall-clock issue order and numeric order are unrelated across shards. If the product sorts globally by ID in real time, it needs the fenced oracle, not ranges.
پیگیری (استف): رهبر oracle وسط دسته failover میکند؛ صدور دوباره را چطور ممنوع میکنی؟Follow-up (Staff): The oracle leader fails over mid-batch. How do you avoid double-issue?
standby همهٔ بازههای رزروشده را از mark بادوام رد میکند و فقط بازهٔ تازه میگیرد؛ epoch قدیمی در reserve رد میشود. بازهٔ قبلی را دوباره واگذار نکن. رهبر قدیمی ممکن است از RAM شناسهٔ یکتا ولی کوچکتر بدهد؛ اگر ترتیب جهانی لازم است، مسیر پاسخ هم باید مالک فعلی را بررسی کند.
The standby skips all durably reserved ranges and reserves new ones under an epoch fence. Never reassign an old range. An old leader may still issue unique but smaller RAM IDs; if global order is required, validate ownership on the response path too.
اشتباهات رایجCommon mistakes
- «این oracle هرگز shard نمیشود.» غلطِ per-guarantee: یکتایی با بازههای جدا آزادانه shard میشود؛ فقط بدون-gap و ترتیب real-time سراسری مقاومت میکنند. بگو کدام تضمین، یا هیچی نگو.
- پایدار کردن mark بعد از تحویل («برای سرعت نوشتنها را بچ کن»). crash میانشان شناسهها را دوباره صادر میکند — همان یک نتیجه ممنوع خریدهشده به قیمت یک fsync کمتر. اول mark، همیشه.
- fencing per-ID بهجای per-epoch allocator. token باید epoch lease allocator را مشخص کند (یک بررسی fencing per-failover) نه هر شناسه صادرشده را — token per-ID به هر صدور یک بررسی بدون امنیت اضافه تحمیل میکند.
- 'This oracle can never be sharded.' Wrong per guarantee: uniqueness shards freely with disjoint ranges; only gap-free and global real-time order resist sharding. Say which guarantee, or say nothing.
- Persisting the mark after serving ('batch the writes for speed'). A crash in between re-issues IDs — the one forbidden outcome bought for one fewer fsync. Mark first, always.
- Fencing per ID instead of per allocator epoch. Tokens must identify the allocator's lease epoch (one fencing check per failover), not each issued ID — per-ID tokens add a check to every issue for no extra safety.
منابع:Sources: Amazon · SDE III interview preparation · Atlassian · engineering interview handbook
44 تمرین ۴ — خطیسنجِ سادهExercise 4 — a mini linearizability checker
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- تک رجیستر با historyهای کوچک (۲-۳ عملیات)؛ جستوجوی کراندار جایگشتها را میشمرد — فقط اندازههای آموزشی.
- مهرهای invoke/return از ساعت harness میآیند؛ تقدم فقط از تمامشده-قبل-شروع استفاده میکند، هرگز مقایسه مهر.
- عملیاتهای معلق (بدون مهر بازگشت، مثل timeoutها) تا هر دو شاخه امتحان نشوند نامعلوماند.
- Single register, small histories (2-3 ops); bounded search enumerates permutations — teaching sizes only.
- Invoke/return stamps come from the harness clock; precedence uses done-before-starts only, never stamp comparison.
- Pending operations (no return stamp, e.g. timeouts) are indeterminate until branched both ways.
چرا این روش و این عددها؟Why this method and these numbers?
برای history کوچک، پیمایش ترتیبها روشی قابلفهم برای یافتن یک اجرای ترتیبی سازگار است؛ n عملیات در بدترین حالت n! ترتیب دارند، پس روش برای history بلند مناسب نیست. یال real-time فقط وقتی A کامل شود و بعد B شروع شود ایجاد میشود. pending operation را نمیتوان خودکار failed گرفت چون ممکن است اثرش ثبت شده باشد. ساعت harness برای نسبت شروع/پایان است؛ timestamp خود داده، نقطهٔ linearization را تعیین نمیکند.
Enumerating orders makes a compatible sequential execution understandable for small histories. Worst-case n! permutations explain why it does not scale. Add a real-time edge only when A returns before B starts. A pending operation may have committed, so do not automatically discard it as failed. Harness time orders invocation/return; stored timestamps do not establish linearization points.
مدل ذهنیMental model
حل گامبهگامStep by step
- اول مهر بعد قید به هر عملیات از harness مهر invoke/return بزن. قاعده تقدم: اگر عملیات X قبل از شروع Y برگشت، X باید در هر ترتیب نامزد قبل Y باشد — تنها قید سخت.
- شمردن نگهدارندهها ترتیبهای نامزد نگهدارنده تقدم را بشمر؛ عملیاتهای همپوشان آزاد جابهجا میشوند. h1 یک ترتیب میپذیرد (نوشتن و خواندن) و h3 هر دو را — آزادی نه شکست.
- قضاوت هر ترتیب قاعده رجیستر را per-order اعمال کن: برو و تازهترین نوشتن را نگه دار و هر خواندن باید بخورد. تنها ترتیب h2 عدد ۱ میخواهد ولی صفر دارد — رد. یک ترتیب بازمانده history را قبول میکند.
- دوشاخه کردن نامعلوم عملیاتهای معلق را هر دو طرف دوشاخه کن: نوشتن timeoutخورده نامعلوم است — با معتبر شدن هر شاخه اثرکرد یا هرگز-نکرد قبول کن. بعد محدودیت را بگو: رأی فقط همین پنجره را مقید میکند.
- Stamp, then constrain Stamp every op with invoke/return from the harness. Precedence rule: if op X returned before op Y started, X must precede Y in every candidate order — the only hard constraint.
- Enumerate keepers Enumerate candidate orders that keep precedence; overlapping operations permute freely. h1 admits one order (write, read); h3 admits both — freedom, not failure.
- Judge each order Apply the register rule per order: walk it, track the latest write, each read must match. h2's only order demands 1 but holds 0 — reject. One surviving order accepts the history.
- Fork the unknown Fork pending operations both ways: a timed-out write is indeterminate — accept if either the took-effect or the never-did branch validates. Then state the limit: the verdict binds this window only.
طراحی مشخصConcrete design
harness: پوششی دور مشتری key-value که per-op مهر invoke/return میزند (ساعت monotonic برای مدتها و ساعت دیواری برای لاگ). خطیسنج: گراف تقدم از یالهای تمامشده-قبل-شروع، بعد جستوجوی جایگشت کراندار با قاعده رجیستر per-order و عملیاتهای معلق دوشاخه. مسیر خواندن: جمع یک پنجره history. مسیر نوشتن: هیچی — خطیسنج هرگز نمینویسد، قضاوت میکند.
Harness: a wrapper around the key-value client stamping invoke/return per op (monotonic clock for durations, wall clock for the log). Checker: precedence graph from done-before-starts edges, then bounded permutation search with the register rule per order, pending ops forked both ways. Read path: collect a history window. Write path: none — the checker never writes, it judges.
مثال محاسبهشدهWorked example
historyهای آموزشی: h1 مساوی write(1) t0-t2 و read-to-1 t3-t4 — یک ترتیب نگهدارنده تقدم (نوشتن و خواندن) و خواندن میخورد: LINEARIZABLE. h2 مساوی همان میلهها با read-to-0 — تنها ترتیب ۱ میخواهد: NOT LINEARIZABLE. h3 مساوی نوشتن t0-t5 با همپوشانی خواندن t2-t4 به صفر — ترتیب (خواندن و نوشتن) معتبر است: LINEARIZABLE.
Teaching histories: h1 = write(1) t0-t2, read-to-1 t3-t4 — one precedence-keeping order (write, read), read matches: LINEARIZABLE. h2 = same bars, read-to-0 — the only order demands 1: NOT LINEARIZABLE. h3 = write t0-t5 overlapping read t2-t4 to 0 — order (read, write) validates: LINEARIZABLE.
دانلود نمودار مستقلDownload standalone diagram
- ثبت: هر عملیات مهر invoke/return میگیرد؛ timeoutها بدون بازگشت میمانند — نامعلوم.
- تقدم بعد شمارش: تمامشده-قبل باید جلو باشد؛ عملیاتهای همپوشان هر ترتیبی میگیرند. عملیاتهای معلق دوشاخه: اثرکرد و هرگز-نکرد.
- بررسی رجیستر per-order و رد ترتیبهای کهنه-بعد-کامل و رأی روی بازماندهها.
- Record: every op gets invoke/return stamps; timeouts keep no return — indeterminate.
- Precedence, then enumerate: done-before must lead; overlapping ops may take any order. Pending ops fork: took-effect and never-did.
- Register check per order, reject stale-after-complete orders, verdict on the survivors.
نقطهٔ تصمیم: نقطه تصمیم گره بررسی رجیستر است: یک ترتیب معتبر برای قبول کافی است. رد یعنی هر ترتیب نگهدارنده تقدم شکست خورد.
رد خرابی: historyای را دنبال کن که ساعت harness دروغ میگوید (invoke بعد از return مهر خورده): گراف تقدم وارونه میشود و هر رأی بعدش زباله است. خطیسنج فقط بهاندازه مهرهایش صادق است — ساعت خراب، رأی خراب.
تضمین نشاندادهشده: شکل رویه اول-اندازه بعد-جستوجو را با شاخه نامعلوم نشان میدهد. اندازه history را کران نمیکند — کارتها میگویند جستوجوی کراندار فقط historyهای کوچک را میخورد.
تضمین نشانندادهشده: اثبات اجراهای آینده هیچجای اینجا نشان داده نشده — رأی درباره یک پنجره history قضاوت میکند نه بیشتر.
Decision point: The decision point is the register-check node: one valid order is enough to accept. Rejection means every precedence-keeping order failed.
Failure trace: Trace a history where the harness clock lies (invoke stamped after return): the precedence graph inverts and every verdict after it is garbage. The checker is only as honest as its stamps — bad clocks, bad verdicts.
Guarantee shown: The figure shows the measure-then-search procedure with the indeterminate branch. It does not bound history size — the cards say bounded search fits small histories only.
Not shown: Proof of future executions is not shown anywhere here — the verdict judges one history window, nothing more.
خودآزماییSelf-check
عملیاتی که قبل از شروع دیگری تمام شده: در هر ترتیب نامزد چه باید برقرار باشد؟
پاسخ: باید در هر ترتیب نامزد جلو باشد. اگر در ترتیبی عقب بیفتد آن ترتیب کنار میرود — ولی ترتیبهای دیگر هنوز ممکن است history را معتبر کنند. تقدم قید میگذارد و بهتنهایی تبرئه نمیکند.
An op done before another starts: what must hold in every candidate order?
Answer: It must lead in every candidate order. If it trails in some order, that order is discarded — but other orders may still validate the history. Precedence constrains, never acquits by itself.
دانلود SVG مستقلDownload standalone SVG
- h1: نوشتن t0-t2 و خواندن t3-t4 که ۱ برمیگرداند — تنها ترتیب نگهدارنده تقدم خواندن را بعد نوشتن میگذارد: قبول.
- h2: همان میلهها ولی خواندن صفر برمیگرداند — پیکان چین نوشتن-اول را تحمیل میکند و نوشتن-اول ۱ میخواهد: رد.
- h3: نوشتن t0-t5 با همپوشانی خواندن t2-t4 که صفر برمیگرداند — همپوشانی هر دو طرف خطی میشود: قبول.
- h1: write t0-t2, read t3-t4 returning 1 — the only precedence-keeping order has the read after the write: accept.
- h2: same bars, read returns 0 — the dashed arrow forces write-first, and write-first demands 1: reject.
- h3: write t0-t5 overlapping read t2-t4 returning 0 — overlap linearizes either way: accept.
نقطهٔ تصمیم: نقطه تصمیم پیکان چیندار h2 است: تمامشده-قبل-شروع تنها قید سخت است. بقیه جستوجوست.
رد خرابی: پیکان چیندار تقدم h2 را دنبال کن: نوشتن در t2 تمام و خواندن در t3 شروع — ولی صفر برمیگرداند. هر ترتیب نامزد باید نوشتن را اول بگذارد و در هر چنین ترتیبی خواندن باید ۱ ببیند. هیچ ترتیبی زنده نمیماند: رد. حالا پیکان چین را بپوشان و دوباره قضاوت کن: با همپوشانی h2 مثل h3 قبول میشد.
تضمین نشاندادهشده: شکل سه history را با میله و تقدم و رأی نشان میدهد، بهعلاوه جعبه قاعده. خود جستوجوی جایگشت را نشان نمیدهد — گام گردشکار رویه را نشان میدهد.
تضمین نشانندادهشده: اندازه traceهای پروداکشن و راهبرد نمونهبرداری بیرون این شکلاند — جستوجوی کراندار نمیتواند درسته قورتشان دهد.
Decision point: The decision point is h2's dashed arrow: done-before-start is the only hard constraint. Everything else is search.
Failure trace: Trace h2's dashed precedence arrow: write done at t2, read starts at t3 — yet returns 0. Every candidate order must put the write first, and in every such order the read must see 1. No order survives: reject. Now cover the dashed arrow and re-judge: with overlap, h2 would pass like h3.
Guarantee shown: The figure shows three histories with bars, precedence and verdicts, plus the rule box. It does not show the permutation search itself — the workflow step shows the procedure.
Not shown: Production trace sizes and sampling strategy are outside this figure — bounded search cannot eat them whole.
خودآزماییSelf-check
همان write(1) و read-to-0: کی خوب است و کی نقض؟
پاسخ: بله — اگر همپوشان باشند صفر خطیسازی قانونی است (خواندن قبل نوشتن گذاشته میشود). اگر نوشتن اول کامل شده باشد صفر نقض است. همان مقدارها با رأیهای مخالف: تقدم همهچیز است.
Same write(1) and read-to-0: when is it fine and when a violation?
Answer: Yes — if they overlap, 0 is a legal linearization (read placed before the write). If the write completed first, 0 is a violation. Same values, opposite verdicts: precedence is everything.
گزینههای جایگزینAlternatives
- بررسی مدل پیادهسازی (جستوجوی فراگیر پروتکل به سبک TLA+): بهجای نمونهبرداری از اجراها، طرح را برای اندازههای کراندار اثبات میکند. قویتر از خطیسنج history ولی سنگینتر برای نوشتن — برای خود پروتکل replication استفادهاش کن و خطیسنج history را برای سیستم در حال اجرا نگه دار.
- Model checking the implementation (TLA+-style exhaustive search of the protocol): proves the design for bounded sizes instead of sampling executions. Stronger than a history checker, heavier to write — use it for the replication protocol itself, and keep the history checker for the running system.
سناریوهای خرابیFailure scenarios
- ساعت دروغگوی harness (invoke بعد از return مهر خورده): تقدم وارونه میشود و رأیها زبالهاند. نگهبان: ساعت monotonic برای مدتها و assert سلامت invoke <= return روی هر عملیات و انداختن پرسروصدای پنجرههای مردود.
- نوشتن معلقِ شکستخورده فرضشده: نوشتن timeoutخورده شاید بعداً اثر کند — شکست نامیدنش باعث میشود خواندن بعدی مقدارش شبح بهنظر برسد. نگهبان: مدلسازی نامعلوم با هر دو شاخه، همیشه.
- پنجره history کوچکتر از شکار مسابقه (نمونهبرداری فقط ساعتهای خلوت): خطیسنج قبول میدهد و اطمینان دروغین است. نگهبان: نمونهبرداری روی اوج و استقرارها و گفتن اینکه رأی پنجره را پوشش میدهد — هرگز سیستم را نه.
- Lying harness clock (invoke stamped after return): precedence inverts and verdicts are garbage. Guard: monotonic clock for durations, sanity-assert invoke <= return on every op, drop windows that fail it loudly.
- Pending write treated as failed: a timed-out write may still take effect later — calling it failed lets a later read of its value look like a phantom. Guard: indeterminate modeling, both branches, always.
- History window too small to catch the race (sampling only quiet hours): the checker passes and confidence is false. Guard: sample across peaks and deploys, and say the verdict covers the window — never the system.
کد قابل اجرا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: mini linearizability checker for one register.
// Try every order that keeps real-time precedence; each read must see
// the latest preceding write. Pending/timeout ops branch both ways.
public class CheckerDemo {
record Op(String name, char kind, String val, int start, int end) {}
public static void main(String[] a) {
List<Op> h1 = List.of(new Op("w", 'w', "1", 0, 2), new Op("r", 'r', "1", 3, 4));
List<Op> h2 = List.of(new Op("w", 'w', "1", 0, 2), new Op("r", 'r', "0", 3, 4));
List<Op> h3 = List.of(new Op("w", 'w', "1", 0, 5), new Op("r", 'r', "0", 2, 4));
System.out.println("h1 completed-write, new read -> " + check(h1));
System.out.println("h2 completed-write, stale read -> " + check(h2));
System.out.println("h3 overlapping read -> " + check(h3));
if (!check(h1).equals("LINEARIZABLE") || !check(h2).equals("NOT LINEARIZABLE")
|| !check(h3).equals("LINEARIZABLE")) throw new AssertionError();
System.out.println("OK: precedence enforced, stale-after-complete rejected");
}
static String check(List<Op> ops) {
for (List<Op> order : permutations(new ArrayList<>(ops))) {
if (!respectsRealtime(order)) continue;
if (validRegister(order)) return "LINEARIZABLE";
}
return "NOT LINEARIZABLE";
}
static boolean respectsRealtime(List<Op> order) {
Map<String, Integer> pos = new HashMap<>();
for (int i = 0; i < order.size(); i++) pos.put(order.get(i).name(), i);
for (Op x : order) for (Op y : order)
if (x.end() != -1 && x.end() < y.start() && pos.get(x.name()) > pos.get(y.name())) return false;
return true;
}
static boolean validRegister(List<Op> order) {
String cur = "0";
for (Op o : order) {
if (o.kind() == 'w') cur = o.val();
else if (!o.val().equals(cur)) return false;
}
return true;
}
static List<List<Op>> permutations(List<Op> in) {
List<List<Op>> out = new ArrayList<>();
perm(in, 0, out);
return out;
}
static void perm(List<Op> l, int k, List<List<Op>> out) {
if (k == l.size()) { out.add(new ArrayList<>(l)); return; }
for (int i = k; i < l.size(); i++) { Collections.swap(l, i, k); perm(l, k + 1, out); Collections.swap(l, i, k); }
}
}
اجرا: javac CheckerDemo.java && java CheckerDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: h1 LINEARIZABLE و h2 NOT LINEARIZABLE و h3 LINEARIZABLE؛ OK. (خروجی واقعی همین محیط)
Run: javac CheckerDemo.java && java CheckerDemo — stdlib Java only, instant.
Expected output: h1 LINEARIZABLE, h2 NOT LINEARIZABLE, h3 LINEARIZABLE; OK. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (استف): timeout یک write را failed حساب میکنی یا indeterminate؟ چرا؟Follow-up (Staff): Do you model a timed-out write as failed or indeterminate, and why?
نامعلوم، همیشه. timeout یعنی نتیجه نامشخص است: درخواست یا نوشتن یا پاسخ هرکدام ممکن است گم شده باشند — همان ابهام کلاسیک. خطیسنج باید هر دو شاخه را امتحان کند (اثرکرد / هرگز-نکرد) و با معتبر شدن هرکدام قبول دهد. شکست مدل کردنش، نوشتنِ دیراثر را نقض دروغین میکند؛ شدنی مدل کردنش نقضهای واقعی را پنهان میکند.
Indeterminate, always. A timeout means the outcome is unknown: the request, the write, or the response may each have been lost — the classic ambiguity. The checker must try both branches (took effect / never did) and accept if either validates. Modeling it as failed turns a late-taking-effect write into a false violation; modeling it as done hides real ones.
پیگیری (استف): خطیسنج روی دههزار history نمونه قبول میدهد؛ آیا سیستم خطیپذیر است؟Follow-up (Staff): The checker passes on 10k sampled histories. Is the system linearizable?
نه. دههزار پنجره قبولشده دههزار پنجره را اثبات میکند نه سیستم را: اجرای بعد و استقرار بعد و درهمتنیدگی اوج بار بعد قضاوتنشدهاند. تست متناهی اطمینان را بالا میبرد؛ فقط اثبات پروتکل (بررسی مدل) بهعلاوه کاوش پیوسته پروداکشن (پروب نسخه و مانیتور واگرایی) تضمین را جلو میبرد. بگو خطیسنج چه را پوشش میدهد: پنجرههای نمونهبرداریشده را.
No. Ten thousand passing windows prove ten thousand windows, not the system: the next execution, the next deploy, the next peak-load interleaving is unjudged. Finite testing raises confidence; only protocol proof (model checking) plus continuous production probing (version probes, divergence monitors) carry the guarantee forward. Say what the checker covers: the sampled windows.
اشتباهات رایجCommon mistakes
- گرفتن مقایسه مهر بهجای تقدم («نوشتنِ مهرزودتر باید اول مرتب شود»). تقدم تمامشده-قبل-شروع روی بازههای invoke/return است — مقدار مهر، Lamport یا دیواری، هرگز تعیینش نمیکند.
- قبول با اولین ترتیب قابلقبول بهجای گشتن همه ترتیبهای نگهدارنده تقدم برای رد. یک ترتیب معتبر قبول میکند؛ ولی اینکه هیچی نیست را فقط بعد از امتحان همه میدانی.
- اجرا روی خواندنهای replica و «درست» کردن شکستها با ضعیف کردن historyها (انداختن خواندنهای کهنه). خطیسنج نقض واقعی یافته — فروشگاه را درست کن (write-back و خواندن رهبر)، هرگز مدرک را نه.
- Treating stamp comparison as precedence ('write stamped earlier must order first'). Precedence is done-before-starts on invoke/return intervals — stamp values, Lamport or wall, never decide it.
- Accepting on the first plausible order instead of searching all precedence-keeping orders for rejection. One valid order accepts; but you only know none exists after trying all.
- Running the checker on replica reads and 'fixing' failures by weakening the histories (dropping the stale reads). The checker found a real violation — fix the store (write-back, leader reads), never the evidence.
منابع:Sources: Jepsen · consistency models · Attiya, Bar-Noy, Dolev · atomic registers · 1995