DDIA·مطالعهSTUDY

نقشه راهRoadmap / فصل 6Chapter 6 / حل تمرین‌هاSolutions

حل آموزشی تمرین‌های فصل 6Chapter 6 exercise solutions, explained

هر حل از سؤال مصاحبه شروع می‌کند، فرض‌های آموزشی را روشن می‌گوید، گام‌به‌گام استدلال می‌کند و نمودارهایی دارد که خواندنشان را یاد می‌گیری. هر حل به تمرین خودش برمی‌گردد. Each solution starts from the interview question, states its teaching assumptions, reasons step by step, and shows diagrams you learn to read aloud. Every solution links back to its exercise.

33 حل تشریحیworked solutions· متناظر با بخش تمرین‌های فصل 6Companion to Chapter 6 exercises

11 تمرین ۱ — lag را به دام بیندازExercise 1 — trap the lag

L2 · SeniorL2 · Seniorاولویت اصلیCore priority40 دقیقه بحث40 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
بعد از ویرایش پروفایل، یک read replica مقدار قدیمی برمی‌گرداند. read-your-writes، monotonic reads و causal consistency را با history کوتاه نسخه‌دار نشان بده. routing به primary، session stickiness و version/LSN barrier را مقایسه کن و برای lag بی‌حد fallback تعریف کن.
After a profile edit, a read replica returns the old value. Show read-your-writes, monotonic-read and causal anomalies with short versioned histories. Compare primary routing, session stickiness and version/LSN barriers, and define the fallback for unbounded lag.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

خواندن را با نسخه مسیریابی کن، هرگز با sleep: خواندنِ نوشته خود به leader می‌رود، تکرارها به replicaی پین‌شده یا عبوری از barrier نسخه می‌روند و barrier فقط replicaهای با LSN برابر ۷ یا بیشتر را می‌پذیرد. lag حد بالا ندارد پس fallback صادقانه leader است. قیمتش بار leader و تأخیر خواندن است؛ بعد از مرگ primary کل طرح روی durability تکثیر سوار است.
Route reads by version, never by sleep: own-write reads go to the leader, repeats go to a pinned replica or one past a version barrier, and the barrier admits only replicas at LSN 7 or more. Lag has no upper bound, so the honest fallback is the leader. The price is leader load and read latency; after a primary failure the whole scheme rests on replication durability.

صورت مسئله و فرض‌هاProblem and assumptions

  • فروشگاه تک‌رهبر با تکثیر ناهمزمان؛ هر نوشته LSN یا نسخه‌ای دارد که مسیریاب می‌تواند مقایسه کند.
  • اعداد آموزشی: lag در حالت عادی کسری از ثانیه و زیر فشار ثانیه تا دقیقه است؛ پنجره یک‌دقیقه‌ای leader در ادامه illustrative است، هرگز تضمین نیست.
  • نصب و اندازه‌گیری replica مال آزمایشگاه تکمیلی است؛ بحث مصاحبه فرض می‌کند یک leader و دو follower هست.
  • Single-leader store with asynchronous replication; every write carries a leader LSN or version the router can compare.
  • Teaching numbers: lag is fractions of a second normally, seconds to minutes under stress; the one-minute leader window below is illustrative, never a guarantee.
  • Replica installation and measurement belong to the extended lab; the interview discussion assumes one leader and two followers exist.

چرا این روش و این عددها؟Why this method and these numbers?

پنجرهٔ یک‌دقیقه‌ای خواندن از leader یک heuristic است؛ replica ممکن است بیش از یک دقیقه عقب باشد. token آخرین write را مقایسه می‌کنیم چون applied_version ≥ required_version مستقیماً نشان می‌دهد همان write به replica رسیده است. اگر پس از سه ثانیه صبر به leader برگردیم، سه ثانیه فقط بودجهٔ انتظار تجربهٔ کاربر است و تضمین رسیدن replica نیست. بودجه باید از deadline کل درخواست کم شود؛ هدف freshness نیز با latency پاسخ فرق دارد.

A one-minute leader-read window is a heuristic: lag can exceed it. Compare applied_version ≥ required_version because it directly proves that replica received the required write. Falling back after three seconds uses a user waiting budget, not a replication guarantee. Derive it from the remaining request deadline and distinguish freshness from response latency.

مدل ذهنیMental model

lag را سفر در زمان ببین: هر follower کمی در گذشته زندگی می‌کند و گذشته فاصله ثابت ندارد. شماره نسخه بلیت سوار شدن است — مسیریاب بلیت را چک می‌کند نه ساعت دیواری را، چون هیچ sleep ثابتی ثابت نمی‌کند follower رسیده است.
Think of lag as time travel: each follower lives slightly in the past, and the past has no fixed distance. A version number is the boarding pass — the router checks the pass, not the clock on the wall, because no fixed sleep can prove a follower caught up.

حل گام‌به‌گامStep by step

  1. سه history را بنویس نوشته v7 با LSN برابر ۷ تأیید می‌شود؛ follower A فوراً اعمالش می‌کند و follower B هنوز روی LSN برابر ۵ است. read-your-writes وقتی می‌شکند که خواندن اول به B بخورد و v5 ببیند. monotonic reads وقتی می‌شکند که خواندن دوم به A بخورد (v7) و سومی به B (v5): زمان عقب رفته. ترتیب علّی وقتی می‌شکند که reply@7 از A خوانده شود ولی question@6 هیچ‌وقت به B نرسیده باشد: جواب پیش از سؤال.
  2. خواندن نوشته خود: قیمت leader را بده خواندن کلیدهایی که کاربر شاید همین حالا نوشته به leader (یا follower هم‌زمان) برود: همیشه تازه، همیشه به قیمت بار leader. پنجره یک‌دقیقه‌ای همه‌از-leader و حذف followerهای عقب‌افتاده از مسیر خواندن همان ایده با تایمرند — فقط به‌عنوان heuristic صادق‌اند چون lag کران ندارد.
  3. یکنوا و علّی: پین کن یا مقایسه کن monotonic reads از پین کردن هر نشست به یک replica می‌آید (hash شناسه کاربر شکل ساده‌اش است) یا از حمل کف نسخه دیده‌شده که replica پاسخ‌دهنده باید از آن بگذرد. ترتیب علّی بیشتر می‌خواهد: نوشته‌های وابسته علّی روی یک shard بمانند یا وابستگی‌ها صریح حمل شوند. نشست‌های چنددستگاهی باید metadata آخرین نوشته را مرکزی نگه دارند وگرنه لپ‌تاپ نوشته گوشی را عقب می‌برد.
  4. بدون کران: fallback بزن، هرگز sleep نکن sleep ثابت هیچ‌چیز را ثابت نمی‌کند: follower گیرکرده از رویش رد می‌شود. barrier نسخه با مدرک replica کهنه را رد می‌کند (5 < 7) و وقتی هیچ followerای احراز نشد مسیریاب به leader برمی‌گردد. اگر حتی آن پرهزینه است، وانمود نکن ناهمزمان هم‌زمان است و به‌جای پیچیده کردن اپلیکیشن پایگاه‌ داده قوی‌تر بخر — linearizable به‌علاوه ACID.
  1. Write the three histories Write v7 is ACKed at LSN 7; follower A applies it at once, follower B still sits at LSN 5. Read-your-writes breaks when the first read hits B and sees v5. Monotonic reads break when the second read hits A (v7) and the third hits B (v5): time moved backward. Causal order breaks when reply@7 is read from A while its question@6 never reached B: answer before question.
  2. Read-your-writes: pay for the leader Route reads of keys the user may just have written to the leader (or a synchronous follower): always fresh, always costs leader load. The one-minute all-from-leader window and dropping over-lagging followers from the read path are the same idea with a timer — honest only as a heuristic, because lag has no bound.
  3. Monotonic and causal: pin or compare Monotonic reads come from pinning each session to one replica (hash of the user ID is the simple form) or from carrying a seen-version floor the answering replica must beat. Causal order needs more: keep causally related writes on one shard, or ship explicit dependencies. Multi-device sessions must keep the last-write metadata central, or the laptop regresses what the phone wrote.
  4. No bound: fall back, never sleep A fixed sleep proves nothing: a stalled follower sleeps through it. The version barrier refuses stale replicas with evidence (5 < 7), and when no follower qualifies the router falls back to the leader. If even that is too costly, stop pretending async is synchronous and buy a stronger database — linearizable plus ACID — instead of complicating the application.

طراحی مشخصConcrete design

مسیریاب خواندن سه ورودی دارد: lastLsn نشست، جدول replicaLsn و پرچم ownWrite. اگر ownWrite درست باشد مستقیم به leader؛ وگرنه replicaای که replicaLsn آن دست‌کم lastLsn است وگرنه leader. کلید پارتیشن کاربر همان کلید مسیریابی stickiness است و هر خواندن نسخه‌ای که دیده را در پاسخ برمی‌گرداند تا نشست کفش را بالا ببرد.

The read router takes three inputs: the session lastLsn, a replicaLsn table, and an ownWrite flag. Own-write reads go straight to the leader; other reads go to a replica whose replicaLsn is at least lastLsn, else the leader. The user partition key doubles as the stickiness key, and every read returns the version it saw so the session can raise its floor.

مثال محاسبه‌شدهWorked example

نوشته v7 در LSN برابر ۷؛ A روی ۷ و B روی ۵. خواندن اول با نیاز ۷ به B می‌رسد و رد می‌شود (5 < 7) پس به leader می‌رود؛ خواندن دوم به A می‌رسد و با v7 پذیرفته می‌شود (7 >= 7). اگر هر دو follower روی ۵ بمانند و leader به ۹ برسد، هر خواندن با نیاز ۷ به fallback می‌رسد. همه اعداد آموزشی‌اند.

Write v7 lands at LSN 7; A sits at 7 and B at 5. The first read needing 7 reaches B and is refused (5 < 7), so it goes to the leader; the second read reaches A and is admitted with v7 (7 >= 7). If both followers stall at 5 while the leader advances to 9, every read needing 7 falls back. All numbers are teaching numbers.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · سه ناهنجاری، یک follower کهنهThree anomalies, one stale follower · توالیsequenceباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
کلاینت پروفایل را می‌نویسد و ACK را در LSN برابر ۷ می‌گیرد؛ follower A فوراً اعمال می‌کند و B هنوز روی ۵ است. شکل را بالا‌به‌پایین در سه پرده بخوان.
The client writes the profile and gets its ACK at LSN 7; follower A applies it at once while B still sits at 5. Read top to bottom in three acts.
  1. پیام‌های ۱ تا ۳: نوشتن به leader و دو فلش تکثیر — یکی توپر به A و یکی چین‌چین به B که هنوز روی ۵ است.
  2. پیام‌های ۴ و ۵: خواندن از B مقدار v5 برمی‌گرداند — نوشته خودت نامرئی است و read-your-writes شکسته.
  3. پیام‌های ۶ تا ۹: خواندن از A مقدار v7 می‌دهد ولی سومی دوباره از B مقدار v5 — همان query، دو جواب؛ زمان عقب رفته.
  4. پیام‌های ۱۰ و ۱۱: reply@7 از A دیده می‌شود ولی question@6 روی B نیست — جواب پیش از سؤال، ترتیب علّی شکسته.
  1. Messages 1 to 3: the write to the leader and two replication arrows — one solid to A, one dashed to B, still at 5.
  2. Messages 4 and 5: reading from B returns v5 — your own write is invisible, read-your-writes is broken.
  3. Messages 6 to 9: A answers v7 but the third read from B says v5 again — one query, two answers; time moved backward.
  4. Messages 10 and 11: reply@7 is visible from A while question@6 never reached B — answer before question, causal order broken.

نقطهٔ تصمیم: نقطه تصمیم مقایسه نسخه است: هر ناهنجاری یعنی خواندن نسخه‌ای پایین‌تر از آنچه باید دیده شود.

رد خرابی: اگر B هم اعمال کرده بود هر سه ناهنجاری ناپدید می‌شد — lag تنها علت است؛ اگر کلاینت ننوشته بود، پیام ۵ فقط eventual consistency بود.

تضمین نشان‌داده‌شده: شکل نشان می‌دهد کدام خواندن کدام نسخه را می‌بیند و چرا. کران lag در پروداکشن را ثابت نمی‌کند.

تضمین نشان‌نداده‌شده: درباره مدت lag و نحوه مسیریابی ساکت است — آن‌ها مال نمودار دوم و اسمبل‌اند.

Decision point: The decision point is the version comparison: every anomaly means reading below the version that should have been seen.

Failure trace: Had B applied too, all three anomalies would vanish — lag is the single cause; had the client never written, message 5 would be mere eventual consistency.

Guarantee shown: The figure shows which read sees which version and why. It proves no production lag bound.

Not shown: It is silent on how long B lags and on how to route — those belong to the second diagram and the assembly.

خودآزماییSelf-check

کدام پیام read-your-writes را می‌شکند و مدرک نسخه‌ای آن چیست؟

پاسخ: پیام ۵: بعد از ACK در LSN برابر ۷، مقدار v5 از replicaی روی ۵ برمی‌گردد.

Which message breaks read-your-writes, and what is its version evidence?

Answer: Message 5: after the ACK at LSN 7, value v5 comes back from a replica sitting at 5.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · سه قاعده مسیریابی، یک fallbackThree routing rules, one fallback · جریان دادهdataflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
همان کلاینت LSN برابر ۷ را به خاطر دارد و سه قاعده برای خواندن بعدی رقابت می‌کنند. چپ‌به‌راست بخوان: زمینه، قاعده، replicaها، نتیجه.
The same client remembers LSN 7, and three rules compete for the next read. Read left to right: context, rule, replicas, result.
  1. سه فلش از کلاینت: خواندنِ نوشته خود به primary، نیاز LSN >= 7 به barrier، و تکرارها به stickiness.
  2. barrier نسخه A را با 7 >= 7 می‌پذیرد و B را با 5 < 7 رد می‌کند — هر دو با مدرک روی فلش.
  3. stickiness تکرارها را به A پین می‌کند و فلش چین‌چین fallback می‌گوید: هیچ‌کدام تازه نبودند، از leader بخوان.
  1. Three arrows out of the client: own-write reads to primary, the LSN >= 7 need to the barrier, repeats to stickiness.
  2. The version barrier admits A with 7 >= 7 and refuses B with 5 < 7 — both carry their evidence on the arrow.
  3. Stickiness pins repeats to A, and the dashed fallback arrow says: none was fresh, read from the leader.

نقطهٔ تصمیم: نقطه تصمیم برچسب هر فلش است: پذیرش یا رد همیشه یک مقایسه نسخه نام می‌برد، هرگز یک حدس زمانی.

رد خرابی: اگر A هم stall کند barrier هیچ‌کس را نمی‌پذیرد و همه خواندن‌ها قیمت leader را می‌دهند — بهای نداشتن کران.

تضمین نشان‌داده‌شده: شکل منطق مسیریابی را نشان می‌دهد؛ durability بعد از failover را نشان نمی‌دهد.

تضمین نشان‌نداده‌شده: درباره مرگ primary و metadata چنددستگاهی ساکت است — آن‌ها مال پیگیری‌ها و بحث‌اند.

Decision point: The decision point is each arrow's label: every admit or refuse names a version comparison, never a time guess.

Failure trace: If A stalls too, the barrier admits nobody and every read pays leader price — the cost of having no bound.

Guarantee shown: The figure shows the routing logic; it does not show durability after failover.

Not shown: It is silent on primary death and multi-device metadata — those belong to the follow-ups and the discussion.

خودآزماییSelf-check

B روی ۵ است و نیاز ۷؛ کدام فلش شلیک می‌شود و خواندن کجا می‌رود؟

پاسخ: فلش refuse با مدرک 5 < 7؛ خواندن از مسیر fallback به leader می‌رود (یا از اول primary).

B sits at 5 and the need is 7; which edge fires, and where does the read go?

Answer: The refuse edge with its 5 < 7 evidence; the read goes to the leader through fallback (or via primary from the start).

نمودار اسمبل نهاییFinal assembly diagram · خط زمانی lag: پنجره کهنگی، barrier و fallbackLag timeline: stale window, barrier, fallback · خط زمانیtimelineباز کردن در صفحه جدا ↗Open standalone ↗

دانلود SVG مستقلDownload standalone SVG

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
نوشتن در t0، رسیدن B در t1، و شروع ناحیه stall در t2. سه لاین را با محور زمان بخوان: leader، follower B، و مسیریاب.
The write lands at t0, B catches up at t1, and the stall zone starts at t2. Read the three lanes against the time axis: leader, follower B, and router.
  1. لاین leader: نوار سبز یکدست — v7 از t0 با LSN برابر ۷ اعمال شده.
  2. لاین B: قرمز تا t1 (کهنه، سرو v5) و سبز بعدش (v7)؛ خواندن r1 در پنجره قرمز رد می‌شود و r2 بعد از t1 پذیرفته.
  3. ناحیه stall از t2 کران بالا ندارد: اگر B آنجا بماند، خواندن r3 با هیچ barrierای پذیرفته نمی‌شود و fallback به leader می‌رود.
  1. Leader lane: one solid green bar — v7 applied since t0 at LSN 7.
  2. B lane: red until t1 (stale, serves v5), green after (v7); read r1 inside the red window is refused, r2 past t1 admitted.
  3. The stall zone past t2 has no upper bound: if B stalls there, no barrier admits read r3, and fallback goes to the leader.

نقطهٔ تصمیم: نقطه تصمیم همان barrier نمودار دوم است، این‌بار روی محور زمان؛ fallback یک ناحیه است نه یک لحظه.

رد خرابی: اگر ناحیه stall هیچ‌وقت نیاید fallback بیکار می‌ماند — ولی طراحی باید برایش آماده باشد، چون lag کران ندارد.

تضمین نشان‌داده‌شده: شکل دو گام اول را در یک خط زمانی سوار می‌کند؛ سازوکار تازه‌ای اضافه نمی‌کند.

تضمین نشان‌نداده‌شده: زمان‌ها آموزشی‌اند نه اندازه‌گیری؛ فرکانس fallback را مانیتورینگ واقعی می‌گوید.

Decision point: The decision point is the same barrier as diagram two, now against time; fallback is a region, not a moment.

Failure trace: If the stall zone never arrives, fallback idles — but the design must still be ready for it, because lag is unbounded.

Guarantee shown: The figure composes the first two steps onto one timeline; it adds no new mechanism.

Not shown: The times are teaching times, not measurements; real monitoring reports the fallback rate.

خودآزماییSelf-check

r1 و r2 چه فرقی دارند؟

پاسخ: r1 در پنجره کهنگی است (5 < 7 رد و به leader) و r2 بعد از t1 است (7 >= 7 پذیرفته از replica).

How do r1 and r2 differ?

Answer: r1 lands in the stale window (5 < 7, refused, to the leader) and r2 lands past t1 (7 >= 7, admitted from the replica).

گزینه‌های جایگزینAlternatives

  • sleep ثابت پیش از خواندن: ارزان‌ترین برای نوشتن و بدون هیچ تضمینی — فقط به‌عنوان heuristic پنهان‌سازی تأخیر نگهش دار و barrier همچنان درستی را اجرا کند.
  • تکثیر هم‌زمان یا نیمه‌هم‌زمان: followerها ذاتاً تازه‌اند ولی یک follower کند هر نوشته را متوقف می‌کند — قیمت تازگی به availability منتقل شده.
  • Fixed sleep before reading: cheapest to write, guarantees nothing — keep it only as a latency-hiding heuristic with the barrier still enforcing correctness.
  • Synchronous or semisynchronous replication: followers are fresh by construction, but one slow follower stalls every write — freshness moved its price to availability.

سناریوهای خرابیFailure scenarios

  • مرگ primary دقیقاً بعد از ACK با تکثیر ناهمزمان: نوشته‌های تأییدشده ولی تکثیرنشده گم می‌شوند پس read-your-writes به leader تازه نوشته خود کاربر را از دست می‌دهد. نگهبان: تکثیر نیمه‌هم‌زمان یا اعلام صادقانه پنجره گم‌شدن.
  • مرگ replica پین‌شده و پین مجدد نشست به کهنه‌تر: monotonic reads بی‌صدا عقب می‌رود. نگهبان: حمل کف نسخه دیده‌شده در پین مجدد و رد replicaهای پایین‌تر از آن.
  • Primary dies right after ACK with async replication: ACKed-but-unreplicated writes are lost, so read-your-writes to the new leader misses the user's own write. Guard: semisynchronous replication or state the loss window honestly.
  • Pinned replica dies and the session re-pins to a staler one: monotonic reads regress silently. Guard: carry the seen-version floor across the re-pin and refuse replicas below it.

کد قابل اجرا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: version-barrier read routing over one leader + two followers.
// The router never trusts a fixed sleep; it compares versions and falls back
// to the leader when no follower is fresh enough (unbounded lag).
public class LagRoutingDemo {
    record Replica(String name, long lsn, String profile) {}

    // Route a read that must see at least requiredLsn. Returns replica name or LEADER.
    static String route(long requiredLsn, long sessionSeen, List<Replica> followers) {
        long need = Math.max(requiredLsn, sessionSeen); // read-your-writes + monotonic floor
        Replica best = null;
        for (Replica r : followers)
            if (r.lsn() >= need && (best == null || r.lsn() > best.lsn())) best = r;
        return best == null ? "LEADER (fallback: no follower fresh enough)" : best.name();
    }

    public static void main(String[] a) {
        // Leader just applied profile v7 at LSN 7; A caught up, B lags at LSN 5.
        List<Replica> followers = List.of(
                new Replica("A", 7, "v7"),
                new Replica("B", 5, "v5"));
        long sessionSeen = 7; // client wrote v7, so it must never read older than v7

        System.out.println("read-your-writes  -> " + route(7, sessionSeen, followers));
        // Monotonic check: a second read pinned to B would regress 7 -> 5, so refuse B.
        System.out.println("monotonic-second-read pinned-to-B would regress 7->5: refused, use "
                + route(0, sessionSeen, followers));
        // Causal: reply (LSN 7) depends on question (LSN 6); a replica at 5 has neither in order.
        System.out.println("causal(question@6,reply@7) on B@5: unsafe, use "
                + route(7, sessionSeen, followers));
        // Unbounded lag: both followers stall at 5 while leader advances to 9.
        List<Replica> stalled = List.of(
                new Replica("A", 5, "v5"),
                new Replica("B", 5, "v5"));
        System.out.println("unbounded-lag      -> " + route(7, sessionSeen, stalled));
        System.out.println("OK: no fixed sleep used; every route names its version evidence");
    }
}

اجرا: javac LagRoutingDemo.java && java LagRoutingDemo — فقط استاندارد جاوا، فوری.

خروجی مورد انتظار: read-your-writes به A می‌رسد؛ خواندن دوم پین‌شده به B رد می‌شود و A می‌دهد؛ علّی روی B@5 ناامن و A می‌دهد؛ lag بی‌حد به LEADER برمی‌گردد؛ OK پایانی. (خروجی واقعی همین محیط)

Run: javac LagRoutingDemo.java && java LagRoutingDemo — stdlib Java only, instant.

Expected output: read-your-writes routes to A; the second read pinned to B is refused for A; causal on B@5 is unsafe for A; unbounded lag falls back to LEADER; closing OK. (actual output of this environment)

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

در آزمایشگاه هر سه ناهنجاری را عمداً بازتولید کن: نوشتن و خواندن فوری از follower، دو خواندن از دو replica، و جابه‌جایی ترتیب علّی؛ بعد برای هرکدام راه‌حل (leader، پین، barrier) را روشن کن و ناپدید شدن ناهنجاری را نشان بده. lag را به‌صورت توزیع (نه یک عدد) بسنج و نرخ fallback را هشدار بده.
In the lab deliberately reproduce all three anomalies: write plus immediate follower read, two reads from two replicas, and swapped causal order; then switch on each fix (leader, pin, barrier) and show the anomaly gone. Measure lag as a distribution, not one number, and alert on the fallback rate.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (سینیور): اگر primary بعد از ACK خراب شود، read-your-writes چه وابستگی‌ای به replication durability دارد؟Follow-up (Senior): If the primary fails after ACK, how does read-your-writes depend on replication durability?

کاملاً: read-your-writes قول می‌دهد نوشته خود کاربر خواندنی است و primary ناهمزمان می‌تواند نوشته‌ای را ACK کند که هیچ‌وقت تکثیر نکرده — leader تازه اصلاً آن را نداشته. با تکثیر نیمه‌هم‌زمان دست‌کم یک follower هر نوشته ACKشده را دارد پس مسیریابی خواندنِ نوشته خود به آنجا (یا leader ارتقایافته) قول را نگه می‌دارد. با تکثیر کاملاً ناهمزمان جواب صادق پنجره گم‌شدن را نام می‌برد: نوشته‌های ACKشده ولی تکثیرنشده می‌توانند ناپدید شوند و هیچ ترفند مسیریابی برشان نمی‌گرداند.

Totally: read-your-writes promises the user's own write is readable, and an async primary can ACK a write it never replicated — the new leader simply never had it. With semisynchronous replication at least one follower holds every ACKed write, so routing own-write reads there (or to the promoted leader) keeps the promise. With fully async replication the honest answer names the loss window: ACKed-but-unreplicated writes can vanish, and no routing trick recovers them.

پیگیری (استف): دو دستگاه یک حساب مشترک دارند: گوشی می‌نویسد و لپ‌تاپ می‌خواند. barrier تو کجا می‌شکند؟Follow-up (Staff): Two devices share one account: the phone writes, the laptop reads. Where does your barrier break?

در حالت نشست: اگر lastLsn در حافظه گوشی باشد، barrier لپ‌تاپ از صفر شروع می‌کند و با خوشحالی خواندن کهنه سرو می‌کند. راه‌حل metadata مرکزی آخرین نوشته است (یا مسیریابی خواندن‌های پس از نوشته هر دو دستگاه به leader) به قیمت یک lookup بین‌دستگاهی در مسیر خواندن. آن lookup را جزو طراحی نام ببر نه تبصره.

At the session state: if lastLsn lives in the phone's memory, the laptop's barrier starts at zero and happily serves stale reads. The fix is central last-write metadata (or routing both devices' post-write reads to the leader), at the price of a cross-device lookup on the read path. Name that lookup as part of the design, not as an afterthought.

پیگیری (استف): هزار درخواست هم‌زمان یک cache miss می‌بینند؛ چه می‌کنی؟Follow-up (Staff): A thousand requests miss the same cache key. What do you do?

single-flight برای هر key بازسازی هم‌زمان را یکی می‌کند؛ انتظار بقیه باید deadline و سقف داشته باشد. TTL jitter از انقضای هم‌زمان کلیدها می‌کاهد ولی hot key واحد را حل نمی‌کند. stale-while-revalidate فقط در مرز تازگی پذیرفته‌شدهٔ محصول مجاز است؛ دادهٔ مالی/ACL را بی‌قید stale نکن. شمار بازسازی هم‌زمان باید کران داشته باشد تا miss storm به DB منتقل نشود.

Use per-key single-flight to coalesce rebuilds with bounded waiters and deadlines. TTL jitter spreads expiry across keys but does not solve one hot key. Stale-while-revalidate needs an explicit product freshness contract; do not serve financial or ACL state arbitrarily stale. Bound concurrent rebuilds to keep miss storms from overwhelming the database.

اشتباهات رایجCommon mistakes

  • sleep ثابت بعد از نوشتن و اسمش را consistency گذاشتن. lag کران ندارد؛ توقف از هر sleepای بیشتر عمر می‌کند و sleep باز هم از هر خواندن مالیات می‌گیرد.
  • پین کردن نشست به یک replica و ادعای read-your-writes. stickiness زمان عقب‌گرد را می‌بندد؛ درباره نوشته‌ای که جای دیگر نشسته هیچ‌چیز نمی‌گوید.
  • مقایسه timestamp ساعت دیواری به‌جای نسخه. ساعت‌ها بین ماشین‌ها skew دارند (آن فصل ۹ است)؛ فقط LSN یا version vector نوشته‌ها را مرتب می‌کند.
  • Sleeping a fixed time after writing and calling it consistency. Lag has no bound; a stall outlives any sleep, and the sleep still taxes every read.
  • Pinning a session to one replica and claiming read-your-writes. Stickiness stops backward time; it says nothing about a write that landed elsewhere.
  • Comparing wall-clock timestamps instead of versions. Clocks skew across machines (that is Chapter 9); only LSNs or version vectors order writes.

منابع:Sources: Amazon · SDE III interview preparation · Atlassian · engineering interview handbook

22 تمرین ۲ — سبد خریدِ هم‌زمانExercise 2 — the concurrent shopping cart

L2 · SeniorL2 · Seniorوابسته به نقشRole-specific35 دقیقه بحث35 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
دو دستگاه سبد مشترک را آفلاین ویرایش می‌کنند: هر کدام یک آیتم شناخته‌شده را حذف می‌کند و هیچ add تازه‌ای وجود ندارد. LWW، union سادهٔ siblings و OR-Set با tag/tombstone را مقایسه کن. سپس add هم‌زمان با remove را اضافه کن و semantics مطلوب محصول را انتخاب کن.
Start with two offline devices removing different known cart items and no new adds. Compare LWW, sibling union and an OR-Set with tagged adds/removals. Then introduce concurrent add/remove and choose product semantics.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

حذف‌های مشاهده‌شده زیر OR-Set همگرا می‌شوند: adds منهای removes خالی است هر دستگاهی هرچه را حذف کرده باشد، چون هر حذف فقط tagهایی که دیده را بایگانی کرده. LWW یک سبد کامل را نگه می‌دارد و ویرایش دیگر را بی‌صدا می‌اندازد (ساعت دستگاه‌ها skew دارد پس بازمانده دلبخواهی است) و union siblingها هر دو سبد را نگه می‌دارد و صورت‌حسابش را به انسان می‌دهد. add تازه هم‌زمان — tagای آفلاین که حذف را ندیده — semantics را دوشقه می‌کند: add-wins نگهش می‌دارد و remove-wins می‌اندازدش و آن انتخاب قرارداد محصول است نه ریاضی. tombstoneها فقط با causal stability بازنشسته می‌شوند، هرگز با تایمر.
Observed removals converge under an OR-Set: adds minus removes is empty whichever device removed what, because every remove filed only tags it saw. LWW keeps one whole cart and silently drops the other edit (device clocks skew, so the survivor is arbitrary), and sibling union keeps both carts and bills a human. A concurrent fresh add — a tag minted offline that never saw the remove — splits the semantics: add-wins keeps it, remove-wins drops it, and that choice is a product contract, not math. Tombstones retire only on causal stability, never on a timer.

صورت مسئله و فرض‌هاProblem and assumptions

  • دو دستگاه یک سبد مشترک دارند، هر دو {milk:t1, bread:t2} را دیده‌اند و بعد آفلاین می‌شوند؛ در فاز یک هیچ add تازه‌ای نیست.
  • بدون ساعت هم‌زمان: timestampها بین دستگاه‌ها هیچ ترتیبی نمی‌دهند و tagها per-add یکتا هستند.
  • فاز دو دقیقاً یک add تازه هم‌زمان اضافه می‌کند (milk@t3 آفلاین ساخته شده)؛ سناریوی آموزشی با آیتم‌های خنثی.
  • Two devices share one cart, both saw {milk:t1, bread:t2}, then go offline; no fresh adds in phase one.
  • No synchronized clocks: timestamps order nothing across devices, tags are unique per add.
  • Phase two adds exactly one concurrent fresh add (milk@t3 minted offline); teaching scenario, neutral cart items.

چرا این روش و این عددها؟Why this method and these numbers?

tag یکتای هر add هویت عمل را حفظ می‌کند؛ remove فقط tagهای دیده‌شده را حذف می‌کند، پس اجتماع مجموعهٔ حذف‌ها هر دو حذف آفلاین را حفظ می‌کند. add تازهٔ ندیده tag دیگری دارد و در add-wins باقی می‌ماند. union سادهٔ مقدارهای sibling این history را ندارد و می‌تواند هر دو کالا را برگرداند. برای GC باید ثابت کنیم replica یا پیامی با tag قدیمی بازنمی‌گردد؛ گذشتن زمان بدون قرارداد عضویت چنین اثباتی نیست.

Unique add tags preserve operation identity. Removes record observed tags, so merging removal sets preserves both offline deletions. An unseen fresh add has another tag and survives add-wins semantics. Plain sibling value union lacks this history and can resurrect both items. GC requires evidence that old tags cannot return through replicas or messages; elapsed time without membership rules is insufficient.

مدل ذهنیMental model

به هر add یک tag نام بده و بگذار remove فقط tagهایی را خط بزند که واقعاً دیده. دو دستگاه که tagهای دیده متفاوت را خط زده‌اند همیشه بعدش توافق دارند — تنها دعوا غریبه‌ای است که با tag تازه‌ای که هیچ‌کس ندیده وارد شده.
Give every add a name tag, and let a remove cross out only the tags it actually saw. Two devices crossing out different seen tags always agree afterwards — the only quarrel is the stranger who walked in wearing a fresh tag nobody saw.

حل گام‌به‌گامStep by step

  1. حذف‌های مشاهده‌شده باید همگرا شوند A شیر را با دیدن t1 حذف می‌کند و B نان را با دیدن t2. union adds برابر {milk:t1, bread:t2} و union removes هم همان مجموعه است پس live بعد از sync روی هر دو دستگاه خالی است. هیچ‌چیز زنده نمی‌شود — این میله همگرایی است که هر راهبرد باید از آن بپرد.
  2. LWW به سبد کامل رأی می‌دهد last-write-wins یک timestamp per-cart مقایسه می‌کند و برنده کامل را نگه می‌دارد: اگر ساعت B جلو باشد، حذف شیرِ A ناپدید می‌شود و شیر به قفسه برمی‌گردد. یک lost update per-conflict، بی‌صدا طبق طراحی، و ساعت دستگاه‌ها skew دارد — بازمانده دلبخواهی است نه درست.
  3. siblingها صورت‌حساب را به انسان می‌دهند union siblingها هر دو سبد را کنار هم نگه می‌دارد و از کسی می‌خواهد ادغامشان کند. هیچ‌چیز گم نمی‌شود ولی هیچ‌چیز همگرا هم نمی‌شود: برای تعارض‌های نادری که انسان باید ببیند درست است و برای سبدی که باید آفلاین تسویه شود غلط.
  4. OR-Set: اول tagها، بعد قرارداد محصول OR-Set adds و removes را جدا بایگانی می‌کند و live را adds منهای removes حساب می‌کند: حذف‌های مشاهده‌شده دقیقاً همگرا می‌شوند. بعد milk@t3 تازه B — آفلاین ساخته شده و حذف را ندیده — از تفریق جان سالم به در می‌برد. add-wins شیر را نگه می‌دارد و remove-wins حتی tag ندیده را هم لغو می‌کند. یک history، دو سبد دفاع‌پذیر: قاعده را در spec بنویس.
  5. فقط با stability فراموش کن tombstone t2 نان فقط وقتی فراموش می‌شود که هر replica زنده‌ای حذف را دیده باشد — causal stability. دستگاهی که یک سال آفلاین بوده آن را ندیده پس collection را وتو می‌کند؛ یک تایمر تنها روزی که آن دستگاه sync کند نان را زنده می‌کند.
  1. Observed removals must converge A removes milk having seen t1, B removes bread having seen t2. Union of adds is {milk:t1, bread:t2}, union of removes is the same set, so live is empty on both devices after sync. Nothing resurrects — this is the convergence bar every strategy must clear.
  2. LWW votes whole carts Last-write-wins compares one timestamp per cart and keeps the whole winner: if B's clock runs ahead, A's milk removal vanishes and milk is back on the shelf. One lost update per conflict, silent by design, and device clocks skew — the survivor is arbitrary, not correct.
  3. Siblings bill a human Sibling union keeps both carts side by side and asks someone to merge them. Nothing is lost, but nothing converges either: right for rare conflicts a person should inspect, wrong for a cart that must check out while offline.
  4. OR-Set: tags, then the product call The OR-Set files adds and removes separately and computes live as adds minus removes: observed removals converge exactly. Then B's fresh milk@t3 — minted offline, never saw the remove — survives the subtraction. Add-wins keeps milk, remove-wins cancels even the unseen tag. Same history, two defensible carts: write the rule in the spec.
  5. Forget only on stability Bread's t2 tombstone may be forgotten only once every live replica has seen the removal — causal stability. A device offline for a year has not seen it, so it vetoes collection; a lone timer would resurrect bread the day that device syncs.

طراحی مشخصConcrete design

حالت OR-Set دو نگاشت است: adds از آیتم به مجموعه tagها و removed از آیتم به tagهای دیده‌شده حذف‌شده. add یک tag تازه می‌سازد، remove فقط tagهای دیده را بایگانی می‌کند، و merge اجتماع هر دو نگاشت را می‌گیرد؛ live تفاضل آن‌هاست. قاعده add-wins/remove-wins یک پرچم پیکربندی در کنار merge است نه جزئی از ریاضی.

The OR-Set state is two maps: adds from item to tag sets and removed from item to seen removed tags. add mints a fresh tag, remove files only seen tags, and merge unions both maps; live is their difference. The add-wins/remove-wins rule is a configuration flag beside the merge, not part of the math.

مثال محاسبه‌شدهWorked example

شروع {milk:t1, bread:t2} روی هر دو دستگاه. فاز یک: A حذف شیر و B حذف نان — union adds و union removes برابرند پس سبد {}. فاز دو: milk@t3 تازه B به adds اضافه می‌شود ولی در removes نیست پس live در add-wins برابر {milk:t3} و در remove-wins برابر {}. tombstone t2 تا stability علّی می‌ماند.

Start {milk:t1, bread:t2} on both devices. Phase one: A removes milk, B removes bread — union of adds equals union of removes, so the cart is {}. Phase two: B's fresh milk@t3 joins adds but not removes, so live is {milk:t3} under add-wins and {} under remove-wins. The t2 tombstone stays until causal stability.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · دو حذف آفلاین، یک syncTwo offline removes, one sync · توالیsequenceباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
دو دستگاه آفلاین‌اند: A شیر را و B نان را حذف کرده و حالا هر دو sync می‌کنند. بالا‌به‌پایین در دو پرده بخوان: اول حذف‌های مشاهده‌شده، بعد add تازه هم‌زمان.
Two devices are offline: A removed milk, B removed bread, and now both sync. Read top to bottom in two acts: observed removes first, then the concurrent fresh add.
  1. پیام‌های ۱ و ۲: هر دستگاه حذفش را با tagهای دیده‌شده push می‌کند — حذف شیر با t1 و حذف نان با t2.
  2. پیام‌های ۳ و ۴: تبادل سبد خالی را برمی‌گرداند — union adds منهای union removes؛ هیچ حذف‌شده‌ای زنده نشده.
  3. پیام‌های ۵ تا ۷: B شیر با tag تازه t3 را اضافه می‌کند که حذف را ندیده؛ همان history زیر add-wins شیر را زنده نگه می‌دارد و زیر remove-wins می‌اندازد.
  1. Messages 1 and 2: each device pushes its remove with the tags it saw — milk gone with t1, bread gone with t2.
  2. Messages 3 and 4: the exchange returns the empty cart — union of adds minus union of removes; no deleted item came back.
  3. Messages 5 to 7: B adds milk with fresh tag t3 that never saw the remove; the same history keeps milk under add-wins and drops it under remove-wins.

نقطهٔ تصمیم: نقطه تصمیم tag است: حذف فقط tag دیده‌شده را می‌بندد و tag تازه هم‌زمان بیرون قاعده است تا محصول تصمیم بگیرد.

رد خرابی: اگر tagها یکتا نبودند، حذف یک tag هر دو حضور را می‌کشت؛ اگر t3 دیده‌شده بود، هیچ دوشقه‌ای پیش نمی‌آمد.

تضمین نشان‌داده‌شده: شکل نشان می‌دهد حذف‌های مشاهده‌شده کجا همگرا می‌شوند و add تازه کجا دوشقه می‌شود. قاعده محصول را انتخاب نمی‌کند.

تضمین نشان‌نداده‌شده: درباره LWW و siblingها و GC ساکت است — آن‌ها مال نمودار دوم و چرخه‌اند.

Decision point: The decision point is the tag: a remove closes only seen tags, and the concurrent fresh tag stays outside the rule until the product decides.

Failure trace: Had tags not been unique, removing one tag would kill both presences; had t3 been seen, no split would arise.

Guarantee shown: The figure shows where observed removes converge and where the fresh add splits. It does not choose the product rule.

Not shown: It is silent on LWW, siblings and GC — those belong to the second diagram and the lifecycle.

خودآزماییSelf-check

چرا پیام‌های ۳ و ۴ سبد خالی می‌دهند ولی پیام ۶ شیر را برمی‌گرداند؟

پاسخ: چون حذف‌های پیام ۱ و ۲ همه tagهای دیده‌شده را پوشش می‌دهند ولی t3 تازه هیچ‌وقت دیده نشده و از تفریق جان سالم به در می‌برد.

Why do messages 3 and 4 give an empty cart while message 6 brings milk back?

Answer: Because the removes in messages 1 and 2 cover every seen tag, while fresh t3 was never seen and survives the subtraction.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · سه راهبرد، سه سرنوشتThree strategies, three fates · جریان دادهdataflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
همان ویرایش‌های آفلاین وارد sync می‌شوند و سه راهبرد جدا قضاوت می‌کنند. چپ‌به‌راست بخوان: ویرایش‌ها، ورودی sync، قضاوت، سه سبد.
The same offline edits enter sync, and three strategies judge separately. Read left to right: edits, sync input, judgment, three carts.
  1. سه فلش چپ: حذف شیر با t1، حذف نان با t2، و add تازه شیر با t3 ندیده — مشاهده‌شده از تازه جداست.
  2. گره sync اجتماع adds و removes را می‌گیرد و هر سه راهبرد همان ورودی را می‌گیرند — مقایسه منصفانه است.
  3. سه نتیجه: LWW یک برنده، siblingها هر دو نسخه، و OR-Set سبد خالی به‌علاوه قاعده t3 — با برچسب قیمت هرکدام.
  1. Three arrows on the left: milk removed with t1, bread removed with t2, and the fresh unseen milk add with t3 — observed stays apart from fresh.
  2. The sync node takes the union of adds and removes, and all three strategies take the same input — a fair comparison.
  3. Three results: LWW keeps one winner, siblings keep both versions, and OR-Set gives the empty cart plus the t3 rule — each labelled with its price.

نقطهٔ تصمیم: نقطه تصمیم ورودی یکسان است: تفاوت سرنوشت‌ها از داده نیست، از قاعده قضاوت است.

رد خرابی: اگر ساعت‌ها معتبر بودند LWW هم همگرا می‌شد — ولی ساعت دستگاه‌ها skew دارد و همان‌جا LWW می‌بازد.

تضمین نشان‌داده‌شده: شکل هر سه سرنوشت را کنار هم می‌گذارد؛ انتخاب محصول را نمی‌کند.

تضمین نشان‌نداده‌شده: درباره طول عمر tombstone ساکت است — آن مال چرخه پایانی است.

Decision point: The decision point is the identical input: the fates differ because of the judgment rule, not the data.

Failure trace: Had clocks been trustworthy, LWW would converge too — but device clocks skew, and that is exactly where LWW loses.

Guarantee shown: The figure lays the three fates side by side; it does not make the product choice.

Not shown: It is silent on tombstone lifetime — that belongs to the closing lifecycle.

خودآزماییSelf-check

کدام راهبرد بدون دخالت انسان همگرا می‌شود و به چه قیمتی؟

پاسخ: OR-Set با tag و tombstone؛ قیمتش حالت tombstone و قرارداد صریح add-wins/remove-wins برای tag تازه است.

Which strategy converges with no human involved, and at what price?

Answer: The OR-Set with tags and tombstones; its price is tombstone state plus an explicit add-wins/remove-wins contract for the fresh tag.

نمودار اسمبل نهاییFinal assembly diagram · چرخه tombstone تا فراموشی امنTombstone lifecycle to safe forgetting · چرخهlifecycleباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
عمر یک آیتم از add تا فراموشی tombstone: مسیر اصلی بالا، add هم‌زمان در لاین انتظار، و خروج‌های پایانی پایین. ستون‌ها را به‌ترتیب بخوان.
One item's life from add to tombstone forgetting: the main path on top, the concurrent add in the waiting lane, terminal exits below. Read the columns in order.
  1. ستون‌های ۰ و ۱: add با tag تازه، بعد حذف با بایگانی tagهای دیده‌شده — تولد و مرگ عادی آیتم.
  2. ستون ۲: اگر tag تازه t3 از راه برسد، تصمیم add-wins یا remove-wins در همان‌جا دوشقه می‌شود و هر شاخه پایانه خودش را دارد.
  3. ستون‌های ۳ و ۴: وقتی هر replica زنده حذف را دید، حالت پایدار علّی می‌شود و فقط آن‌وقت tombstone جمع می‌شود و فراموش می‌شود.
  1. Columns 0 and 1: add with a fresh tag, then remove filing the seen tags — the item's ordinary birth and death.
  2. Column 2: if fresh tag t3 arrives, the add-wins or remove-wins decision forks right there, each branch with its own terminal.
  3. Columns 3 and 4: once every live replica saw the remove, the state is causally stable, and only then is the tombstone collected and forgotten.

نقطهٔ تصمیم: دو نقطه تصمیم جدا: قاعده add تازه قرارداد محصول است و جمع‌آوری tombstone پایداری علّی — هیچ‌کدام تایمر نیستند.

رد خرابی: اگر دستگاه یک‌سال‌آفلاین قبل از sync بمیرد و مرده اعلام شود، وتویش با تغییر membership لغو می‌شود — ولی آن خودش قرارداد پرریسک دیگری است.

تضمین نشان‌داده‌شده: شکل هر دو قرارداد (قاعده تازه و GC امن) را در یک چرخه جمع می‌کند؛ زمان واقعی sync را نمی‌گوید.

تضمین نشان‌نداده‌شده: درباره هزینه نگه‌داشتن tombstoneها و UX نمایش ساکت است.

Decision point: Two separate decision points: the fresh-add rule is a product contract, and tombstone collection is causal stability — neither is a timer.

Failure trace: If the year-offline device dies before syncing and is declared dead, its veto lifts with the membership change — but that is itself another risky contract.

Guarantee shown: The figure gathers both contracts (fresh rule and safe GC) into one lifecycle; it says nothing about real sync times.

Not shown: It is silent on the cost of holding tombstones and on display UX.

خودآزماییSelf-check

tombstone نان دقیقاً چه زمانی جمع می‌شود؟

پاسخ: وقتی هر replica زنده حذف t2 را دیده باشد — پایداری علّی؛ دستگاه یک‌سال‌آفلاین تا sync آن را وتو می‌کند.

Exactly when is the bread tombstone collected?

Answer: Once every live replica has seen the t2 removal — causal stability; the year-offline device vetoes it until it syncs.

گزینه‌های جایگزینAlternatives

  • LWW وقتی تعارض‌ها نادر و loss قابل‌قبول است: یک timestamp، بدون tombstone و بدون مسئله GC — ولی هرگز برای سبد، که هر تعارضش نیت کاربر را حمل می‌کند.
  • داور مرکزی per-cart (یک نویسنده همه ویرایش‌ها را مرتب کند): توافق کامل و صفر نوشتن آفلاین — با فرض offline-first تمرین در تضاد است.
  • LWW when conflicts are rare and loss is acceptable: one timestamp, no tombstones, no GC problem — but never for carts, where every conflict carries user intent.
  • Central arbiter per cart (one writer orders all edits): perfect agreement, zero offline writes — contradicts the offline-first premise of the exercise.

سناریوهای خرابیFailure scenarios

  • بازگشت دستگاه یک‌سال‌آفلاین بعد از GC زودهنگام: حالت همگراشده‌اش هنوز bread@t2 را live دارد و merge نان را همه‌جا زنده می‌کند. نگهبان: tombstone را فقط بعد از causal stability جمع کن یا sync عقب‌مانده را تا جبران عقب‌ماندگی ببند.
  • استفاده مجدد tag در دو add: حذف یک tag را بایگانی می‌کند و حضور هر دو را می‌کشد. نگهبان: tag باید per-add یکتا باشد (شناسه دستگاه به‌علاوه شمارنده شکل آموزشی است).
  • Year-offline device returns after premature GC: its converged state still carries bread@t2 as live, and the merge resurrects bread everywhere. Guard: collect a tombstone only past causal stability, or block the straggler's sync until it catches up.
  • Reused tag across two adds: the remove files one tag and kills both items' presence. Guard: tags must be unique per add (device ID plus counter is the teaching form).

کد قابل اجرا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: observed removals vs concurrent fresh adds in a shopping cart.
// OR-Set done right: state = (adds, tombstones); live = adds minus tombstones.
// A remove only files the tags it SAW; a concurrent fresh tag survives (add-wins)
// unless the product rule says removes cancel even unseen tags (remove-wins).
public class ORSetDemo {
    record Cart(Map<String, Set<String>> adds, Map<String, Set<String>> removed) {
        Cart() { this(new HashMap<>(), new HashMap<>()); }
    }

    static void add(Cart c, String item, String tag) {
        c.adds().computeIfAbsent(item, k -> new HashSet<>()).add(tag);
    }

    static void removeSeen(Cart c, String item) {
        Set<String> seen = c.adds().getOrDefault(item, Set.of());
        c.removed().computeIfAbsent(item, k -> new HashSet<>()).addAll(seen);
    }

    static Cart merge(Cart x, Cart y) {
        Cart m = new Cart();
        for (Cart s : List.of(x, y))
            for (var e : s.adds().entrySet())
                m.adds().computeIfAbsent(e.getKey(), k -> new HashSet<>()).addAll(e.getValue());
        for (Cart s : List.of(x, y))
            for (var e : s.removed().entrySet())
                m.removed().computeIfAbsent(e.getKey(), k -> new HashSet<>()).addAll(e.getValue());
        return m;
    }

    static Set<String> live(Cart m, String item) {
        Set<String> s = new HashSet<>(m.adds().getOrDefault(item, Set.of()));
        s.removeAll(m.removed().getOrDefault(item, Set.of()));
        return new TreeSet<>(s);
    }

    static Set<String> liveItems(Cart m) {
        Set<String> items = new TreeSet<>(m.adds().keySet());
        items.removeIf(k -> live(m, k).isEmpty());
        return items;
    }

    public static void main(String[] a) {
        // Phase 1 (book fig 6-10): both saw {milk:t1, bread:t2}; A removes milk,
        // B removes bread; NO fresh adds. Tombstones cover every tag -> empty cart.
        Cart devA = new Cart(), devB = new Cart();
        for (Cart c : List.of(devA, devB)) { add(c, "milk", "t1"); add(c, "bread", "t2"); }
        removeSeen(devA, "milk");
        removeSeen(devB, "bread");
        System.out.println("observed-removes only -> merged cart = " + liveItems(merge(devA, devB))
                + " (nothing resurrected)");

        // Phase 2: A removes milk having seen only t1; B CONCURRENTLY re-adds milk
        // with a fresh tag t3 it minted offline (never saw the remove).
        Cart a2 = new Cart(), b2 = new Cart();
        for (Cart c : List.of(a2, b2)) { add(c, "milk", "t1"); add(c, "bread", "t2"); }
        removeSeen(a2, "milk");
        add(b2, "milk", "t3");
        Cart m2 = merge(a2, b2);
        System.out.println("concurrent add-vs-remove, add-wins    -> milk tags = " + live(m2, "milk"));
        System.out.println("concurrent add-vs-remove, remove-wins -> milk tags = [] "
                + "(product rule cancels even the unseen t3)");

        // Phase 3: the t2 tombstone for bread may be forgotten only once every live
        // replica has seen it (causal stability); a device offline for a year vetoes GC.
        System.out.println("tombstone-GC safe when every live replica saw the remove; "
                + "a year-offline device vetoes collection");
        System.out.println("OK: observed-removal converges; fresh-add rule must be an explicit product call");
    }
}

اجرا: javac ORSetDemo.java && java ORSetDemo — فقط استاندارد جاوا، فوری.

خروجی مورد انتظار: فاز یک سبد خالی بدون زنده شدن؛ فاز دو در add-wins برابر [t3] و در remove-wins برابر []؛ قانون GC و OK پایانی. (خروجی واقعی همین محیط)

Run: javac ORSetDemo.java && java ORSetDemo — stdlib Java only, instant.

Expected output: phase one merges to an empty cart with no resurrection; phase two gives [t3] under add-wins and [] under remove-wins; GC rule and closing OK. (actual output of this environment)

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

سناریوی شکل ۶-۱۰ کتاب را اجرا کن و خالی بودن سبد را assert کن؛ add هم‌زمان را زیر هر دو قاعده assert کن؛ یک عقب‌مانده یک‌ساله را شبیه‌سازی کن و نشان بده GC زودهنگام نان را زنده می‌کند.
Run the book figure 6-10 scenario and assert the empty cart; assert the concurrent add under both rules; simulate a year-long straggler and show early GC resurrecting bread.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (سینیور): دستگاهی یک سال آفلاین بوده؛ tombstone را چه زمانی می‌توان پاک کرد؟Follow-up (Senior): A device has been offline for a year. When is it safe to collect tombstones?

فقط وقتی هر replica زنده‌ای حذف را دیده باشد — causal stability — چون collection یعنی فراموشی و replicaای که حذف را ندیده در sync بعدی tagهای liveاش را برمی‌گرداند. دستگاه یک‌سال‌آفلاین تا sync نکرده (یا با تغییر membership صریح مرده اعلام نشده که خودش قرارداد سطح محصول با ریسک خودش است) collection را وتو می‌کند. هرگز با تایمر تنها جمع نکن: گذر زمان مدرک تحویل نیست.

Only when every live replica has seen the removal — causal stability — because collection means forgetting, and a replica that never saw the remove will reintroduce its live tags at the next sync. The year-offline device vetoes collection until it syncs (or is declared dead by an explicit membership change, which is itself a product-level call with its own risks). Never collect on a lone timer: elapsed time is not evidence of delivery.

پیگیری (استف): زیر add-wins سبد شیری را نشان می‌دهد که کاربر صریح حذف کرده. در review چطور دفاع می‌کنی؟Follow-up (Staff): Under add-wins the cart shows milk the user explicitly deleted. How do you defend that in review?

با جدا کردن سازوکار از نیت: دستگاهی هم‌زمان بدون دیدن حذف شیر را دوباره اضافه کرده و انداختن بی‌صدا add کاربر هم غلط است. دفاع قرارداد صریح محصول است — add-wins یعنی add هم‌زمان حذف را می‌برد — به‌علاوه UX که آن را نشان دهد (شیر به‌عنوان دوباره‌اضافه‌شده با حذف یک‌ضربه‌ای). اگر محصول زنده شدن را تحمل نکند remove-wins را انتخاب کن و بگو، با قیمت خودش: addهای هم‌زمان ناپدید می‌شوند.

By separating mechanism from intent: some device re-added milk concurrently without seeing the deletion, and silently dropping a user's add is also wrong. The defense is the explicit product contract — add-wins means a concurrent re-add beats a remove — plus UX that surfaces it (show milk as re-added, offer one-tap remove-all). If the product cannot tolerate resurrection, choose remove-wins and say so, with its own cost: concurrent adds vanish.

اشتباهات رایجCommon mistakes

  • حذف کلید و اسمش را remove گذاشتن. بدون بایگانی tagهای دیده‌شده، sync بعدی با replicaای که هنوز آن‌ها را دارد آیتم را زنده می‌کند.
  • خواندن بزرگ‌ترین timestamp به‌عنوان مقدار درست. LWW نوشته‌ها را با ساعت‌های skewدار مرتب می‌کند؛ هم‌زمانی را تشخیص نمی‌دهد فقط بازمانده انتخاب می‌کند.
  • منقضی کردن tombstoneها با سن. زمان تحویل را ثابت نمی‌کند؛ فقط causal stability می‌گذارد tombstone بدون زنده شدن بازنشسته شود.
  • Deleting the key and calling it a remove. Without filing the seen tags, the next sync with a replica that still holds them resurrects the item.
  • Reading the biggest timestamp as the correct value. LWW orders writes by skewed clocks; it never detects concurrency, it just picks a survivor.
  • Expiring tombstones by age. Time does not prove delivery; only causal stability lets a tombstone retire without resurrection.

منابع:Sources: Preguiça, Baquero, Shapiro · CRDTs · 2018 · Atlassian · engineering interview handbook

33 تمرین ۳ — مرز تضمین‌های quorumExercise 3 — quorum guarantee boundaries

L3 · Staff / PrincipalL3 · Staff / Principalوابسته به نقشRole-specific45 دقیقه بحث45 min discussion

بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level

سؤال مصاحبهInterview question
با n=5,w=3,r=3 نشان بده چه تضمینی از تقاطع مجموعه‌ها داریم و چه تضمینی نداریم. partial write، lost ACK، stale replacement و تغییر membership را تحلیل کن. مانیتور staleness را با version، probe و divergence طراحی کن؛ failure را با rollback یکی ندان.
For n=5,w=3,r=3, explain quorum intersection and its limits. Analyze partial writes, lost ACKs, stale replacement and changing membership; define useful version probes and divergence monitoring.

پاسخ ۶۰ثانیه‌ایThe 60-second answer

اشتراک quorum از حساب مجموعه‌ها می‌آید: هر دو مجموعهٔ سه‌عضوی از پنج عضو دست‌کم یک عضو مشترک دارند. اما خواندن تازه به یک membership مشخص، دوام نوشته‌های ACKشده و قواعد مقایسهٔ نسخه و برخورد با نوشتن‌های هم‌زمان نیاز دارد. گم‌شدن ACK لزوماً به معنی گم‌شدن داده نیست. version probe، مجموعهٔ replicaهای عقب‌مانده و read-after-write probe را مانیتور کن؛ زمانِ کهنگی نیز با مرجع زمانی و عدم‌قطعیت تعریف‌شده قابل اندازه‌گیری است، نه با تبدیل بی‌دلیل فاصلهٔ نسخه به ثانیه.
Set arithmetic guarantees every two three-node sets among five intersect. Freshness additionally needs defined membership, durable acknowledged writes and version/concurrent-write rules. A lost ACK does not imply lost data. Monitor version probes, lagging replicas and read-after-write probes. Time-based staleness is possible with an explicit time reference and uncertainty; numeric version distance alone is not seconds.

صورت مسئله و فرض‌هاProblem and assumptions

  • پنج replica با w=3 و r=3؛ نسخه‌های v6/v7/v8 همه نوشته‌ها را مرتب می‌کنند و ترتیب لاگ نیست.
  • آهنگ probe (هر ۱۰ ثانیه) و کران‌ها آموزشی‌اند؛ اعداد پروداکشن از اندازه‌گیری می‌آیند.
  • membership ثابت است مگر در سناریوی rebalancing که مجموعه اعضا قدیم و جدید صریح نام برده می‌شوند.
  • Five replicas, w=3, r=3; versions v6/v7/v8 order all writes, no log order exists.
  • Teaching cadence for the probe (every 10 seconds) and teaching bounds; production numbers come from measurement.
  • Membership is fixed except in the rebalancing scenario, where old and new member sets are named explicitly.

چرا این روش و این عددها؟Why this method and these numbers?

از اصل شمول و عدم‌شمول داریم |W∩R|≥|W|+|R|−n؛ برای ۳+۳−۵، حداقل یک عضو مشترک است. این اثبات دربارهٔ مجموعه‌هاست؛ تضمین نمی‌کند عضو مشترک داده را واقعاً پایدار نگه داشته یا عضو جایگزین همان history را دارد. probe ده‌ثانیه‌ای انتخاب آموزشیِ هزینه در برابر سرعت کشف است: کوتاه‌تر، ترافیک بیشتر و تشخیص سریع‌تر. اختلاف نسخه را به ثانیه تبدیل نمی‌کنیم مگر نرخ تولید/مرجع زمانی و عدم‌قطعیت معلوم باشد.

Inclusion–exclusion gives |W∩R|≥|W|+|R|−n; 3+3−5 means at least one shared member. This set proof cannot establish durable contents or replacement history. A ten-second probe interval trades monitoring cost against detection delay: shorter costs more and detects sooner. Version distance cannot become seconds without a production-rate/time reference and uncertainty model.

مدل ذهنیMental model

دو کمیته سه‌نفره از پنج نفر را تصور کن: همیشه یک عضو مشترک دارند و آن عضو تنها پلی است که خبر می‌تواند از آن بگذرد. تازگی وجود پل نیست — به خاطر سپردن پل، رسیدن واقعی نامه‌ها، و کشیده شدن هر دو کمیته از همان پنج نفر است.
Picture two committees of three drawn from five people: they always share a member, and that member is the only bridge news can cross. Freshness is not the bridge existing — it is the bridge remembering, the letters actually arriving, and both committees drawn from the same five.

حل گام‌به‌گامStep by step

  1. بشمار جمع چه قولی می‌دهد حساب را بشمار: مجموعه‌های C(5,3) دوبه‌دو دست‌کم یک گره مشترک دارند پس quorum نوشتن و خواندن همیشه یک عضو پل مشترک دارند. نامساوی فقط همین را می‌گوید و بس.
  2. history خوش را برو history نسخه‌دار خوش را برو: v7 با سه ACK روی {n1,n2,n3} می‌نشیند؛ خواندن {n3,n4,n5} روی پل v7 و جای دیگر v6 می‌یابد و بیشینه را می‌گیرد. نسخه‌ها سوار نوشته‌اند؛ بیشینه از پل می‌گذرد.
  3. فرض‌ها را بشکن، نه جمع را هر فرض را به‌نوبت بشکن. ACK گمشده: v8 روی سه گره نشسته درحالی‌که هماهنگ‌کننده شکست گزارش می‌دهد — خواندن بعدی نوشته شکست‌خورده را برمی‌گرداند. جایگزینی کهنه: n3 از اسنپ‌شات v6 بازسازی می‌شود و همان هم‌پوشانی v6 حمل می‌کند و v8 از دست می‌رود. membership: خواندن از اعضای تازه هیچ‌وقت مجموعه نوشتن قدیم را ندیده — جمع روی دو پنج‌تای متفاوت حساب شده.
  4. نسخه probe کن، نه ثانیه بدون ترتیب لاگ مانیتور کن: هر پنج‌تا را برای تازه‌ترین نسخه probe کن، بیشینه و کمینه و مجموعه عقب‌مانده را گزارش بده و بعد از کران آموزشی با سناریوی منطبق page بزن. rebalancing جدا با joint quorum دیده‌بانی می‌شود.
  1. Count what the sum promises Count the arithmetic: C(5,3) sets pairwise intersect in at least one node, so write quorum and read quorum always share a bridge member. That is all the inequality ever says.
  2. Walk the happy history Walk the happy versioned history: v7 lands on {n1,n2,n3} with three ACKs; the read of {n3,n4,n5} finds v7 on the bridge and v6 elsewhere, and takes the max. Versions ride writes; the max crosses the bridge.
  3. Break the premises, not the sum Break each premise in turn. Lost ACKs: v8 sits on three nodes while the coordinator reports failure — the next read returns the failed write. Stale replacement: n3 is rebuilt from a v6 snapshot and the same overlap carries v6, missing v8. Membership: a read from new members never met the old write set — the sum was computed over two different fives.
  4. Probe versions, not seconds Monitor without a log order: probe all five for newest versions, report max, min and the behind-set, and page past a teaching bound with the matching scenario attached. Rebalancing is watched separately with joint quorums.

طراحی مشخصConcrete design

مانیتور کهنگی یک version prober نمونه‌بردار روی هر پنج replica است (آموزشی: هر ۱۰ ثانیه)، متریک max - min به‌علاوه مجموعه عقب‌مانده، داشبوردی که نسخه per-replica را نشان می‌دهد و پیجری که بعد از کران آموزشی با سناریوی منطبق (ACK گمشده، بازسازی یا جابه‌جایی) شلیک می‌کند. rebalancing دیده‌بان joint-quorum جدا دارد: quorumهای قدیم و جدید باید با هم هم‌پوشانی داشته باشند نه فقط با خودشان.

The staleness monitor is a sampled version prober over all five replicas (teaching: every 10 seconds), a metric of max - min plus the behind-set, a dashboard that shows versions per replica, and a pager that fires past a teaching bound with the matching scenario attached (lost ACK, rebuild, or move). Rebalancing gets a separate joint-quorum watch: old and new quorums must overlap each other, not just themselves.

گام 1 از 3 · خط فکریStep 1 of 3 · reasoning thread · اشتراک quorum نسخه را حمل می‌کندQuorum intersection carries the version · توالیsequenceباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
نوشتن v7 روی {n1,n2,n3} با سه ACK و بعد خواندن {n3,n4,n5}. بالا‌به‌پایین بخوان: پرده نوشتن، بعد پرده خواندن، با ملاقات در n3.
Write v7 to {n1,n2,n3} with three ACKs, then read {n3,n4,n5}. Read top to bottom: the write act, then the read act, meeting at n3.
  1. پیام‌های ۱ تا ۶: کلاینت v7 را به سه replica می‌نویسد و سه ACK جمع می‌کند — quorum نوشتن محقق شده.
  2. پیام‌های ۷ تا ۹: کلاینت سه‌مجموعه دیگری را می‌خواند؛ فقط n3 عضو هر دو است.
  3. پیام‌های ۱۰ تا ۱۲: n3 مقدار v7 می‌دهد و بقیه v6 پس خواندن بیشینه را می‌گیرد — تازگی از {n3} گذشته.
  1. Messages 1 to 6: the client writes v7 to three replicas and collects three ACKs — the write quorum is met.
  2. Messages 7 to 9: the client reads a different three-set; only n3 belongs to both.
  3. Messages 10 to 12: n3 answers v7 while the others answer v6, so the read takes the max — freshness crossed on {n3}.

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

رد خرابی: اگر نوشته فقط به دو replica می‌رسید، w=3 هیچ‌وقت محقق نمی‌شد و کلاینت retry می‌کرد — پل بدون سه ACKاش ساخته نمی‌شود.

تضمین نشان‌داده‌شده: شکل نشان می‌دهد هم‌پوشانی نسخه بیشینه را به خواندن می‌رساند. نشان نمی‌دهد هم‌پوشانی چه فرض‌هایی دارد.

تضمین نشان‌نداده‌شده: درباره ACK گمشده، بازسازی و membership ساکت است — آن‌ها مال نمودار بعدی‌اند.

Decision point: The decision point is the shared node: freshness crosses only where the two sets meet.

Failure trace: Had the write reached only two replicas, w=3 would never be met and the client would retry — the bridge never forms without its three ACKs.

Guarantee shown: The figure shows the overlap carrying the max version to the read. It does not show what the overlap assumes.

Not shown: It is silent on lost ACKs, rebuilds and membership — those are the next diagram.

خودآزماییSelf-check

کدام تک‌پیام تازگی را از مجموعه نوشتن به مجموعه خواندن می‌رساند؟

پاسخ: پیام ۱۰ (پاسخ v7 از n3): تنها خواندنی است که از داخل مجموعه نوشتن پاسخ گرفته.

Which single message carries freshness from the write set to the read set?

Answer: Message 10 (n3 answering v7): it is the only read answered from inside the write set.

گام 2 از 3 · خط فکریStep 2 of 3 · reasoning thread · همان حساب، تازگی شکستهSame arithmetic, broken freshness · توالیsequenceباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
همان n=5 و w=3 و r=3 در سه پرده: ACKهای گمشده، پل بازسازی‌شده از اسنپ‌شات قدیمی، و خواندن از اعضای تازه. حساب هیچ‌وقت تکان نمی‌خورد.
Same n=5, w=3, r=3 in three acts: ACKs lost, a bridge rebuilt from an old snapshot, and a read from new members. The arithmetic never moves.
  1. پرده یک: v8 به سه گره می‌رسد ولی دو ACK گم می‌شود پس هماهنگ‌کننده شکست گزارش می‌دهد — ولی خواندن بعدی v8 را برمی‌گرداند. شکست rollback نیست.
  2. پرده دو: n3 از اسنپ‌شات قدیمی v6 برگردانده می‌شود؛ هم‌پوشانی هنوز {n3} است ولی پل حالا v6 حمل می‌کند و v8 از دست می‌رود.
  3. پرده سه: quorum خواندن از اعضای تازه {n6..} هیچ‌وقت مجموعه نوشتن قدیم را ندیده — نامساوی روی دو membership متفاوت حساب شده.
  1. Act one: v8 reaches three nodes but two ACKs are lost, so the coordinator reports failure — yet the next read returns v8. Failure is not rollback.
  2. Act two: n3 is restored from an old v6 snapshot; the overlap is still {n3}, but the bridge now carries v6 and v8 is missed.
  3. Act three: a read quorum from new members {n6..} never met the old write set — the inequality was computed over two different memberships.

نقطهٔ تصمیم: نقطه تصمیم فرض شکسته هر پرده را نام می‌برد: durability ACKها، حافظه پل، هویت membership.

رد خرابی: هر فرض را روی فلش‌ها دنبال کن: ACKهایی که نرسیدند، پلی که فراموش کرد، اعضایی که هم را ندیدند. جمع‌ها هیچ‌وقت عوض نمی‌شوند؛ مدرک عوض می‌شود.

تضمین نشان‌داده‌شده: شکل سه راه مردن تازگی با حساب سالم را نشان می‌دهد. رتبه‌بندی‌شان نمی‌کند — تله‌متری پروداکشن می‌کند.

تضمین نشان‌نداده‌شده: درباره نحوه دیده‌بانی این‌ها چیزی نمی‌گوید — آن مال مانیتور اسمبل است.

Decision point: The decision point names the broken premise per act: durability of ACKs, memory of the bridge, identity of the membership.

Failure trace: Trace each premise on the arrows: ACKs that never arrive, a bridge that forgets, members that never met. The sums never change; the evidence does.

Guarantee shown: The figure shows three ways freshness dies with the arithmetic intact. It does not rank them — production telemetry does.

Not shown: It says nothing about how to watch for these — that is the assembly monitor.

خودآزماییSelf-check

کدام خط دمو w + r > n را می‌شکند؟

پاسخ: هیچ خطی: 3+3>5 در هر سه پرده برقرار است. تازگی روی فرض‌ها می‌شکند (durability ACK، حافظه پل، یک membership) هرگز روی جمع.

Which line of the demo breaks w + r > n?

Answer: No line: 3+3>5 holds in every act. Freshness breaks on the premises (ACK durability, bridge memory, one membership), never on the sum.

نمودار اسمبل نهاییFinal assembly diagram · مانیتور کهنگی بدون لاگStaleness monitor without a log · جریان دادهdataflowباز کردن در صفحه جدا ↗Open standalone ↗

دانلود نمودار مستقلDownload standalone diagram

روایت مدرس / چطور این نمودار را بخوانیمTeacher narration / how to read this diagram
ترتیب لاگ نیست پس prober با آهنگ آموزشی از هر replica تازه‌ترین نسخه‌اش را می‌پرسد. چپ‌به‌راست بخوان: probe، پاسخ‌ها، مقایسه، اقدام.
No log order exists, so a prober asks every replica for its newest version on a teaching cadence. Read left to right: probe, answers, compare, act.
  1. فلش‌های probe از replicaهای رسیده و عقب‌مانده یکسان تازه‌ترین نسخه را می‌پرسند — نسخه‌ها تنها ساعت اینجایند.
  2. v8 و v6 وارد متریک می‌شوند که بیشینه ۸ و کمینه ۶ و مجموعه عقب‌مانده را گزارش می‌دهد — واگرایی، نه ثانیه.
  3. داشبورد نسخه per-replica را نشان می‌دهد؛ پیجر بعد از کران آموزشی با سناریوی منطبق شلیک می‌کند.
  1. Probe arrows ask caught-up and lagging replicas alike for their newest version — versions are the only clock here.
  2. v8 and v6 flow into the metric, which reports max=v8, min=v6 and the behind-set — divergence, not seconds.
  3. The dashboard shows versions per replica; the pager fires past the teaching bound with the matching scenario attached.

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

رد خرابی: اگر خود prober پارتیشن شود، نبود پاسخ‌ها تازگی همگانی را تقلید می‌کند — مانیتور را با heartbeat دیده‌بانی کن وگرنه کهنگی دقیقاً وقتی تاریک می‌شود که مهم است.

تضمین نشان‌داده‌شده: شکل probe و مقایسه و paging را در یک حلقه سوار می‌کند؛ کران پروداکشن تعیین نمی‌کند.

تضمین نشان‌نداده‌شده: فقط membership پایدار را پوشش می‌دهد؛ rebalancing دیده‌بان joint-quorum جدا از بخش طراحی می‌خواهد.

Decision point: The decision point is the metric node: max minus min plus the behind-set replaces the missing log order.

Failure trace: If the prober itself is partitioned, absence of answers mimics universal freshness — monitor the monitor with a heartbeat, or staleness goes dark exactly when it matters.

Guarantee shown: The figure composes probing, comparing and paging into one loop; it sets no production bound.

Not shown: It covers steady membership only; rebalancing needs the joint-quorum watch from the design.

خودآزماییSelf-check

probe یک v8 و چهار نسخه قدیمی‌تر برمی‌گرداند. مانیتور چه گزارش می‌دهد؟

پاسخ: بیشینه ۸، کمینه ۶، عقب‌مانده‌ها {n3 با ۶ و n4/n5 با ۷}: واگرایی در نسخه‌ها، چون ثانیه ترتیب لاگی می‌خواهد که نیست.

The probe returns one v8 and four older versions. What does the monitor report?

Answer: max=v8, min=v6, behind={n3@n6, n4/n5@v7}: divergence in versions, because seconds would need a log order that does not exist.

گزینه‌های جایگزینAlternatives

  • read repair (بازگرداندن نسخه بیشینه به quorum پیش از بازگشت خواندن): عقب‌مانده‌ها را در مسیر خواندن شفا می‌دهد ولی repair پس‌زمینه تنها بدون قول زمان-خواندن linearizability نیست.
  • quorum نوشتن بزرگ‌تر (w=5): هر خواندن بعدی نوشته را می‌بیند به قیمت تأخیر و availability نوشتن — لاگ تک‌رهبر همان را ارزان‌تر می‌خرد.
  • Read repair (write back the max version to a quorum before returning): heals laggards on the read path, but a lone background repair without a read-time promise is not linearizability.
  • Bigger write quorum (w=5): every later read sees the write, at the price of write latency and availability — the single-leader log buys the same with less.

سناریوهای خرابیFailure scenarios

  • گزارش شکست هماهنگ‌کننده درحالی‌که نوشته durable نشسته (ACKهای گمشده): retry ساده‌لوح v9 را روی v8 می‌نویسد و نیت را دوشقه می‌کند. نگهبان: retryها را با نسخه fence کن — پیش از بازنویسی بخوان یا هویت نوشته را حمل کن.
  • replica بازسازی‌شده از اسنپ‌شات قدیمی برمی‌گردد و خواندن سرو می‌کند: هر quorum شامل آن نسخه‌های قدیمی‌تر گزارش می‌دهد. نگهبان: گره‌های بازسازی‌شده باید پیش از سرو از مرز تأییدشده جلو بزنند، پسرعموی بدون‌رهبر watermark حذف.
  • rebalancing هم‌زمان داده را جابه‌جا می‌کند درحالی‌که quorumها از هر دو membership کشیده می‌شوند: هیچ‌کدام با دیگری هم‌پوشانی ندارد. نگهبان: joint quorum — تا پایان جابه‌جایی هم‌پوشانی قدیم و جدید را لازم بگیر.
  • Coordinator reports failure while the write sits durable (lost ACKs): a naive retry writes v9 over v8 and forks intent. Guard: fence retries with versions — read before rewriting, or carry the write's identity.
  • Replica rebuilt from an old snapshot rejoins and serves reads: every quorum containing it reports older versions. Guard: rebuilt nodes must catch up past the acknowledged frontier before serving, the leaderless cousin of the deletion watermark.
  • Concurrent rebalancing moves data while quorums are drawn from both memberships: neither quorum overlaps the other. Guard: joint quorums — require overlap between old and new sets until the move 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: n=5, w=3, r=3. The ARITHMETIC (any two 3-sets of 5 overlap)
// never breaks; the FRESHNESS inference breaks when its assumptions do.
public class QuorumDemo {
    // replica -> newest version stored
    static int readMax(Set<String> quorum, Map<String, Integer> store) {
        int m = -1;
        for (String r : quorum) m = Math.max(m, store.getOrDefault(r, -1));
        return m;
    }

    public static void main(String[] a) {
        Map<String, Integer> store = new HashMap<>(Map.of(
                "n1", 6, "n2", 6, "n3", 6, "n4", 6, "n5", 6));
        // Happy path: write v7 to {n1,n2,n3} (w=3 ACKs), read {n3,n4,n5} -> overlap {n3} -> v7.
        store.put("n1", 7); store.put("n2", 7); store.put("n3", 7);
        System.out.println("happy path: read{n3,n4,n5} sees v" + readMax(Set.of("n3", "n4", "n5"), store)
                + " (overlap {n3} carries v7)");

        // Assumption 1: ACK set = durable set. v8 reaches {n1,n2,n3} but two ACKs are lost,
        // so the coordinator reports FAILURE while v8 sits on three nodes.
        store.put("n1", 8); store.put("n2", 8); store.put("n3", 8);
        System.out.println("lost ACKs: coordinator reports v8 FAILED, yet read{n3,n4,n5} sees v"
                + readMax(Set.of("n3", "n4", "n5"), store) + " (failure is not rollback)");

        // Assumption 2: overlapping node still holds what it ACKed. n4/n5 caught up
        // to v7 via read repair; then n3 is rebuilt from an old snapshot (back to
        // v6) and a read quorum {n3,n4,n5} returns v7, missing v8.
        store.put("n4", 7); store.put("n5", 7);
        store.put("n3", 6);
        System.out.println("stale replacement: n3 rebuilt from old snapshot -> read{n3,n4,n5} sees v"
                + readMax(Set.of("n3", "n4", "n5"), store) + ", v8 missed despite w+r>n");

        // Assumption 3: same membership. Data rebalances to {n4..n8}; a read quorum drawn
        // from the NEW members {n6,n7,n8} never intersects the old write set.
        Set<String> oldWrite = Set.of("n1", "n2", "n3"), newRead = Set.of("n6", "n7", "n8");
        Set<String> overlap = new HashSet<>(oldWrite); overlap.retainAll(newRead);
        System.out.println("membership change: old-write vs new-read overlap = " + overlap
                + " (inequality was computed over different member sets)");

        // Monitor without a log order: probe versions, report max/min + laggards.
        Map<String, Integer> probe = Map.of("n1", 8, "n2", 8, "n3", 6, "n4", 7, "n5", 7);
        int mx = Collections.max(probe.values()), mn = Collections.min(probe.values());
        List<String> behind = new ArrayList<>();
        for (var e : probe.entrySet()) if (e.getValue() < mx) behind.add(e.getKey() + "@v" + e.getValue());
        Collections.sort(behind);
        System.out.println("staleness probe: max=v" + mx + " min=v" + mn + " behind=" + behind);
        System.out.println("OK: 3+3>5 held in every line above; assumptions did the breaking");
    }
}

اجرا: javac QuorumDemo.java && java QuorumDemo — فقط استاندارد جاوا، فوری.

خروجی مورد انتظار: مسیر خوش‌بینانه v7 را از {n3} می‌بیند؛ ACKهای گمشده v8 شکست‌خورده ولی خواندنی را نشان می‌دهند؛ جایگزینی کهنه v8 را از دست می‌دهد؛ هم‌پوشانی membership خالی است؛ probe بیشینه/کمینه/عقب‌مانده را گزارش می‌دهد؛ OK پایانی. (خروجی واقعی همین محیط)

Run: javac QuorumDemo.java && java QuorumDemo — stdlib Java only, instant.

Expected output: happy path sees v7 via {n3}; lost ACKs show a FAILED v8 still readable; stale replacement misses v8; membership overlap is empty; probe reports max/min/behind; closing OK. (actual output of this environment)

آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria

در کلاینت شبیه‌سازی‌شده ACK گمشده و تأخیر تصادفی تزریق کن؛ assert کن نوشته شکست‌خورده گزارش‌شده هنوز بالا می‌آید؛ گرهی را از اسنپ‌شات قدیمی بازسازی کن و assert کن probe واگرا می‌شود؛ وسط تست membership را عوض کن و assert کن هم‌پوشانی joint-quorum هست.
Inject lost ACKs and random delays in the simulated client; assert a reported-failed write still surfaces; rebuild a node from an old snapshot and assert the probe diverges; change membership mid-test and assert joint-quorum overlap.

پاسخ سؤال‌های پیگیریFollow-up answers

پیگیری (استف): در rebalancing هم‌زمان، quorum قدیم و جدید چگونه باید هم‌پوشانی داشته باشند؟Follow-up (Staff): How should old and new quorums overlap during concurrent rebalancing?

باید با هم هم‌پوشانی داشته باشند نه فقط با خودشان: هنگام جابه‌جایی quorumهایی بخواه که هم مجموعه قدیم و هم جدید را قطع کنند (joint quorum / اکثریت توسعه‌یافته) تا هیچ نوشته‌ای جایی ننشیند که خواندن هیچ‌وقت نگاهش نکند. خواندن و نوشتن در گذار کار می‌کنند به قیمت quorumهای بزرگ‌تر؛ فقط وقتی هر replica membership تازه را تأیید کرد مجموعه قدیم بازنشسته می‌شود.

They must overlap each other, not just themselves: during the move, require quorums that intersect both the old and the new member set (joint quorums / expanded majorities), so no write lands where a read will never look. Reads and writes keep working through the transition at the price of bigger quorums; only after every replica acknowledges the new membership may the old set retire.

پیگیری (استف): آیا read repair پس‌زمینه به‌تنهایی linearizability می‌دهد؟Follow-up (Staff): Does background read repair alone give linearizability?

نمی‌دهد: repair نسخه بیشینه را به بیرون کپی می‌کند که عقب‌مانده‌ها را شفا می‌دهد ولی درباره مجموعه خواندن بعدی قولی نمی‌دهد. linearizability می‌خواهد خود خواندن با نوشته تازه قطع کند (یا token fencing در مسیر خواندن)؛ کپی پس‌زمینه بهداشت است نه تضمین.

It does not: repair copies the max version outward, which heals laggards but promises nothing about the next read's set. Linearizability needs the read itself to intersect a fresh write (or a fencing token on the read path); background copying is hygiene, not a guarantee.

اشتباهات رایجCommon mistakes

  • خواندن w + r > n به‌عنوان قول تازگی. آن گره مشترک را قول می‌دهد؛ تازگی اضافه‌تر durability ACK، حافظه پل و یک membership می‌خواهد.
  • برخورد با شکست گزارش‌شده مثل rollback. نوشته شکست‌خورده شاید روی سه گره durable نشسته و در خواندن بعدی بالا بیاید — retry را بر همان اساس با نسخه انجام بده.
  • سنجش کهنگی با ثانیه. بدون ترتیب لاگ ساعت سراسری نیست که از آن عقب باشی؛ نسخه‌ها و مجموعه‌های عقب‌مانده واحدهای صادق‌اند.
  • Reading w + r > n as a freshness promise. It promises an intersecting node; freshness additionally needs ACK durability, bridge memory and one membership.
  • Treating a reported failure as a rollback. A failed write may sit durable on three nodes and surface in the next read — retry accordingly, with versions.
  • Measuring staleness in seconds. Without a log order there is no global clock to be seconds behind; versions and behind-sets are the honest units.

منابع:Sources: Attiya, Bar-Noy, Dolev · atomic registers · 1995 · Jepsen · consistency models