11 تمرین ۱ — شناسهٔ سرتاسری در عملExercise 1 — End-to-end IDs in practice
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- دامنه تکدفترکل: هر دو حساب در یک پایگاه دادهاند، پس یک تراکنش میتواند درخواست و حرکتها و لاگ را bind کند. legهای بینسیستمی تمرین ۳ است.
- اعداد آموزشی: A=1000 و B=500 و req-7 دویست واحد میبرد. اعداد کوچک بررسی replay را خوانا نگه میدارند؛ بنچمارک نیستند.
- تحویل حداقل-یکبار: پاسخ میتواند گم شود، کلاینتها با همان شناسه retry میکنند و رسیدن همزمان تکراریها ممکن است.
- یک شناسه بهازای هر نیت کاربر، ساخته کلاینت (UUID). سرور برای retryهای کلاینت شناسه نمیسازد.
- Single-ledger scope: both accounts live in one database, so one transaction can bind request, moves and log. Cross-system legs are exercise 3.
- Teaching numbers: A=1000, B=500, req-7 moves 200. Small integers keep the replay check readable; they are not benchmarks.
- At-least-once delivery: replies can be lost, clients retry with the same ID, and concurrently arriving duplicates are possible.
- One ID per user intent, generated by the client (UUID). The server never invents IDs for client retries.
چرا این روش و این عددها؟Why this method and these numbers?
request_id هویت intent کاربر است، پس در retry ثابت میماند؛ hash payload جلوی استفادهٔ همان ID برای intent متفاوت را میگیرد. دو حساب در یک DB هستند تا debit و credit و نتیجه در یک تراکنش commit شوند؛ جمع ۱۰۰۰+۵۰۰=۱۵۰۰ باید پس از انتقال ۲۰۰ ثابت بماند. این اعداد کوچک مشاهدهٔ invariant را ساده میکنند. دو DB مستقل همین مرز اتمیک را ندارند و به طراحی تمرین چندshardی نیاز دارند.
A request ID identifies user intent and stays fixed across retries; a payload hash detects reusing it for a different intent. Both accounts share a database so debit, credit and result commit together. The initial 1000+500=1500 remains conserved after transferring 200; small numbers make the invariant inspectable. Independent databases do not share this atomic boundary.
مدل ذهنیMental model
حل گامبهگامStep by step
- اول INSERT، یکبار commit API یک تراکنش باز میکند، اول سطر درخواست را INSERT میکند، بعد بدهی و بستانکاری و سطر لاگ را میافزاید و commit میکند. یکتایی و حرکت پول و لاگ یک سرنوشت اتمی دارند.
- پاسخ گمشده میشود lookup commit موفق شد ولی پاسخ روی سیم مرد. کلاینت با همان شناسه و payload retry میکند؛ conflict یکتایی به سطر ذخیرهشده میرسد که نتیجهاش عیناً برمیگردد. همان شناسه با payload عوضشده رد میشود؛ همان payload با شناسه تازه بهعنوان نیت تازه پذیرفته میشود.
- replay دفترکل را اثبات میکند یک مصرفکننده جدا فقط ستون لاگ ورودی را میخواند و هر بالانس را بازمحاسبه میکند. اگر replay در آفست یکسان با حالت زنده فرق کند، آن باگ projection است نه lag — خط OK پایانی دمو دقیقاً همین بررسی است.
- INSERT first, commit once The API opens one transaction, INSERTs the request row first, then debits, credits and appends the log row, then commits. Uniqueness, money movement and the log share one atomic fate.
- Lost reply becomes a lookup Commit succeeded but the reply died on the wire. The client retries with the same ID and payload; the UNIQUE conflict routes to the stored row, whose result is returned verbatim. Same ID with a changed payload is rejected; the same payload under a new ID is honored as a new intent.
- Replay proves the ledger A separate consumer reads only the input-log column and recomputes every balance. If replay at the same offset ever differs from live state, that is a projection bug, not lag — the demo's final OK line is exactly this check.
طراحی مشخصConcrete design
جدولها: requests(request_id PK, payload, status, result) و accounts(id, balance) که ستون درخواستها هم نقش لاگ ورودی را دارد. API: POST /transfers {request_id, from, to, amount} با 200 {result} و 409 برای استفاده دوباره شناسه با payload متفاوت و 202 pending وقتی دوقلوی همزمان هنوز در حال کار است. مسیر نوشتن: BEGIN، درج سطر درخواست، چک موجودی، UPDATE هر دو حساب، ذخیره نتیجه، COMMIT — یک تراکنش. مسیر خواندن: خواندنهای صورتحساب از نمای بالانسِ بازسازیشده از لاگ میآیند؛ ممکن است با تأخیر کراندار و اندازهگیریشده از commit عقب باشند.
Tables: requests(request_id PK, payload, status, result), accounts(id, balance), and the requests column doubling as the input log. API: POST /transfers {request_id, from, to, amount} returns 200 {result}, 409 on ID reuse with a different payload, 202 pending while a concurrent twin is still working. Write path: BEGIN, INSERT request row, check funds, UPDATE both accounts, result stored, COMMIT — one transaction. Read path: statement reads come from the balance view rebuilt from the log; they may lag the commit by a bounded, measured delay.
مثال محاسبهشدهWorked example
اجرای آموزشی با A=1000 و B=500. req-7 دویست واحد میبرد: commit پاسخ میدهد (بعد پاسخ انداخته میشود) و retry با DEDUP برمیگردد و بالانسها همان ۸۰۰/۷۰۰ میمانند. استفاده دوباره req-7 برای ۹۹۹ رد میشود. req-8 تازه با همان payload دویست پذیرفته میشود: بالانسها ۶۰۰/۹۰۰. replay مستقل از لاگ هم ۶۰۰/۹۰۰ میدهد — برابر، پس OK.
Teaching run with A=1000, B=500. req-7 moves 200: commit replies (then the reply is dropped), retry returns DEDUP with balances still 800/700. Reusing req-7 for 999 is REJECTED. A fresh req-8 with the same 200 payload is honored: balances 600/900. Independent replay from the log gives 600/900 — equal, so OK.
دانلود نمودار مستقلDownload standalone diagram
- بالا: POST با id=req-7 به API میرسد؛ API در یک تراکنش سطر درخواست را INSERT میکند، از A کم و به B اضافه میکند و سطر لاگ را میافزاید و بعد commit میکند.
- پاسخ 200 گم میشود: کلاینت commit را از شکست تشخیص نمیدهد، پس تنها حرکت امن retry با همان شناسه و همان payload است.
- INSERTِ retry به conflict یکتایی میخورد؛ API سطر SUCCESS ذخیرهشده را میخواند و همان نتیجه را برمیگرداند — بالانسها دوباره تکان نمیخورند.
- Top: POST id=req-7 reaches the API; inside one transaction the API INSERTs the request row, debits A, credits B and appends the log row, then commits.
- The 200 OK is LOST: the client cannot tell commit from failure, so the only safe move is retrying with the SAME id and SAME payload.
- The retry's INSERT hits the UNIQUE conflict; the API reads the stored SUCCESS row and returns the same result — balances do not move again.
نقطهٔ تصمیم: قید UNIQUE retry را به lookup کلید تبدیل میکند: conflict یعنی سطر ذخیرهشده را بخوان، هرگز به بالانسها دست نزن.
رد خرابی: باگ کلاینت را روی همان شکل دنبال کن: اگر retry شناسه تازه میآورد، INSERT دوم موفق میشد و پول دوبار میرفت — دفترکل وقتی کلاینت کلید را عوض کند retry را از نیت تازه تشخیص نمیدهد. payload یکسان کلید تکراریابی نیست؛ شناسه است.
تضمین نشاندادهشده: شکل نشان میدهد یک شناسه در ثبت و گمشدن و retry یک بار اعمال میشود. جلوی پرداخت عمدی دوبار با دو شناسه را نمیگیرد.
تضمین نشانندادهشده: درباره legهای بینسیستمی (تمرین ۳) و اینکه نمای بالانس چقدر میتواند عقب باشد چیزی نمیگوید — تازگی قرارداد جداست.
Decision point: The UNIQUE constraint turns a retry into a key lookup: conflict means read the stored row, never touch the balances.
Failure trace: Trace the client bug on the same figure: if the retry carried a NEW id, the second INSERT would succeed and money would move twice — the ledger cannot tell a retry from a new intent when the client changes the key. Same payload is not the dedup key; the ID is.
Guarantee shown: The figure shows one ID applied once across submit, loss and retry. It does not stop a user from paying twice on purpose with two IDs.
Not shown: It says nothing about cross-system legs (exercise 3) or how far behind the balance view may lag — freshness is a separate contract.
خودآزماییSelf-check
یک retry میرسد درحالیکه تلاش اول ممکن است commit شده باشد یا نه. API قبل از دست زدن به بالانسها چه میکند؟
پاسخ: قبل از دست زدن به بالانسها سطر درخواست را INSERT (یا conflict-check) کن؛ روی conflict برای payload یکسان نتیجه ذخیرهشده را برگردان یا payload عوضشده را رد کن.
A retry arrives while the first attempt may or may not have committed. What does the API do before touching balances?
Answer: INSERT (or conflict-check) the request row before touching balances; on conflict, return the stored result for the same payload or reject a changed payload.
دانلود SVG مستقلDownload standalone SVG
- بالا: ثبت، commit موفق، پاسخ گمشده، retry با همان شناسه و payload، و نتیجه ذخیرهشده بدون حرکت پول.
- وسط: A موقع commit از ۱۰۰۰ به ۸۰۰ میرود و در retry همان ۸۰۰ میماند — پله دوم یعنی خرج دوباره.
- پایین: شناسه یکسان با payload یکسان dedup میشود؛ شناسه یکسان با payload تازه رد میشود؛ شناسه تازه با payload یکسان انتقال تازه است.
- Top: submit, commit ok, reply LOST, retry with the same id and payload, stored result returned with no money moved.
- Middle: A steps 1000 to 800 at the commit and stays 800 through the retry — a second step here would mean a double-spend.
- Bottom: same id plus same payload dedups; same id plus new payload is rejected; new id plus same payload is a new transfer.
نقطهٔ تصمیم: لاین تخت وسط همان اثبات است: بعد از پله commit، هر retry خط بالانس را افقی نگه میدارد.
رد خرابی: retry با شناسه تازه را روی لاین وسط دنبال کن: بالانس پله دوم تا ۶۰۰ میرفت — نمودار باگ کلیدی سمت کلاینت را بهصورت پله دوم نمایان میکند نه پنهان در متن.
تضمین نشاندادهشده: شکل اثر دقیقاً-یکبار یک کلید در گمشدن و retry را نشان میدهد. نمیگوید کلاینت چقدر صبر میکند بعد retry میزند.
تضمین نشانندادهشده: درباره رسیدن همزمان مسابقهای همان شناسه چیزی نمیگوید — آن سریالسازی مال شکل توالی و پاسخ پیگیری است.
Decision point: The flat middle lane is the proof: after the commit step, every retry leaves the balance line horizontal.
Failure trace: Trace a new-ID retry on the middle lane: the balance would step a second time to 600 — the chart makes a client-side key bug visible as a second step instead of hiding it in prose.
Guarantee shown: The figure shows exactly-once effect for one key across loss and retry. It does not bound how long the client waits before retrying.
Not shown: It says nothing about concurrent same-ID arrivals racing each other — that serialization belongs to the sequence figure and the follow-up answer.
خودآزماییSelf-check
کدام لاین شکل اثر دقیقاً-یکبار را اثبات میکند و نقضش روی آن چه شکلی است؟
پاسخ: لاین وسط بالانس: یک پله موقع commit و تخت در همه retryها. هر پله دوم همان خرج دوباره نمایانشده است.
Which lane of the figure proves the exactly-once effect, and what would a violation look like on it?
Answer: The middle balance lane: one step at commit, flat through every retry. Any second step is the double-spend made visible.
دانلود نمودار مستقلDownload standalone diagram
- کلاینت به API: ثبت با retryهای همان شناسه؛ API به دفترکل: یک تراکنش اتمی درخواست با حرکتها با لاگ؛ دفترکل به API: commit موفق یا سطر ذخیرهشده روی conflict.
- دفترکل به کار replay: سطرهای لاگ بهترتیب؛ کار به نمای بالانس: بالانسهای بازمحاسبهشده فقط از لاگ.
- نمای بالانس به خواندنهای صورتحساب: خواندنهای سروشده که ممکن است با تأخیر کراندار از commit عقب باشند — تازگی اندازهگیری میشود نه فرض.
- Client to API: POST plus same-id retries; API to Ledger: one atomic txn of request plus moves plus log; Ledger back: commit ok or the stored row on conflict.
- Ledger to Replay job: log rows in order; job to Balance view: recomputed balances from the log alone.
- Balance view to statement reads: served reads that may lag the commit by a bounded delay — freshness is measured, not assumed.
نقطهٔ تصمیم: یک commit اتمی و دو مصرفکننده: پاسخ کلاینت سطر ذخیرهشده را میخواند و کار replay سطرهای لاگ را. هیچکدام ورودی تازه نمینویسند.
رد خرابی: ناسازگاری replay را روی همان شکل دنبال کن: اگر خروجی کار replay در یک آفست یکسان لاگ با دفترکل فرق کند، عیب باگ projection است نه lag — lag فقط نمای عقبمانده ولی سازگار را توضیح میدهد و تابلوی lag در تمرین ۴ دقیقاً همان را میپاید.
تضمین نشاندادهشده: شکل نشان میدهد یک commit دو مصرفکننده مستقل را تغذیه میکند. تازه بودن نمای بالانس را قول نمیدهد — خواندنها ممکن است عقب باشند.
تضمین نشانندادهشده: درباره مقیاس مصرفکننده replay یا توافق دفترکلهای سیستمهای مختلف چیزی نمیگوید — پارتیشنبندی و چندshardی تمرینهای بعدیاند.
Decision point: One atomic commit, two consumers: the client reply reads the stored row, the replay job reads the log rows. Neither path writes fresh input.
Failure trace: Trace replay mismatch on the same figure: if the Replay job's output ever differs from the Ledger at the same log offset, the fault is a projection bug, not lag — lag only explains a behind-but-consistent view, and the lag board in exercise 4 watches exactly that.
Guarantee shown: The figure shows one commit feeding two independent consumers. It does not promise the balance view is fresh — reads may lag.
Not shown: It says nothing about how the replay consumer scales or how ledgers in different systems agree — partitioning and multishard are later exercises.
خودآزماییSelf-check
دو مصرفکننده مستقل آن یک commit اتمی را نام ببر.
پاسخ: نتیجه کلاینت (از سطر درخواست ذخیرهشده) و کار replay (از سطرهای لاگ). ورودی تازه از هیچکدام وارد نمیشود.
Name the two independent consumers of the one atomic commit.
Answer: The client result (from the stored request row) and the replay job (from the log rows). Fresh input enters through neither.
گزینههای جایگزینAlternatives
- تکراریابی روی payload طبیعی (مبدأ، مقصد، مبلغ، پنجره): بدون تغییر کلاینت، ولی دو انتقال عمدی یکسان در یک پنجره به یکی فرومیروند — یک پرداخت واقعی بیصدا گم میشود. فقط وقتی نگه دار که تکرار هیچوقت مشروع نیست؛ پول از آنها نیست.
- commit دوفازی بین سرویس موجودی و دفترکل: هزینه هماهنگی برای مسئلهای که یک تراکنش پایگاه داده همینجا حلش میکند. فقط وقتی به 2PC ارتقا بده که legها واقعاً در سیستمهای مختلفاند — آن تمرین ۳ است نه این.
- Dedup on the natural payload (from, to, amount, window): no client change, but two intentional identical transfers inside one window collapse into one — silently dropping a real payment. Reserve for cases where repeats are never legitimate; money is not one of them.
- Two-phase commit across balance service and ledger: pays coordination cost for a problem one database transaction already solves here. Promote to 2PC only when legs genuinely live in different systems — that is exercise 3, not this one.
سناریوهای خرابیFailure scenarios
- رسیدن همزمان همان شناسه: هر دو INSERT مسابقه میدهند؛ قید یکتایی دقیقاً یکی را میپذیرد. بازنده باید سطر را بخواند — SUCCESS نتیجه ذخیرهشده را برمیگرداند و PENDING با ۲۰۲ و محل poll — و هرگز نباید انتقال دوم محاسبه کند.
- crash بین commit و پاسخ: در کلاینت از commit کند قابلتشخیص نیست. نگهبان همان retry با شناسه یکسان با نتیجه ذخیرهشده است؛ کلاینت تردید را با پرسوجوی شناسه درخواست حل میکند نه با ساختن شناسه تازه.
- lag یا توقف مصرفکننده replay: خوانندهها بالانس کهنه میبینند. نگهبان متریک lag کراندار با هشدار است؛ lag فقط نمای عقبمانده ولی سازگار را توضیح میدهد — اختلاف در آفست یکسان corruption است (تمرین ۴).
- Concurrent same-ID arrivals: both INSERTs race; the UNIQUE constraint admits exactly one. The loser must read the row — SUCCESS returns the stored result, PENDING returns 202 plus a poll location — and must never compute a second transfer.
- Crash between commit and reply: indistinguishable from a slow commit at the client. Guard is the same-ID retry plus the stored result; the client resolves doubt by querying the request ID, never by minting a new one.
- Replay consumer lag or stall: readers see stale balances. Guard with a bounded lag metric and alert; lag explains behind-but-consistent views only — same-offset differences are corruption (exercise 4).
کد قابل اجرا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: end-to-end request IDs (Example 13-2 pattern).
// One atomic commit binds UNIQUE(request_id) + debit/credit + input log.
// A lost commit reply + user retry must move money exactly once.
public class TransferDemo {
static class Row { String payload; String status; String result; Row(String p){payload=p;status="PENDING";} }
Map<String, Row> requests = new LinkedHashMap<>();
Map<String, Long> balances = new LinkedHashMap<>(Map.of("A", 1000L, "B", 500L));
List<String> inputLog = new ArrayList<>();
// Returns the stored/fresh result string. Simulates one DB transaction.
synchronized String submit(String id, String from, String to, long amount) {
String payload = from + "->" + to + ":" + amount;
Row old = requests.get(id);
if (old != null) {
if (!old.payload.equals(payload)) return "REJECTED: id reused with different payload";
return "DEDUP: " + old.result; // same id + same payload -> stored result
}
Row row = new Row(payload);
requests.put(id, row); // UNIQUE(request_id): a concurrent second insert fails here
if (balances.get(from) < amount) { row.status = "FAILED"; row.result = "insufficient funds"; inputLog.add(id + "=FAIL"); return row.result; }
balances.put(from, balances.get(from) - amount);
balances.put(to, balances.get(to) + amount);
row.status = "SUCCESS"; row.result = "moved " + amount + " " + from + "->" + to;
inputLog.add(id + "=" + payload);
return row.result;
}
// Independent consumer: rebuild balances ONLY from the input log.
static Map<String, Long> replay(List<String> log) {
Map<String, Long> b = new LinkedHashMap<>(Map.of("A", 1000L, "B", 500L));
for (String e : log) {
if (e.endsWith("=FAIL")) continue;
String p = e.substring(e.indexOf('=') + 1); // from->to:amount
String[] u = p.split("->|:");
b.put(u[0], b.get(u[0]) - Long.parseLong(u[2]));
b.put(u[1], b.get(u[1]) + Long.parseLong(u[2]));
}
return b;
}
public static void main(String[] a) {
TransferDemo db = new TransferDemo();
String r1 = db.submit("req-7", "A", "B", 200);
System.out.println("first submit = " + r1 + " (reply LOST on the wire)");
String r2 = db.submit("req-7", "A", "B", 200); // user retry, same id + same payload
System.out.println("retry same id+payload = " + r2);
System.out.println("reuse id, new payload = " + db.submit("req-7", "A", "B", 999));
System.out.println("same payload, new id = " + db.submit("req-8", "A", "B", 200) + " (independent transfer allowed)");
System.out.println("balances = " + db.balances + " (each id moved money exactly once)");
Map<String, Long> rebuilt = replay(db.inputLog);
System.out.println("replay from log = " + rebuilt);
System.out.println(rebuilt.equals(db.balances) ? "OK: replay matches live state" : "MISMATCH");
}
}
اجرا: javac TransferDemo.java && java TransferDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: DEDUP نتیجه ذخیرهشده را برمیگرداند و retry بالانس را عوض نمیکند؛ شناسه تکراری با payload تازه REJECT میشود؛ شناسه تازه با همان payload دوباره پول میبرد؛ replay برابر حالت زنده است (A=600 و B=900)؛ OK. (خروجی واقعی همین محیط)
Run: javac TransferDemo.java && java TransferDemo — stdlib Java only, instant.
Expected output: DEDUP returns the stored result with balances unchanged by the retry; reused ID with new payload is REJECTED; new ID with same payload moves money again; replay equals live (A=600, B=900); OK. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): دو درخواست همزمان با یک ID برسند؛ پاسخ موفق یا pending را چگونه تعیین میکنی؟Follow-up (Senior): Two requests with the same ID arrive concurrently. How do you return success or pending?
قید یکتایی آنها را سریال میکند: یک INSERT میبرد و انتقال را اجرا میکند؛ دیگری conflict کلید میگیرد و سطر را میخواند. اگر سطر از قبل SUCCESS میگوید نتیجه ذخیرهشده را برگردان. اگر هنوز PENDING میگوید (برنده وسط تراکنش است) ۲۰۲ در حال انجام با محل poll برگردان — و هرگز محاسبه دوم شروع نکن. حالت pending به timeout نیاز دارد: برنده crashکرده باید بالاخره سطرش را به FAILED بغلتاند تا منتظرها از poll دست بکشند.
The UNIQUE constraint serializes them: one INSERT wins and runs the transfer; the other gets a key conflict and reads the row. If the row already says SUCCESS, return the stored result. If it still says PENDING (the winner is mid-transaction), return 202 pending with a poll location — and never start a second computation. The pending state needs a timeout: a crashed winner must eventually roll its row to FAILED so waiters stop polling.
پیگیری (سینیور): کلاینت بعد از پاسخ گمشده هیچوقت retry نمیکند. پول گم شده؟Follow-up (Senior): The client never retries after a lost reply. Is money lost?
retry نمیکند؛ پول قبلاً رفته. کلاینت با خواندن شناسه درخواست (GET /transfers/{id}) یا صورتحسابش قطعیت را برمیگرداند: نتیجه ذخیرهشده همان رسید است. قاعده طراحی: هر پاسخ جهشدهنده شناسه درخواست را دارد تا پاسخ گمشده همیشه بدون نیت تازه قابلحل دوباره باشد.
It does not retry; the money already moved. The client recovers certainty by reading the request ID (GET /transfers/{id}) or its statement: the stored result is the receipt. Design rule: every mutating response includes the request ID, so a lost reply is always re-resolvable without a new intent.
اشتباهات رایجCommon mistakes
- دو UPDATE لخت بدون سطر درخواست: retry از انتقال تازه قابلتشخیص نیست، پس پول دوبار میرود. سطر درخواست کلید تکراریابی است نه تزئین.
- تکراریابی روی payload بهجای شناسه: انتقالهای عمدی یکسان بلعیده میشوند. payload یکسان مجاز است؛ شناسه یکسان یکبار است.
- برگرداندن خطا روی conflict یکتایی بهجای نتیجه ذخیرهشده: کلاینت «قبلاً انجام شده» را از «شکست خورده» تشخیص نمیدهد و شناسه تازه میسازد — و همان خرج دوبارهای را میسازد که قید برای جلوگیریش بود.
- Two bare UPDATEs with no request row: the retry cannot be distinguished from a new transfer, so money moves twice. The request row is the dedup key, not decoration.
- Deduping on the payload instead of the ID: identical intentional transfers get swallowed. Same payload is allowed; same ID is once.
- Returning an error on the UNIQUE conflict instead of the stored result: the client cannot tell 'already done' from 'failed' and mints a new ID — causing the very double-spend the constraint was meant to stop.
منابع:Sources: AWS · safe retries with idempotent APIs · PostgreSQL 18 · transaction isolation
22 تمرین ۲ — مهاجرتِ دوگانهExercise 2 — Dual migration
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- دامنه کاتالوگ: schema تخت قدیم (یک سطر بهازای هر محصول) در برابر schema تازه (سطر بهعلاوه سطر جزئیات). سه محصول آموزشی: چراغ ۳۰، صندلی ۱۲۰، میز ۲۵۰.
- مسیر نوشتن در مهاجرت هیچوقت جابهجا نمیشود: همه ورودی تازه هنوز در لاگ مشترک مینشیند و هر دو projection مستقل مصرفش میکنند.
- خوانندهها در canary هر دو schema را تحمل میکنند؛ flag درصد را تعیین میکند. rollback فقط تا وقتی ممکن میماند که نوشتنهای تازه old-readable بمانند.
- Catalog scope: flat old schema (one row per product) versus new schema (row plus details row). Three teaching products: Lamp 30, Chair 120, Desk 250.
- The write path never moves during migration: all fresh input still lands in the shared log, and both projections consume it independently.
- Readers tolerate either schema during canary; the flag decides the percent. Rollback stays possible only while new writes remain old-readable.
چرا این روش و این عددها؟Why this method and these numbers?
shared log همهٔ تغییرها را به دو projection میرساند تا dual write مستقل شکاف نسازد. مقایسه در checkpoint مشترک لازم است چون نمای عقبمانده با نمای خراب فرق دارد. درصد canary فرض مدیریت ریسک است: مرحله را وقتی افزایش بده که mismatch، lag و خطای هر cohort داخل معیار باشد؛ گذشتن چند دقیقه بهتنهایی کافی نیست. rollback فقط تا وقتی معتبر است که مسیر قدیم دادهٔ تازه را بفهمد؛ حذف زودهنگام فیلدها آن را میبندد.
A shared log feeds both projections without independent dual-write gaps. Compare at a common checkpoint to distinguish lag from corruption. Canary percentages manage exposure: advance only when mismatch, lag and per-cohort errors meet criteria, not merely after elapsed time. Rollback requires the old path to understand new data; early field removal closes that option.
مدل ذهنیMental model
حل گامبهگامStep by step
- backfill و بعد catch-up هر دو projector همان لاگ را از اولین آفست مصرف میکنند: اول تاریخچه انبوه و بعد catch-up جریانی تا نوک. نمای قدیم همهحال صددرصد خواندنها را سرو میدهد؛ هیچچیز مسیر نوشتن عوض نمیشود.
- سایه پیش از canary هر خواندن روی هر دو نما اجرا میشود ولی فقط جواب قدیم سروشده میشود؛ اختلافها بهعنوان mismatch سایه شمرده میشوند. درصد canary تا وقتی mismatch غیرصفر است یا lag هر نما از کرانش بگذرد صفر میماند — باگ افتادن جزئیات دمو دقیقاً همینجا میمیرد.
- canary و cutover و بازنشستگی دیر flag روی صفر mismatch و lag کراندار از ۵ به ۵۰ به ۱۰۰ میرود؛ هر پسرفتی آن را در یک حرکت به صددرصد قدیم برمیگرداند. بعد از معیارها بهعلاوه دوره نگهداشت در ۱۰۰٪، projector قدیم را متوقف کن و یک snapshot را در نگهداری نگه دار و بعد بازنشسته کن.
- Backfill, then catch up Both projectors consume the same log from the first offset: bulk history first, then streaming catch-up to the tip. The old view keeps serving 100% of reads the whole time; nothing about the write path changes.
- Shadow before canary Every read runs against both views but only the old answer is served; differences are counted as shadow mismatches. The canary percent stays 0 while mismatches are nonzero or either view's lag exceeds its bound — the demo's dropped-details bug dies exactly here.
- Canary, cutover, retire late The flag walks 5 to 50 to 100 on zero mismatches and bounded lag; any regression flips it to 100% old in one move. After criteria plus a hold period at 100%, stop the old projector, keep one snapshot through retention, then retire.
طراحی مشخصConcrete design
اجزا: یک لاگ ورودی فقطافزودنی؛ دو projector مستقل (تخت قدیم و شکافته تازه)؛ دو نما؛ یک flag ترافیک (درصد هر نما، قابلچرخش در یک انتشار). API: خواندنهای لیست محصول نسخه ندارند — مسیریاب نما را انتخاب میکند؛ نوشتنها همیشه به شکل لاگی میروند که هر دو projector میفهمند. معیارهای cutover: صفر mismatch سایه در یک چرخه کاری کامل، lag نمای تازه زیر SLO خواندن، بهعلاوه دوره نگهداشت در ۱۰۰٪ پیش از انداختن نمای قدیم. مالکیت: لاگ مالک مسیر نوشتن است؛ هیچ نمایی حق نوشتن مستقیم ندارد.
Components: one append-only input log; two independent projectors (old-flat, new split); two views; one traffic flag (percent per view, flippable in one deploy). API: product-list reads carry no version — the router picks the view; writes always target the log shape both projectors understand. Cutover criteria: zero shadow mismatches over a full business cycle, new-view lag under the read SLO, plus a hold period at 100% before the old view is dropped. Ownership: the log owns the write path; neither view may accept direct writes.
مثال محاسبهشدهWorked example
اجرای آموزشی با سه محصول. هر دو نما تا ۳ سطر backfill میشوند. مقایسه سایه ۲ mismatch مییابد (جزئیات p2 و p3 افتاده از باگ تزریقی)، پس canary صفر میماند. بعد از اصلاح projector و بازپخش همان لاگ، mismatchها صفر میخوانند و flag از ۵ به ۵۰ به ۱۰۰ میرود. تمرین rollback با یک flag به قدیم برمیگردد و هر ۳ سطر قدیم سالماند.
Teaching run with three products. Both views backfill to 3 rows. Shadow compare finds 2 mismatches (p2 and p3 details dropped by the injected bug), so the canary stays 0%. After fixing the projector and replaying the same log, mismatches read 0 and the flag walks 5 to 50 to 100. A rollback drill flips one flag back to old with all 3 old rows intact.
دانلود نمودار مستقلDownload standalone diagram
- Backfill تاریخچه را در نمای تازه بازپخش میکند و بعد CatchUp تا نوک مصرف میکند؛ نمای قدیم در همهحال صددرصد سرو میدهد.
- Shadow هر خواندن را در ترافیک زنده صفر مقایسه میکند؛ فقط صفر mismatch دروازه Canary را باز میکند که از ۵ به ۵۰ به ۱۰۰ میرسد.
- Cutover با معیار تصمیم میگیرد نه شجاعت؛ mismatch یا پسرفت SLO به Rollback میافتد که flag را میچرخاند و به RolledBack میرسد — اصلاح کن و بعد در Canary دوباره وارد شو، هرگز در Cutover.
- Backfill replays history into the new view, then CatchUp consumes to the tip; the old view serves 100% throughout.
- Shadow compares every read at 0% live traffic; only zero mismatches open the Canary, which grows 5 to 50 to 100.
- Cutover decides on criteria, not courage; mismatch or SLO regression drops to Rollback, which flips the flag and lands RolledBack — fix, then re-enter at Canary, never at Cutover.
نقطهٔ تصمیم: rollback یک جعبه است نه عذرخواهی: یک چرخش flag صددرصد خواندنها را به نمای قدیم برمیگرداند چون هیچوقت از سرو نایستاد.
رد خرابی: پرش از سایه را روی همان شکل دنبال کن: پریدن مستقیم از CatchUp به Canary یعنی اولین mismatch به کاربران واقعی میرسد — حالت انتظار Shadow هست تا باگها در ترافیک زنده صفر پیدا شوند، مثل باگ تزریقی جزئیات در دمو.
تضمین نشاندادهشده: شکل همه دروازههای جلو و خروجیهای rollback را نشان میدهد. درستی projectionها را اثبات نمیکند — فقط checker و مقایسه سایه میکنند.
تضمین نشانندادهشده: درباره نوشتنهایی که schema تازه میپذیرد ولی قدیم نمیتواند نمایش دهد چیزی نمیگوید — آن قاتل rollback در پاسخ پیگیری است.
Decision point: Rollback is a box, not an apology: one flag flip returns 100% of reads to the old view because it never stopped serving.
Failure trace: Trace a skipped shadow on the same figure: jumping from CatchUp straight to Canary means the first mismatch reaches real users — the Shadow wait state exists so bugs are found at 0% live, as the demo's injected details bug was.
Guarantee shown: The figure shows every forward gate and the rollback exits. It does not prove the projections are correct — only the checker and the shadow compare do.
Not shown: It says nothing about writes the new schema accepts but the old cannot represent — that rollback-killer is answered in the follow-up.
خودآزماییSelf-check
وقتی canary ناسازگار شود وارد کدام حالت میشویم و چه چیزی آن گذار را فوری میکند؟
پاسخ: Rollback: یک چرخش flag به نمای قدیم که فقط چون نمای قدیم در کل مهاجرت خواندن سرو داده ممکن است.
Which state is entered when the canary mismatches, and what makes that transition instant?
Answer: Rollback: one flag flip to the old view, which is possible only because the old view served reads through the whole migration.
دانلود نمودار مستقلDownload standalone diagram
- هر دو projector همان تاریخچه با نوک را بازپخش میکنند؛ هیچکدام نوشتن مستقیم نمیپذیرند — لاگ مالک مسیر نوشتن است.
- نمای قدیم و تازه کنار هم سرو میدهند؛ مسیریاب مقایسه میکند (سایه) و بعد درصدهای flagشده ۵ و ۵۰ و ۱۰۰ را شیفت میدهد.
- هر mismatch flag را به صددرصد قدیم برمیگرداند: rollback فوری است چون نمای قدیم و projectorش هیچوقت نایستادند.
- Both projectors replay the same history plus the tip; neither accepts direct writes — the log owns the write path.
- Old view and new view serve side by side; the router compares (shadow) and then shifts flagged percents 5, 50, 100.
- Any mismatch flips the flag to 100% old: rollback is instant because the old view and its projector never stopped.
نقطهٔ تصمیم: مسیر نوشتن هیچوقت شاخه نمیشود: یک لاگ داخل، دو projection بیرون، و فقط flag خواندن جابهجا میشود.
رد خرابی: باگ کاشتهشده را روی همان شکل دنبال کن: projector تازه جزئیات قیمت بالای ۱۰۰ را میاندازد، پس مقایسه سایه در مسیریاب روی p2 و p3 ناسازگار میشود — flag از صفر تکان نمیخورد و کاربران باگ را نمیبینند. بدون یال سایه، همان باگ سوار canary به پروداکشن میرفت.
تضمین نشاندادهشده: شکل مالکیت واحد نوشتن با خواندنهای مستقل دوگانه را نشان میدهد. بازنشستگی را نشان نمیدهد — نمای قدیم فقط بعد از دوره نگهداشت در چرخهحیات انداخته میشود.
تضمین نشانندادهشده: درباره سازگاری روبهجلوی نوشتنها در canary چیزی نمیگوید — پاسخ پیگیری میگوید rollback کی میمیرد.
Decision point: The write path never branches: one log in, two projections out, and only the read flag moves.
Failure trace: Trace the planted bug on the same figure: the new projector drops details for price > 100, so the shadow compare at the router mismatches on p2 and p3 — the flag never moves past 0%, and users never see the bug. Without the shadow edge, the same bug would ride the canary to production.
Guarantee shown: The figure shows single write ownership with dual independent reads. It does not show retirement — the old view is dropped only after the hold period in the lifecycle.
Not shown: It says nothing about forward-compatibility of writes during canary — the follow-up answers when rollback dies.
خودآزماییSelf-check
rollback هیچ دادهای جابهجا نمیکند. کدام جزء این را درست میکند؟
پاسخ: لاگ ورودی مشترک: برگرداندن خواندنها هیچ جابهجایی داده نمیخواهد چون projection قدیم همیشه بهروز مانده.
Rollback moves no data. Which component makes that true?
Answer: The shared input log: flipping reads back needs no data movement because the old projection stayed current all along.
گزینههای جایگزینAlternatives
- cutover یکباره (توقف نوشتن، مهاجرت، شروع دوباره): سادهترین برای استدلال، ولی قطعی نوشتن میخواهد و rollbackای جز برگشت از بکاپ ندارد. فقط برای کاتالوگهای کوچک با پنجره نگهداری اعلامشده قابلقبول است؛ وگرنه هزینه دو نما قیمت صادقانه دانتایم صفر است.
- بازنویسی درجا (ALTER TABLE با backfill درجا): نصف ذخیرهسازی، ولی خوانندهها schema نیمهمهاجرتشده میبینند و rollback یعنی بازنویسی دیگر. وقتی تغییر افزایشی و backward-readable است ترجیح بده؛ وقتی شکل تازه old-readable نیست نماهای دوگانه را نگه دار.
- Big-bang cutover (stop writes, migrate, restart): simplest to reason about, but needs a write outage and has no rollback except restoring from backup. Acceptable only for tiny catalogs with a declared maintenance window; otherwise the dual-view cost is the honest price of zero downtime.
- In-place row rewrite (ALTER TABLE + backfill in place): half the storage, but readers see a half-migrated schema and rollback means another rewrite. Prefer when the change is additive and backward-readable; keep dual views when the new shape is not old-readable.
سناریوهای خرابیFailure scenarios
- باگ در projection تازه (افتادن جزئیات): با مقایسه سایه در ترافیک زنده صفر گرفته میشود. نگهبان دروازه صفر-mismatch پیش از هر درصد canary است — دمو دقیقاً همین باگ را تزریق میکند.
- lag نمای تازه فراتر از SLO خواندن: خواندنهای canary سطرهای کهنه را مثل تازه سرو میکنند. نگهبان متریک lag کراندار هر نماست؛ تا هر نما عقب است flag را بلوکه کن.
- توقف بیصدای projector قدیم وسط مهاجرت: هدف rollback کهنه میشود. نگهبان projection هر دو نما از watermark نوک یکسان با هشدار عقبماندگی نمای قدیم است — rollback فقط تا وقتی فوری است که بهروز باشد.
- Bug in the new projection (dropped details): caught by shadow compare at 0% live. Guard is zero-mismatch gating before any canary percent — the demo injects exactly this bug.
- New-view lag beyond the read SLO: canary reads would serve stale rows as if they were fresh. Guard with a bounded lag metric per view; block the flag while either view is behind.
- Old projector silently stalls mid-migration: rollback target goes stale. Guard by projecting both views from the same tip watermark and alerting when the old view falls behind — rollback is only instant while it is current.
کد قابل اجرا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: dual-schema migration from one shared input log.
// Old flat projection vs new (table + details) projection, % traffic,
// shadow compare, deliberate bug in the new projection, rollback.
public class MigrationDemo {
record Upsert(String id, String name, long price, String detail) {}
// old view: flat row per product. new view: row + details row.
Map<String, String> oldView = new LinkedHashMap<>();
Map<String, String> newView = new LinkedHashMap<>();
Map<String, String> newDetails = new LinkedHashMap<>();
List<Upsert> log = new ArrayList<>(List.of(
new Upsert("p1", "Lamp", 30, "warm LED"),
new Upsert("p2", "Chair", 120, "oak"),
new Upsert("p3", "Desk", 250, "standing")));
void projectOld(Upsert u) { oldView.put(u.id, u.name + "|" + u.price); }
void projectNew(Upsert u, boolean buggy) {
newView.put(u.id, u.name + "|" + u.price);
// BUG (injected): details dropped when price > 100
if (!(buggy && u.price > 100)) newDetails.put(u.id, u.detail);
}
public static void main(String[] a) {
MigrationDemo m = new MigrationDemo();
// backfill + catch-up: both projections consume the same log
for (Upsert u : m.log) { m.projectOld(u); m.projectNew(u, true); }
System.out.println("backfill done: old=" + m.oldView.size() + " new=" + m.newView.size());
// shadow read: compare every read against the old view (0% live traffic yet)
int mism = 0;
for (String id : m.oldView.keySet())
if (!m.newDetails.containsKey(id)) { mism++; System.out.println("shadow mismatch: " + id + " details missing in new view"); }
System.out.println("shadow mismatches = " + mism + " -> canary BLOCKED, stays 0%");
// fix projection, re-project from the same log (replay, not patch)
m.newView.clear(); m.newDetails.clear();
for (Upsert u : m.log) m.projectNew(u, false);
mism = 0;
for (String id : m.oldView.keySet()) if (!m.newDetails.containsKey(id)) mism++;
System.out.println("after fix: mismatches = " + mism + " -> canary 5% -> 50% -> 100%");
// rollback drill: flag back to 100% old; old view untouched throughout
String flag = "new";
flag = "old"; // one flag flip
System.out.println("rollback: traffic flag = " + flag + " (old view served reads all along: " + m.oldView.size() + " rows)");
System.out.println("OK: write path never moved; old view retired only after cutover criteria held");
}
}
اجرا: javac MigrationDemo.java && java MigrationDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: backfill سهبهعلاوهسه؛ سایه ۲ mismatch مییابد (جزئیات p2 و p3) پس canary صفر میماند؛ بعد از اصلاح mismatchها صفر و canary جلو میرود؛ rollback با یک flag و نمای قدیم سالم؛ OK. (خروجی واقعی همین محیط)
Run: javac MigrationDemo.java && java MigrationDemo — stdlib Java only, instant.
Expected output: Backfill 3+3; shadow finds 2 mismatches (p2, p3 details) so canary stays 0%; after fix mismatches are 0 and canary proceeds; rollback flips one flag with the old view intact; OK. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (استف): کاربری بعد از خواندن schema جدید دادهای بنویسد که نسخه قدیم نمیفهمد؛ rollback هنوز ممکن است؟Follow-up (Staff): A client writes data the old schema cannot represent after reading the new schema. Is rollback still possible?
نه همیشه. اگر کلاینت فقط schema تازه را خوانده باشد rollback هنوز یک چرخش flag است. ولی اگر دادهای نوشته که schema قدیم نمیتواند نمایش دهد (فیلد اجباری تازه یا شکافی که سطر تخت نمیگیرد)، برگشتن یا آن داده را میاندازد یا غلط سرو میکند. نگهبان: در canary نوشتنها باید old-readable بمانند (فیلدهای تازه اختیاری و نادیدهگرفتهشده توسط projector قدیم) یا کار forward-fill باشد که شکل تازه را پیش از rollback به نمای قدیم برگرداند. اگر هیچکدام نیست صریح بگو: rollback از آن نوشتن به بعد خراب است و تنها راه برگشت اصلاح روبهجلوست.
Not always. If the client only READ the new schema, rollback is still one flag flip. But if it WROTE data the old schema cannot represent (a new required field, a split the flat row cannot hold), flipping back either drops that data or serves it wrongly. Guard: during canary, writes must stay old-readable (new fields optional/ignored by the old projector), or carry a forward-fill job that backfills the new shape into the old view before any rollback. If neither holds, say so: rollback is broken from that write on, and the only way back is forward-fix.
پیگیری (استف): دقیقاً کی نمای قدیم را حذف میکنی؟Follow-up (Staff): When exactly do you delete the old view?
بعد از پایداری معیارها: خواندن صددرصد تازه در یک چرخه کاری کامل با صفر mismatch و lag داخل SLO و بدون rollback در پنجره نگهداشت. بعد projector قدیم را متوقف کن و آخرین snapshotش را یک دوره نگهداری دیگر نگه دار (بیمه ارزان) و فقط بعد حذف کن. بازنشستگی فقط با درصدهای canary — بدون نگهداشت — اگر باگ کندسوز یک هفته بعد سر بزند تو را بیپناه میگذارد.
After the criteria hold: 100% new reads for a full business cycle with zero mismatches, lag inside SLO, and no rollback in the hold window. Then stop the old projector, keep its last snapshot for one more retention period (cheap insurance), and only then delete. Retiring on canary percentages alone — without the hold — strands you if a slow-burn bug surfaces a week later.
اشتباهات رایجCommon mistakes
- مهاجرت خواندنها پیش از بازپخش تاریخچه توسط نمای تازه: درصدهای canary روی نمای ناقص جوابهای غلط مطمئن سرو میکنند. اول backfill با catch-up، بعد درصدها.
- گذاشتن نوشتن مستقیم در نماها حین مهاجرت: projectionها از لاگ واگرا میشوند و replay دیگر هیچ نمایی را بازنمیسازد. همه ورودی تازه از لاگ میرود، بدون استثنا.
- بازنشستگی نمای قدیم در canary صددرصد بدون دوره نگهداشت: rollback همان روزی میمیرد که باگ کندسوز به آن نیاز دارد. اول معیارهای cutover با نگهداشت، بعد بازنشستگی.
- Migrating reads before the new view has replayed history: canary percentages over an incomplete view serve confident wrong answers. Backfill plus catch-up first, percentages second.
- Letting views accept direct writes during migration: the projections diverge from the log and replay can no longer rebuild either view. All fresh input goes through the log, no exceptions.
- Retiring the old view at 100% canary without a hold period: rollback dies the same day a slow-burn bug needs it. Cutover criteria plus hold, then retire.
منابع:Sources: Atlassian · engineering interview handbook · Amazon · SDE III interview preparation
33 تمرین ۳ — پرداختِ چندshardیِ مینیاتوریExercise 3 — Miniature multishard payment
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- سه shard هرکدام با یک پردازنده تکنخ (مبدأ و مقصد و کارمزد). رهبر هر shard مالک leg خودش است؛ هیچ تراکنشی روی shardها کشیده نمیشود.
- مبدأ ۱۰۰۰ و مقصد ۱۰۰ دارد؛ پرداخت ۲۰۰ با کارمزد ۵ است. invariant در مرز منطقی مشترک، مجموع موجودیها بهعلاوه فقط legهای اعمالنشده برابر ۱۱۰۰ است.
- دیسکها میتوانند وسط نوشتن crash کنند و پردازندهها وسط انتشار بمیرند؛ شبکه ممکن است تکراری بدهد ولی رکوردهای قاببندیشده را خراب نمیکند (CRC خرابی را رد میکند).
- Three shards with one single-threaded processor each (source, destination, fee). Each shard's leader owns its leg; no transaction spans shards.
- Source starts at 1000, destination at 100; payment is 200 plus fee 5. At a common logical boundary, balances plus only unapplied legs sum to 1100.
- Disks can crash mid-write and processors can die mid-publish; the network may duplicate but not corrupt framed records (CRC rejects corruption).
چرا این روش و این عددها؟Why this method and these numbers?
framing مرز رکورد، checksum خرابی بایت و fsync درخواست دوام پیش از ACK را مشخص میکنند؛ هیچکدام بهتنهایی تراکنش چندshardی نیست. تصمیم پایدار از تغییر نظر پس از restart جلوگیری میکند و dedup پایدار مانع دوبارهاعمالشدن leg میشود. ۱۰۰۰+۱۰۰=۱۱۰۰ کل اولیه است؛ پس از اعتبار مقصد ۲۰۰، فقط کارمزد ۵ هنوز در راه است، نه تمام ۲۰۵. جمعِ خواندنهای مستقل در لحظههای مختلف شاهد conservation نیست؛ در دمو از logهای بازیابیشده در مرز منطقی مشترک استفاده میکنیم.
Framing identifies record boundaries, checksums detect byte corruption and fsync requests durability before ACK; none creates a cross-shard transaction. Durable decisions prevent re-deciding after restart; durable dedup prevents duplicate legs. The initial total is 1000+100=1100. After crediting 200 to the destination, only fee 5 remains in transit, not all 205. Independent reads at different instants do not prove conservation; compare recovered logs at a common logical boundary.
مدل ذهنیMental model
حل گامبهگامStep by step
- قاببندی و checksum و fsync length مرز رکورد را مشخص میکند، CRC خرابی payload را مییابد و force پیش از ACK، داده را از buffer فرایند خارج میکند. recovery فقط suffix ناقص را truncate میکند؛ checksum خراب یا طول نامعتبر باعث توقف و بررسی میشود. این دمو دوام در crash پردازنده با دیسک باقیمانده را میآزماید.
- یکبار تصمیم و رزرو نمایان یک رکورد D تصمیم، مبلغ رزرو و خروجیهای لازم را ثبت میکند. replay همان رکورد کسر موجودی و obligationها را بازسازی میکند؛ بنابراین شکاف میان لاگ تصمیم و state جدا نداریم.
- انتشار و dedup و همگرایی legها با کلید (shard,payment_id) ارسال میشوند. مقصد از رکورد C هم اعتبار و هم dedup را بازسازی میکند؛ retry میتواند همهٔ legها را دوباره بفرستد و مقصد تکرار را حذف میکند. هر leg اعمالشده از مبلغ in-transit کم میشود؛ این جمع در مرز مشترک harness حساب میشود، نه از خواندن تصادفی چند shard.
- Frame, checksum, fsync Length identifies record boundaries, CRC detects corrupt payloads and force precedes ACK. Recovery truncates an incomplete suffix only; checksum mismatches or invalid lengths stop recovery. This demo tests processor restart with surviving files.
- Decide once, reserve visibly One D record persists the decision, reservation amounts and required outputs. Replaying it reconstructs source debit and obligations, avoiding a gap between a decision log and separately stored state.
- Publish, dedup, converge Publish legs keyed by (shard,payment_id). Each C record reconstructs both credit and dedup; replay may resend all legs and destinations absorb repeats. Remove applied legs from in-transit value at the harness’s common logical boundary, not from arbitrary cross-shard reads.
طراحی مشخصConcrete design
لاگ src رکورد D(id,amount,fee,approved) دارد؛ همان رکورد مرجع تصمیم، کسر موجودی و تعهد ارسال است. لاگهای dst و fee هرکدام C(id,amount) دارند و replay همان رکورد هم effect و هم dedup را میسازد. src با S(id) پایان انتشار را ثبت میکند. framing شامل length/payload/CRC است و ACK بعد از force(true) میآید؛ دم ناقص پیش از append بعدی truncate میشود و checksum ناسازگار بازیابی را متوقف میکند. دمو crash پردازنده با فایلهای باقیمانده را مدل میکند؛ دوام در قطع برق و directory fsync روی فایلهای تازه نیاز به آزمون filesystem و runbook جدا دارد.
در harness میتوان لاگها را در مرز منطقی مشترک خواند و فقط legهای اعمالنشده را pending دانست؛ این قابلیت، API اتمیک برای balanceهای توزیعشده نیست. در production باید وضعیت pending/settled، ACK هر leg و reconciliation را با شناسهٔ پایدار طراحی کرد.
The src log records D(id,amount,fee,approved), the authority for decision, reservation and publication obligations. The dst and fee logs record C(id,amount); replay reconstructs both effects and dedup. Src records S(id) after publication. Frames contain length/payload/CRC with force(true) before ACK. Recovery truncates incomplete suffixes before later appends and stops on checksum mismatch. The demo models processor crashes with surviving files; power-loss durability and directory fsync for new files require separate filesystem testing.
The harness inspects logs at a common logical boundary and counts only unapplied legs as pending. This is not an atomic distributed balance API. Production pending/settled states, per-leg ACKs and reconciliation need persistent identities and explicit protocols.
مثال محاسبهشدهWorked example
پس از رزرو، src=795 و dst=100 و fee=0 و مبلغ در راه ۲۰۵ است؛ جمع ۱۱۰۰ میشود. پس از credit مقصد، dst=300 و فقط ۵ کارمزد در راه است؛ نگهداشتن ۲۰۵ بهعنوان پول در راه دوبارهشماری است. پس از recovery و اعمال کارمزد، src=795,dst=300,fee=5 و pending=0 داریم. پردازندهٔ تازه فقط فایل میخواند؛ هیچ map از فرایند قبلی کپی نمیشود.
After reservation: src=795, dst=100, fee=0, in-transit=205, total=1100. After destination credit: dst=300 and only fee 5 remains in transit; counting all 205 again double-counts money. Recovery applies the fee, yielding src=795,dst=300,fee=5,pending=0. A fresh processor reads files only; no prior process maps are copied.
دانلود نمودار مستقلDownload standalone diagram
- اول قاببندی با fsync: طول و CRC32 و force الصاق را در برابر crash پایدار میکنند؛ الصاق خام فایل این را نمیکند.
- یکبار تصمیم بگیر و APPROVED را ذخیره کن و بعد ۲۰۵ را در دفتر معلق رزرو کن — پول در حال انتقال یک سطر نمایان میشود.
- legها را منتشر کن؛ روی crash، Restart verdict ذخیرهشده را بازمیخواند و فقط انتشارهای گمشده را بازپخش میکند — مقصدها تکرارها را dedup میکنند.
- Frame plus fsync first: length, CRC32 and force make the append crash-durable; a raw file append would not.
- Decide once and store APPROVED, then reserve 205 into the pending ledger — in-flight money becomes a visible row.
- Publish the legs; on crash, Restart re-reads the stored verdict and replays only the missing publishes — destinations dedup the repeats.
نقطهٔ تصمیم: تصمیم یک سطر ذخیرهشده است که هر گام بعدی میخواندش؛ restart انتشارها را بازپخش میکند ولی verdict را هرگز بازمحاسبه نمیکند.
رد خرابی: پس از credit مقصد و پیش از کارمزد crash منطقی رخ میدهد. فقط ۵ کارمزد در راه است؛ پردازندهٔ تازه تصمیم و credit قبلی را از فایل بازیابی و کارمزد را تکمیل میکند. مفاهیم منطقی decision/reservation/outbox شکل در کد از یک رکورد D بازسازی میشوند.
تضمین نشاندادهشده: تصمیم پایدار و replayِ idempotent را نشان میدهد. جمع اقتصادی فقط با مرز مشترک و pending هر leg سنجیده میشود؛ رؤیت اتمیک بین shardها تضمین نیست.
تضمین نشانندادهشده: درباره رؤیت اتمیک بین shardها چیزی نمیگوید — عمداً، چون پروتکل آن را نمیدهد.
Decision point: The decision is a stored row read by every later step; restart replays publishes but never recomputes the verdict.
Failure trace: A logical crash occurs after destination credit but before fee credit. Only fee 5 remains in transit. A fresh processor recovers the decision and prior credit from files and completes the fee. The figure’s logical decision/reservation/outbox concepts are reconstructed from one D record in the code.
Guarantee shown: Shows durable decisions and idempotent replay. Economic totals require a common boundary and per-leg pending accounting; cross-shard atomic visibility is not guaranteed.
Not shown: It says nothing about atomic visibility across shards — deliberately, because the protocol does not offer it.
خودآزماییSelf-check
کدام یال شکل تصمیم دوباره را ممنوع میکند و اگر برداشته شود چه میشکند؟
پاسخ: یال بازخوانی: restart همان APPROVED/DECLINED ذخیرهشده را بار میکند بهجای تصمیم دوباره، پس تغییرهای همزمان بالانس نمیتوانند verdict را برگردانند.
Which edge of the figure forbids deciding twice, and what breaks if it is removed?
Answer: The re-read edge: restart loads the stored APPROVED/DECLINED instead of deciding again, so concurrent balance changes cannot flip the verdict.
دانلود SVG مستقلDownload standalone SVG
- PaymentDecision: یک سطر هر شناسه پرداخت که یکبار نوشته و هرگز بهروز نمیشود — restart بازخوانیاش میکند.
- PendingReservation: پول در حال انتقال بهمثابه سطر واقعی که فقط با settle آزاد میشود — crash آن را نمایان میگذارد نه ناپدید.
- حسابها با رؤیت: legها جدا مینشینند (عادی) و معلق همیشه نشان داده میشود (حسابشده) و هیچ snapshot اتمیکی نیست (همگرایی رؤیت نیست).
- PaymentDecision: one row per payment ID, written once and never updated — restart re-reads it.
- PendingReservation: in-flight funds as a real row, released only by settle — a crash leaves it visible, never vanished.
- Accounts plus visibility: legs land separately (normal), pending is always shown (accounted), and no atomic snapshot exists (convergence is not visibility).
نقطهٔ تصمیم: پول معلق یک سطر است: رکورد رزرو پول در حال انتقال را در هر لحظه داخل مرز حسابداری نگه میدارد.
رد خرابی: پس از credit مقصد و پیش از کارمزد crash منطقی رخ میدهد. فقط ۵ کارمزد در راه است؛ پردازندهٔ تازه تصمیم و credit قبلی را از فایل بازیابی و کارمزد را تکمیل میکند. مفاهیم منطقی decision/reservation/outbox شکل در کد از یک رکورد D بازسازی میشوند.
تضمین نشاندادهشده: تصمیم پایدار و replayِ idempotent را نشان میدهد. جمع اقتصادی فقط با مرز مشترک و pending هر leg سنجیده میشود؛ رؤیت اتمیک بین shardها تضمین نیست.
تضمین نشانندادهشده: درباره سرعت همگرایی legها چیزی نمیگوید — timeliness جداگانه اندازهگیری میشود، در تابلوی lag تمرین ۴.
Decision point: Pending money is a row: the reservation record keeps in-flight funds inside the accounting boundary at every instant.
Failure trace: A logical crash occurs after destination credit but before fee credit. Only fee 5 remains in transit. A fresh processor recovers the decision and prior credit from files and completes the fee. The figure’s logical decision/reservation/outbox concepts are reconstructed from one D record in the code.
Guarantee shown: Shows durable decisions and idempotent replay. Economic totals require a common boundary and per-leg pending accounting; cross-shard atomic visibility is not guaranteed.
Not shown: It says nothing about how fast legs converge — timeliness is measured separately, in exercise 4's lag board.
خودآزماییSelf-check
یک crash دویستوپنج واحد را وسط انتشار سرگردان میکند. کدام رکورد بررسی جمع را سبز نگه میدارد و چطور؟
پاسخ: PendingReservation: پول crashکرده وسط انتشار را داخل مرز حسابداری نگه میدارد تا بررسی جمع هنوز بقا کند.
A crash strands 205 units mid-publish. Which record keeps the sum check green, and how?
Answer: PendingReservation: it keeps crashed mid-publish money inside the accounting boundary so the sum check still conserves.
دانلود نمودار مستقلDownload standalone diagram
- درخواست قاببندیشده را الصاق کن و سطر را بخوان و اول verdict را ذخیره کن — پیش از انتشار هرچیز تصمیم بگیر.
- سه leg همانشناسه را از outbox منتشر کن؛ هر لاگ نشستنی تکرارها را روی همان شناسه dedup میکند.
- هر shard را با معلق جمع بزن: ۱۱۰۰ پیش و حین و بعد از crash — خط OK پایانی دمو.
- Append the framed request, read the row, store the verdict first — decide before anything publishes.
- Publish three same-ID legs from the outbox; each landing log dedups repeats on that ID.
- Sum every shard plus pending: 1100 before, during and after the crash — the demo's final OK line.
نقطهٔ تصمیم: یک شناسه و سه لاگ و یک جمع بقاشده: همان شناسه پرداخت کلید تصمیم و هر leg و هر بررسی dedup است.
رد خرابی: پس از credit مقصد و پیش از کارمزد crash منطقی رخ میدهد. فقط ۵ کارمزد در راه است؛ پردازندهٔ تازه تصمیم و credit قبلی را از فایل بازیابی و کارمزد را تکمیل میکند. مفاهیم منطقی decision/reservation/outbox شکل در کد از یک رکورد D بازسازی میشوند.
تضمین نشاندادهشده: تصمیم پایدار و replayِ idempotent را نشان میدهد. جمع اقتصادی فقط با مرز مشترک و pending هر leg سنجیده میشود؛ رؤیت اتمیک بین shardها تضمین نیست.
تضمین نشانندادهشده: درباره legهایی که برای همیشه نمینشینند چیزی نمیگوید — جبران مقصد بستهشده در پاسخ پیگیری است.
Decision point: One ID, three logs, one conserved sum: the same payment ID keys the decision, every leg, and every dedup check.
Failure trace: A logical crash occurs after destination credit but before fee credit. Only fee 5 remains in transit. A fresh processor recovers the decision and prior credit from files and completes the fee. The figure’s logical decision/reservation/outbox concepts are reconstructed from one D record in the code.
Guarantee shown: Shows durable decisions and idempotent replay. Economic totals require a common boundary and per-leg pending accounting; cross-shard atomic visibility is not guaranteed.
Not shown: It says nothing about permanently unlandable legs — compensation for a closed destination is answered in the follow-up.
خودآزماییSelf-check
بعد از crash وسط انتشار، کدام دو سازوکار restart را بدون اعمال دوباره همگرا میکنند؟
پاسخ: تصمیم ذخیرهشده (بازخوانی، هرگز بازمحاسبه) با dedup هر leg روی همان شناسه: تکرارها نادیده گرفته و legهای گمشده بازپخش میشوند.
After a mid-publish crash, what two mechanisms make the restart converge without double-applying?
Answer: The stored decision (re-read, never recomputed) plus per-leg dedup on the same ID: repeats are ignored, missing legs are replayed.
گزینههای جایگزینAlternatives
- 2PC روی سه shard: رؤیت اتمیک واقعی، ولی هر پرداخت هزینه هماهنگی بینشاردی میدهد و دسترسپذیری کندترین shard را به ارث میبرد. فقط وقتی انتخابش کن که خوانندهها هیچوقت نباید پرداخت جزئی ببینند (نادر)؛ وگرنه پروتکل مرحلهای با جبران ارزانتر و دردسترستر است.
- پول تکشارد (عبور هر حساب از یک shard دفترکل): با shard نکردن اتمیک بودن را نگه میدارد و توان را به یک ماشین سقف میزند. تا وقتی نرخ میگنجد نگه دار؛ روزی که نگنجید بر اساس حساب shard کن و پروتکل بالا را بپرداز.
- 2PC across the three shards: real atomic visibility, but every payment pays cross-shard coordination and inherits the slowest shard's availability. Choose it only when readers must never observe a partial payment (rare); otherwise the staged protocol plus compensation is cheaper and more available.
- Single-shard money (route every account through one ledger shard): keeps atomicity by refusing to shard, and caps throughput at one machine. Keep while the rate fits; shard by account the day it does not, and pay the protocol above.
سناریوهای خرابیFailure scenarios
- crash منطقی بعد از credit مقصد: پردازندهٔ تازه تصمیم و dedup را از فایل میخواند و replay میکند؛ credit مقصد تکرار نمیشود و کارمزد باقیمانده اعمال میشود.
- نوشتن نیمهکاره در suffix با truncate پیش از append بعدی ترمیم میشود. checksum خراب، حتی در رکورد آخر، بیصدا حذف نمیشود: recovery متوقف میشود تا repair یا restore معتبر تصمیم بگیرد.
- تلاش مقصد برای خرج leg رسیده پیش از settle کارمزد: پرداخت جزئی مشاهده میشود. نگهبان معنای صادقانه رؤیت است — legها فقط با قاعده صریح محصول per-leg قابلخرجاند و رزرو معلق باقیمانده تسویهنشده را تا settle یا جبران حسابشده نگه میدارد.
- A logical crash after destination credit is recovered by a fresh processor reading decisions and dedup from files. Replay does not credit the destination twice and applies the outstanding fee.
- Truncate an incomplete suffix before later appends. A checksum mismatch, even in the last frame, stops recovery rather than silently discarding possibly committed history; repair/restore needs a verified decision.
- Destination tries to spend an arriving leg before fee settles: it observes a partial payment. Guard is honest visibility semantics — legs are spendable per-leg only by explicit product rule, and pending reservation keeps the unsettled remainder accounted until settle or compensation.
کد قابل اجرا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.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.util.*;
import java.util.zip.CRC32;
// Teaching crash model: fsynced files survive a processor restart.
// The harness can inspect all logs at one logical boundary; this is NOT an
// atomic remote balance API or proof against power loss/directory-entry loss.
public class ShardPayDemo {
record Decision(String id, long amount, long fee, boolean approved) {}
final Path dir;
final Map<String, Decision> decisions = new LinkedHashMap<>();
final Map<String, Long> balances = new LinkedHashMap<>();
final Set<String> seen = new HashSet<>(), settled = new HashSet<>();
ShardPayDemo(Path dir) throws IOException {
this.dir=dir; Files.createDirectories(dir);
balances.put("src",1000L); balances.put("dst",100L); balances.put("fee",0L);
for (String row : read("src")) {
String[] t=row.split(" ");
if (t[0].equals("D")) {
Decision d=new Decision(t[1],Long.parseLong(t[2]),Long.parseLong(t[3]),Boolean.parseBoolean(t[4]));
if (decisions.putIfAbsent(d.id,d)==null && d.approved)
balances.merge("src",-(d.amount+d.fee),Long::sum);
} else if (t[0].equals("S")) settled.add(t[1]);
}
for (String shard : List.of("dst","fee"))
for (String row : read(shard)) {
String[] t=row.split(" "); String key=shard+":"+t[1];
if (seen.add(key)) balances.merge(shard,Long.parseLong(t[2]),Long::sum);
}
}
void append(String shard,String payload) throws IOException {
byte[] p=payload.getBytes(StandardCharsets.UTF_8); CRC32 crc=new CRC32(); crc.update(p);
ByteBuffer b=ByteBuffer.allocate(p.length+12).putInt(p.length).put(p).putLong(crc.getValue()); b.flip();
try (FileChannel ch=FileChannel.open(dir.resolve(shard+".log"),StandardOpenOption.CREATE,StandardOpenOption.WRITE,StandardOpenOption.APPEND)) {
while(b.hasRemaining()) ch.write(b);
ch.force(true);
}
}
List<String> read(String shard) throws IOException {
Path path=dir.resolve(shard+".log"); List<String> rows=new ArrayList<>();
if (!Files.exists(path)) return rows;
ByteBuffer b=ByteBuffer.wrap(Files.readAllBytes(path)); int validEnd=0;
while(b.hasRemaining()) {
if(b.remaining()<4) break; // incomplete final frame
int length=b.getInt();
if(length<0 || length>1_048_576) throw new IOException("invalid length: fail closed");
if(b.remaining()<length+8) break;
byte[] payload=new byte[length]; b.get(payload); long expected=b.getLong();
CRC32 crc=new CRC32(); crc.update(payload);
if(crc.getValue()!=expected) throw new IOException("checksum mismatch: stop recovery");
rows.add(new String(payload,StandardCharsets.UTF_8)); validEnd=b.position();
}
// Repair an incomplete suffix before accepting any new append.
if(validEnd!=b.limit()) try(FileChannel ch=FileChannel.open(path,StandardOpenOption.WRITE)) {
ch.truncate(validEnd); ch.force(true);
}
return rows;
}
void credit(String shard,String id,long amount) throws IOException {
String key=shard+":"+id;
if(seen.contains(key)) return;
// The log record is the authority for BOTH the effect and dedup on replay.
append(shard,"C "+id+" "+amount);
seen.add(key); balances.merge(shard,amount,Long::sum);
}
void process(String id,long amount,long fee,int crashAfterLegs) throws IOException {
if(amount<0 || fee<0) throw new IllegalArgumentException();
Decision d=decisions.get(id);
if(d==null) {
d=new Decision(id,amount,fee,balances.get("src")>=amount+fee);
// ONE durable record describes decision, reservation AND pending outputs.
append("src","D "+id+" "+amount+" "+fee+" "+d.approved);
decisions.put(id,d);
if(d.approved) balances.merge("src",-(amount+fee),Long::sum);
}
if(d.amount!=amount || d.fee!=fee) throw new IllegalArgumentException("ID reused with different payload");
if(!d.approved || settled.contains(id)) return;
if(crashAfterLegs==0) return;
credit("dst",id,d.amount); if(crashAfterLegs==1) return;
credit("fee",id,d.fee); if(crashAfterLegs==2) return;
append("src","S "+id); settled.add(id);
}
long inTransitAtCommonBoundary() {
long pending=0;
for(Decision d:decisions.values()) if(d.approved) {
if(!seen.contains("dst:"+d.id)) pending+=d.amount;
if(!seen.contains("fee:"+d.id)) pending+=d.fee;
}
return pending;
}
void checkConservation() {
long total=balances.values().stream().mapToLong(Long::longValue).sum()+inTransitAtCommonBoundary();
if(total!=1100) throw new AssertionError("conservation: "+total);
}
public static void main(String[] args) throws IOException {
for(int cut=0;cut<=3;cut++) {
Path dir=Files.createTempDirectory("shardpay-");
ShardPayDemo first=new ShardPayDemo(dir); first.process("pay-1",200,5,cut);
first.checkConservation();
// Fresh processor: NO copying maps or dedup from the previous instance.
ShardPayDemo restarted=new ShardPayDemo(dir); restarted.checkConservation();
restarted.process("pay-1",200,5,3); restarted.process("pay-1",200,5,3);
restarted.checkConservation();
if(!restarted.balances.equals(Map.of("src",795L,"dst",300L,"fee",5L)) || restarted.inTransitAtCommonBoundary()!=0)
throw new AssertionError("replay changed effects");
System.out.println("PASS cut="+cut+" recovered="+restarted.balances+" pending=0");
}
Path dir=Files.createTempDirectory("shardpay-tail-");
ShardPayDemo p=new ShardPayDemo(dir); p.process("pay-1",200,5,1);
Files.write(dir.resolve("src.log"),new byte[]{0,0},StandardOpenOption.APPEND);
ShardPayDemo repaired=new ShardPayDemo(dir); repaired.process("pay-1",200,5,3); repaired.checkConservation();
byte[] corrupt=Files.readAllBytes(dir.resolve("src.log")); corrupt[4]^=1;
Files.write(dir.resolve("src.log"),corrupt);
boolean rejected=false;
try { new ShardPayDemo(dir); } catch(IOException e) { rejected=true; }
if(!rejected) throw new AssertionError("corruption silently accepted");
System.out.println("PASS truncated suffix repaired; checksum corruption rejected");
}
}
اجرا: javac ShardPayDemo.java && java ShardPayDemo — فقط استاندارد جاوا، فوری (از دایرکتوری موقت استفاده میکند).
خروجی مورد انتظار: PASS برای cutهای ۰ تا ۳ با recovered={src=795, dst=300, fee=5} pending=0 و سپس تأیید ترمیم دم ناقص و رد checksum خراب. هر پردازندهٔ تازه تصمیم و اثر را از فایلها میسازد؛ نقطههای crash شبیهسازی شدهاند.
Run: javac ShardPayDemo.java && java ShardPayDemo — stdlib Java only, instant (uses a temp dir).
Expected output: PASS cut=0..3 recovered={src=795, dst=300, fee=5} pending=0, then PASS truncated suffix repaired; checksum corruption rejected. Fresh processors reconstruct every decision and effect from files; crash boundaries are simulated.
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (استف): مقصد برای همیشه بسته شده باشد؛ چگونه پرداخت pending را حل میکنی؟Follow-up (Staff): The destination account closes permanently. How do you resolve a pending transfer?
ابتدا سرنوشت legهای قبلی را با شناسههایشان reconcile کن؛ timeout به معنی اعمالنشدن نیست. مبلغی که هنوز هرگز credit نشده میتواند آزاد شود؛ مبلغ creditشده باید با debit جبرانیِ مجاز و بادوام در مقصد برگردد و سپس در مبدأ credit شود. compensation شناسه و نوع عملیات مستقل و dedup خودش را دارد؛ استفاده از همان کلید credit قبلی میتواند آن را اشتباه تکراری کند. اگر برگشت مجاز یا ممکن نیست، وضعیت unresolved و مسیر عملیاتی لازم است؛ refund بیقید ۲۰۵ میتواند پول خلق کند.
Reconcile previous legs by identity first; timeout does not establish non-application. Release never-credited reservations, but recover credited funds through an authorized durable compensating debit before refunding source. Compensation needs its own operation type/identity and dedup; reusing the original credit key can suppress it. If reversal is unavailable, retain an unresolved state and operational recovery. An unconditional 205 refund can create money.
پیگیری (استف): بعد از بازیابی، پرداخت integral است یا atomic یا هیچکدام؟Follow-up (Staff): After recovery, is the payment integral, atomic, or neither?
بعد از بازیابی کامل، integrity یعنی تصمیم واحد، اثر حداکثر یکبارهٔ هر leg و بقای اقتصادی در مرز مقایسهٔ معتبر. این مدل رؤیت اتمیک نمیدهد. برای دیدن یک snapshot مشترک باید خوانندگان و نویسندگان پروتکل تراکنش/commit و snapshot یکسانی داشته باشند؛ صرف افزودن نام consensus یا 2PC بدون قرارداد خواندن کافی نیست.
After complete recovery, integrity means one decision, at-most-once leg effects and economic conservation at a valid comparison boundary. The model does not provide atomic visibility. Shared snapshots require readers and writers to follow a common transaction/commit and snapshot protocol; naming consensus or 2PC alone is insufficient.
اشتباهات رایجCommon mistakes
- پایدار خواندن الصاق خام فایل: بدون قاببندی و checksum و fsync و گذر بازیابی که دمهای پاره را میاندازد، crash وسط نوشتن پول میسازد یا گم میکند. پایداری همان پروتکل چهاربخشی است نه فراخوانی الصاق.
- اتمیک خواندن همگرایی نهایی: جمعهای متوازن یعنی خوانندهها همه legها را با هم دیدهاند نیست. بقاشده با حسابشده را وعده بده و مشاهدهپذیری جزئی را صریح نام ببر.
- بیرون گذاشتن پول معلق از حسابداری: پول بین رزرو و settle به سطر رزرو نمایان تعلق دارد. پول در حال انتقال حسابنشده بررسی جمع را دقیقاً وقتی میشکند که crash بیشترین نیاز را به آن دارد.
- Calling a raw file append durable: without framing, checksum, fsync and a recovery pass that discards torn tails, a crash mid-write invents money or loses it. Durability is the four-part protocol, not the append call.
- Reading final convergence as atomic visibility: balanced totals do not mean readers saw all legs together. Promise conserved plus accounted, and name partial observability explicitly.
- Leaving pending money out of the accounting: funds between reserve and settle belong to a visible reservation row. Unaccounted in-flight money breaks the sum check exactly when a crash needs it most.
منابع:Sources: AWS · safe retries with idempotent APIs · Jepsen · consistency models
44 تمرین ۴ — حسابرسیِ سرتاسریExercise 4 — End-to-end auditing
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- دامنه تاریخچه انتقال: چهار انتقال آموزشی t1 تا t4 روی حسابهای A=500 و B=300 و C=200. derivation تحت حسابرسی بیصدا t3 را میاندازد.
- checker هیچ کدی با derivation ندارد: بازمحاسبه مستقلنوشتهشده از همان لاگ که در یک checkpoint اعلامشده (آفست ۴) مقایسه میشود.
- checkpointها آفستهای لاگاند که هر دو طرف میتوانند نام ببرند. نمایی که هنوز به checkpoint نرسیده عقب است نه خراب.
- Transfer history scope: four teaching transfers t1..t4 over accounts A=500, B=300, C=200. The derivation under audit silently drops t3.
- The checker shares no code with the derivation: independently written recomputation from the same log, compared at one stated checkpoint (offset 4).
- Checkpoints are log offsets both sides can name. A view that has not reached the checkpoint yet is behind, not corrupt.
چرا این روش و این عددها؟Why this method and these numbers?
اگر checker و projection یک کد داشته باشند، یک bug مشترک میتواند هر دو را به یک جواب غلط برساند؛ محاسبهٔ مستقل شانس کشف را بالا میبرد. مقایسه در offset=4 انتخاب آموزشی است چون t3 گمشده باید در هر دو طرف تا آن مرز پردازش شده باشد. اختلاف قبل از هممرزشدن میتواند فقط lag باشد. checksum سریع است ولی محل خطا را نمیگوید؛ برای repair باید به bucket و سپس رکوردهای متفاوت برسیم.
Sharing derivation code with the checker can reproduce the same bug twice; independent recomputation improves detection. Offset 4 is chosen because both sides should have processed missing transfer t3 by then. Earlier differences may be lag. A checksum detects mismatch quickly but does not locate it; descend through buckets to records for repair.
مدل ذهنیMental model
حل گامبهگامStep by step
- بازپخش تا checkpoint checker لاگ مرجع را با کد مستقلنوشتهشده بازپخش میکند و دقیقاً در checkpoint اعلامشده (آفست ۴) میایستد. تابعهای derivation را هرگز import نمیکند — کد مشترک نقاط کور مشترک میآورد.
- مقایسه در یک نقطه state زنده سنجاقشده در همان checkpoint خوانده و فیلدبهفیلد diff گرفته میشود. نمای عقبمانده ولی سازگار بهمثابه lag کراندار ثبت میشود؛ فقط اختلاف در checkpoint یکسان mismatch است — t3 افتاده دمو دقیقاً همینجا روی A و C شلیک میکند.
- مسیریابی با تعمیر دروازهدار mismatch به pager و تعمیر تأییدانسانی میرود که از لاگ بازپخش میشود؛ lag با روندش به تابلو میرود. هزینه از تابلوی پیوسته به replay ساعتی به جمعهای روزانه به بازبینی هر incident رشد میکند — درحالیکه لاگ ناقص یا باگ مشترک بیرون هر بررسی میماند و صریح نام برده میشود.
- Replay to the checkpoint The checker replays the authoritative log with independently written code, stopping exactly at the stated checkpoint (offset 4). It never imports the derivation's functions — shared code would share blind spots.
- Compare at one point Live state is read pinned at the same checkpoint and diffed field by field. A behind-but-consistent view is recorded as lag with a bound; only a same-checkpoint difference is a mismatch — the demo's dropped t3 fires on A and C exactly here.
- Route, repair gated Mismatch routes to the pager and a human-approved repair that replays through the log; lag routes to the board with its trend. Cost grows from continuous board to hourly replay to daily sums to per-incident review — while an incomplete log or a shared bug stays outside every check, named explicitly.
طراحی مشخصConcrete design
اجزا: لاگ ورودی مرجع با آفستهای نامدار؛ derivation تحت حسابرسی؛ checker (باینری جدا و کد جدا)؛ تابلوی lag و pager. پروتکل: checker تا checkpoint با شماره N بازپخش میکند و state زنده سنجاقشده در N را میخواند و فیلدبهفیلد diff میگیرد. تطابق با lag صفر سبز است؛ تطابق با lag ورودی تابلوست؛ هر diff در checkpoint یکسان page میکند. دروازه تعمیر: تشخیص هیچوقت state را بازنویسی نمیکند — انسان تعمیر را از diff checker تأیید میکند و خود تعمیر از لاگ بازپخش میشود. زمانبندی: تابلوی lag پیوسته و replay checkpoint ساعتی و جمعبندی total روزانه.
Components: the authoritative input log with named offsets; the derivation under audit; the checker (separate binary, separate code); a lag board and a pager. Protocol: checker replays to checkpoint N, reads live state pinned at N, diffs field by field. Match plus lag zero is green; match with lag is a board entry; any same-checkpoint diff pages. Repair gate: detection never rewrites state — a human approves repair from the checker diff, and the repair itself replays through the log. Schedule: lag board continuous, checkpoint replay hourly, totals sum-check daily.
مثال محاسبهشدهWorked example
اجرای آموزشی با t1 تا t4 روی A=500 و B=300 و C=200 در checkpoint ۴. derivation تحت حسابرسی بیصدا t3 را میاندازد، پس زنده A=410 و C=230 میخواند در برابر بازمحاسبهشده A=360 و C=280: ناسازگاری روی A و C. نمای checkpoint دو lag میخواند (عقب نه خراب). لاگ بریدهشده تا ۳ ورودی صادقانه از پیشوندش بازمحاسبه میکند. خط پایانی: OK.
Teaching run with t1..t4 over A=500, B=300, C=200, checkpoint 4. The audited derivation silently drops t3, so live reads A=410, C=230 against recomputed A=360, C=280: MISMATCH on A and C. A checkpoint-2 view reads LAG (behind, not corrupt). A log truncated to 3 entries recomputes honestly from its prefix. Final line: OK.
دانلود نمودار مستقلDownload standalone diagram
- لاگ مرجع را تا checkpoint اعلامشده با کدی بازپخش کن که هیچچیز با derivation ندارد.
- state زنده سنجاقشده در همان checkpoint را بخوان و فیلدبهفیلد diff بگیر: موردانتظار از replay و واقعی از نما.
- عقبمانده ولی سازگار به تابلوی lag میرود؛ هر اختلاف در checkpoint یکسان به انسان page میکند؛ چرخه سر زمانبندی دوباره اجرا میشود.
- Replay the authoritative log to the stated checkpoint with code that shares nothing with the derivation.
- Read live state pinned at the same checkpoint and diff field by field: expected from replay, actual from the view.
- Behind-but-consistent routes to the lag board; any same-checkpoint difference pages a human; the cycle re-runs on schedule.
نقطهٔ تصمیم: checkpoint همان قرارداد است: هر دو طرف پیش از هر مقایسه آفست N را نام میبرند.
رد خرابی: رانش checkpoint را روی همان شکل دنبال کن: اگر checker «زندهترین» را با «هرچه replay رسیده» مقایسه کند، هر lag عادی مثل corruption page میکند — pager تمام روز میخواند و mismatch واقعی t3 در نویز غرق میشود. سنجاق کردن هر دو طرف به N همان چیزی است که verdict را معنادار میکند.
تضمین نشاندادهشده: شکل تشخیص با مسیریابی صادقانه به تابلو یا pager را نشان میدهد. تعمیر را نشان نمیدهد — تشخیص هیچوقت state را بازنویسی نمیکند.
تضمین نشانندادهشده: درباره کوری باگ مشترک یا لاگهای ناقص چیزی نمیگوید — آن حدود در شکل بعدی نمودار شدهاند.
Decision point: The checkpoint is the contract: both sides name offset N before any comparison happens.
Failure trace: Trace a drifting checkpoint on the same figure: if the checker compared 'latest live' against 'whatever replay reached', every normal lag would page as corruption — the pager would fire all day and the real t3 mismatch would drown in noise. Pinning both sides to N is what makes the verdict mean anything.
Guarantee shown: The figure shows detection with honest routing to board or pager. It does not show repair — detection never rewrites state.
Not shown: It says nothing about shared-bug blindness or incomplete logs — those limits are charted in the next figure.
خودآزماییSelf-check
اگر checker بدون سنجاق کردن هر دو طرف به یک checkpoint مقایسه کند چه میشکند؟
پاسخ: مقایسه تازهترین با تازهترین: lag عادی تمام روز مثل corruption page میکرد و mismatch واقعی در نویز غرق میشد.
What breaks if the checker compares without pinning both sides to one checkpoint?
Answer: Comparing latest-against-latest: normal lag would page as corruption all day, and the real mismatch would drown in noise.
دانلود SVG مستقلDownload standalone SVG
- تابلوی lag (ارزانترین و پیوسته) عقبماندگی را در دقیقهها میگیرد؛ replay checkpoint (ساعتی) افتادنهای خاموش مثل t3 را میگیرد.
- جمعبندی total (روزانه) رانش کند را میگیرد؛ بازبینی انسانی (هر incident) تنها خریدار کلاس باگ مشترک است.
- دو جعبه هلویی — لاگ ناقص و باگ مشترک — بیرون هر میلهاند: حدود نامبرده نه پنهان.
- Lag board (cheapest, continuous) catches behind-ness in minutes; checkpoint replay (hourly) catches silent drops like t3.
- Totals sum-check (daily) catches slow drift; human review (per incident) is the only buyer for the shared-bug class.
- The two peach boxes — incomplete log, shared bug — sit outside every bar: named limits, not hidden ones.
نقطهٔ تصمیم: میلههای بلندتر تشخیصهای متفاوت میخرند نه اثبات قویتر: هزینه و قطعیت محورهای جدایند و نقاط کور کشیده شدهاند نه پنهان.
رد خرابی: تعمیر خودکار را روی همان شکل دنبال کن: هیچچیز روی محور هزینه حق بازنویسی state را نمیخرد — بلندترین میله قضاوت انسانی هر incident را میخرد نه رباتی با کلید حذف. تشخیص و تعمیر خریدهای متفاوتیاند.
تضمین نشاندادهشده: شکل نشان میدهد هر بررسی چه هزینه دارد و چه میگیرد. بررسیها را بر اساس اهمیت رتبه نمیکند — تابلوی lag در بیشتر روزها مهمترین است.
تضمین نشانندادهشده: برای دو جعبه هلویی هیچ تشخیصی وعده نمیدهد: لاگهای ناقص و باگهای مشترک بیرون هر میلهاند.
Decision point: Longer bars buy different detections, never stronger proof: cost and certainty are separate axes, and the blind spots are drawn, not hidden.
Failure trace: Trace an automated repair on the same figure: nothing on the cost axis earns the right to rewrite state — the longest bar buys human judgment per incident, not a robot with a delete key. Detection and repair are different purchases.
Guarantee shown: The figure shows what each check costs and catches. It does not rank checks by importance — the lag board matters most on most days.
Not shown: It promises no detection for the two peach boxes: incomplete logs and shared bugs are outside every bar.
خودآزماییSelf-check
کدام بررسی با کمترین هزینه بیشترین تکرار را دارد و pager را از چه چیزی محافظت میکند؟
پاسخ: تابلوی lag: پیوسته با کمترین هزینه اجرا میشود و هر عقبماندگی بیخطر را از corruption واقعی جدا میکند.
Which check runs most often for the least cost, and what does it protect the pager from?
Answer: The lag board: it runs continuously at the lowest cost and separates every benign behind-ness from real corruption.
دانلود نمودار مستقلDownload standalone diagram
- همان لاگ checker (یال replay) و derivation تحت حسابرسی (یال derivation) را تغذیه میکند — یک منبع و دو مسیر کد.
- checker و state زنده در verdict با موردانتظار و واقعی سنجاقشده در checkpoint ۴ به هم میرسند — mismatch t3 دمو همینجا شلیک میکند.
- mismatch به انسان page میکند؛ lag با کران و روندش روی تابلو مینشیند — تشخیص تمام میشود و تعمیر دروازهدار شروع میشود.
- The same log feeds the checker (replay edge) and the derivation under audit (derivation edge) — one source, two code paths.
- Checker and live state meet at the verdict with expected and actual pinned to checkpoint 4 — the demo's t3 mismatch fires here.
- Mismatch pages a human; lag lands on the board with its bound and trend — detection ends, gated repair begins.
نقطهٔ تصمیم: دو محاسبه وارد میشوند و یک verdict بیرون میرود: موردانتظار از checker و واقعی از state زنده که در دقیقاً یک checkpoint مقایسه میشوند.
رد خرابی: تعمیر خودکار را روی همان شکل دنبال کن: هیچ یالی از verdict به state برنمیگردد — حلقه در pager و تابلوی lag تمام میشود. یال تعمیر اول به دروازه تأیید انسانی نیاز دارد؛ دمو mismatchها را چاپ میکند و میایستد، دقیقاً مثل شکل.
تضمین نشاندادهشده: شکل حلقه کامل تشخیص با خروجیهای صادقانه را نشان میدهد. وقتی خود لاگ ورودی ناقص است هیچچیز اثبات نمیکند — پاسخ پیگیری میگوید آنوقت چه میمیرد.
تضمین نشانندادهشده: درباره مکانیک یا زمانبندی تعمیر فراتر از تشخیص چیزی نمیگوید — تعمیر با تأیید انسانی از لاگ بازپخش میشود.
Decision point: Two computations enter, one verdict leaves: expected from the checker, actual from live state, compared at exactly one checkpoint.
Failure trace: Trace auto-repair on the same figure: no edge leads from the verdict back into state — the loop ends at the pager and the lag board. A repair edge would need a human approval gate first; the demo prints mismatches and stops, exactly like the figure.
Guarantee shown: The figure shows the full detection loop with honest exits. It proves nothing when the input log itself is incomplete — the follow-up says what dies then.
Not shown: It says nothing about repair mechanics or schedules beyond detection — repair replays through the log under human approval.
خودآزماییSelf-check
دو یال از لاگ ورودی بیرون میروند. چه چیزی یکی را حسابرسی میکند بهجای تکراری؟
پاسخ: یال replay در برابر یال derivation: هر دو همان لاگ را میخوانند ولی فقط مسیر checker مستقل نوشته شده — همان نامتقارنی حسابرسی است.
Two edges leave the input log. What makes one of them an audit instead of a duplicate?
Answer: Replay edge versus derivation edge: both read the same log, but only the checker's path is written independently — that asymmetry is the audit.
گزینههای جایگزینAlternatives
- assertionهای داخل خط derivation: ارزانترین اجرا، ولی همان کد و همان نقاط کور خط را دارند — باگ مشترک از هر assert رد میشود. assertها را برای fail-fast نگه دار، ولی حسابرسی مستقل نیستند.
- اجرای دوگانه کامل با رأی (دو derivation مستقل و page روی هر اختلاف): قویترین تشخیص، تقریباً دو برابر محاسبه با قاعده تساویشکن وقتی دو checker با هم اختلاف کنند. برای دفترکلهای پولبر نگه دار؛ تک-checker با تابلوی lag اینجا پیشفرض متناسب است.
- Inline assertions inside the derivation pipeline: cheapest to run, but they share the same code and the same blind spots as the pipeline — a shared bug passes every assert. Keep asserts for fail-fast, but they are not an independent audit.
- Full dual-run with vote (two independent derivations, page on any disagreement): strongest detection, roughly double compute plus a tie-break rule for when the two checkers disagree with each other. Reserve for money-moving ledgers; the single-checker-plus-lag-board here is the proportionate default.
سناریوهای خرابیFailure scenarios
- update افتاده خاموش (t3): در replay checkpoint بعدی بهمثابه mismatch در checkpoint یکسان روی A و C گرفته میشود. نگهبان replay مستقل زمانبندیشده است — دمو دقیقاً همین را تزریق میکند.
- اجرای checker روی آفست لاگ تازهتر از رسیده state زنده: هر مقایسه بهغلط page میکند. نگهبان سنجاق هر دو خواندن به checkpoint اعلامشده و رفتار با هر عقبماندگی بهمثابه lag کراندار است، هرگز corruption.
- قطعی checker (بدون verdict در یک چرخه): سکوت مثل سلامتی به نظر میرسد. نگهبان هشدار تازگی روی خود جریان verdict است — حسابرسی گمشده incident است نه قبولی.
- Silent dropped update (t3): caught at the next checkpoint replay as a same-checkpoint mismatch on A and C. Guard is the scheduled independent replay — the demo injects exactly this.
- Checker runs against a newer log offset than live state has reached: every comparison pages falsely. Guard is pinning both reads to the stated checkpoint and treating anything behind as lag with a bound, never as corruption.
- Checker outage (no verdicts for a cycle): silence looks like health. Guard is a freshness alert on the verdict stream itself — a missing audit is an incident, not a pass.
کد قابل اجرا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: end-to-end audit checker over transfer histories.
// Independent recomputation to a STATED checkpoint; same-checkpoint compare;
// lag vs corruption distinguished; missing update injected; incomplete log admitted.
public class AuditDemo {
record Xfer(String id, String from, String to, long amount) {}
List<Xfer> log = new ArrayList<>(List.of(
new Xfer("t1", "A", "B", 100), new Xfer("t2", "B", "C", 40),
new Xfer("t3", "A", "C", 50), new Xfer("t4", "C", "A", 10)));
// The REAL derivation under audit (has an injected silent bug: t3 skipped).
Map<String, Long> derivedWithBug(int checkpoint) {
Map<String, Long> b = new LinkedHashMap<>(Map.of("A", 500L, "B", 300L, "C", 200L));
for (int i = 0; i < checkpoint; i++) {
Xfer x = log.get(i);
if (x.id.equals("t3")) continue; // SILENT DROPPED UPDATE (injected)
b.put(x.from, b.get(x.from) - x.amount);
b.put(x.to, b.get(x.to) + x.amount);
}
return b;
}
// The CHECKER: written independently, straight from the log. No shared code.
static Map<String, Long> recompute(List<Xfer> log, int checkpoint) {
Map<String, Long> b = new LinkedHashMap<>(Map.of("A", 500L, "B", 300L, "C", 200L));
for (int i = 0; i < checkpoint; i++) {
Xfer x = log.get(i);
b.put(x.from, b.get(x.from) - x.amount);
b.put(x.to, b.get(x.to) + x.amount);
}
return b;
}
public static void main(String[] a) {
AuditDemo d = new AuditDemo();
int cp = 4; // stated checkpoint: log offset 4
Map<String, Long> actual = d.derivedWithBug(cp);
Map<String, Long> expect = recompute(d.log, cp);
System.out.println("checkpoint=" + cp + " actual=" + actual + " expected=" + expect);
for (String k : expect.keySet())
if (!expect.get(k).equals(actual.get(k)))
System.out.println("MISMATCH at " + k + ": live=" + actual.get(k) + " recomputed=" + expect.get(k));
// lag, not corruption: derived state is just older (checkpoint 2 vs 4)
Map<String, Long> older = d.derivedWithBug(2);
System.out.println("stale read cp=2 vs checkpoint 4 -> " + (older.equals(expect) ? "match" : "LAG (behind, not corrupt): " + older));
// honest limit: if the input log itself is incomplete, the checker is blind
List<Xfer> partial = d.log.subList(0, 3);
System.out.println("log truncated to 3 entries: checker recomputes " + recompute(partial, 3));
System.out.println("OK: caught silent t3 at same checkpoint; lag separated; incomplete log admitted");
}
}
اجرا: javac AuditDemo.java && java AuditDemo — فقط استاندارد جاوا، فوری.
خروجی مورد انتظار: در checkpoint ۴ checker روی A و C ناسازگاری flag میکند (t3 خاموش)؛ نمای checkpoint دو lag میخواند نه corruption؛ لاگ سهتایی صادقانه از همان که دارد بازمحاسبه میکند؛ OK. (خروجی واقعی همین محیط)
Run: javac AuditDemo.java && java AuditDemo — stdlib Java only, instant.
Expected output: At checkpoint 4 the checker flags MISMATCH on A and C (silent t3); a checkpoint-2 view reads LAG, not corruption; a 3-entry log recomputes honestly from what it has; OK. (actual output of this environment)
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (استف): لاگ ورودی خودش ناقص باشد؛ auditor چه چیزی را نمیتواند ثابت کند؟Follow-up (Staff): The input log itself is incomplete. What can the auditor no longer prove?
آنوقت فراتر از همان پیشوند هیچچیز اثبات نمیکند — و باید همین را بگوید. بازمحاسبه از لاگ بریده مقادیر «موردانتظار» غلط میدهد بیتقصیر derivation؛ مقایسهشان بهغلط page میکند و به تیم یاد میدهد pager را نادیده بگیرد. طراحی صادقانه: checker اول کامل بودن لاگ تا checkpoint را راستیآزمایی میکند (پیوستگی آفست و بدون شکاف) و وقتی پیشفرض شکست خورد از verdict خودداری میکند — «نامعلوم، لاگ ناقص» گزارش میدهد. حسابرسی که ورودیاش را راستیآزمایی نکند هیچچیز را حسابرسی نمیکند.
Then it can prove nothing beyond that prefix — and it must say so. Recomputation from a truncated log yields 'expected' values that are wrong through no fault of the derivation; comparing them pages falsely and teaches the team to ignore the pager. The honest design: the checker first verifies log completeness to the checkpoint (offset continuity, no gaps), and refuses to verdict — reporting 'unknown, log incomplete' — when the premise fails. An auditor that cannot verify its input audits nothing.
پیگیری (استف): derivation و checker یک باگ مشترک داشته باشند. حسابرسی میگیردش؟Follow-up (Staff): Derivation and checker share one Bug. Does the audit catch it?
نمیتواند — از روی ساخت. اگر هر دو مسیر همان بدخوانی را داشته باشند (همان کتابخانه و همان بدفهمی spec و همان تابع کپیشده)، خروجیهایشان توافق میکنند و حسابرسی روی state غلط سبز است. دفاعها جزئیاند: checker را از spec بنویس نه از کد خط؛ نویسندگی را بچرخان؛ جمعبندی total متفاوت بگذار (محاسبه دیگری که بعضی باگهای مشترک را میگیرد)؛ و ریسک باقیمانده را در رانبوک نام ببر بهجای ادعای استقلال کامل. عمق تشخیص متناسب با ناهمسانی واقعی است.
It cannot — by construction. If both paths share the misreading (same library, same spec misunderstanding, same copied function), their outputs agree and the audit is green over a wrong state. Defenses are partial: write the checker from the spec, not from the pipeline code; rotate authorship; vary a Totals sum-check (a different computation that catches some shared bugs); and name the residual risk in the runbook instead of claiming full independence. Detection depth is proportional to genuine dissimilarity.
اشتباهات رایجCommon mistakes
- مقایسه تازهترین با تازهترین بهجای checkpoint یکسان: lag عادی replication مثل corruption page میکند و تیم pager را بیصدا میکند و mismatch واقعی در نویز پنهان میشود. اول checkpoint را سنجاق کن و بعد مقایسه کن.
- استفاده دوباره از تابعهای خود خط داخل checker: حسابرسی تکراری میشود و باگهای مشترک بیصدا رد میشوند. استقلال یعنی کد جداگانهنوشتهشده از spec نه فراخوانی کتابخانه مشترک.
- تعمیر خودکار روی تشخیص: آنوقت باگ checker یا جهش lag state خوب را بازنویسی میکند. تشخیص page میکند؛ انسان تأیید میکند؛ تعمیر از لاگ بازپخش میشود — همیشه به همین ترتیب.
- Comparing latest-against-latest instead of same-checkpoint: normal replication lag pages as corruption, the team mutes the pager, and the real mismatch hides in the noise. Pin the checkpoint first, compare second.
- Reusing the pipeline's own functions inside the checker: the audit becomes a duplicate, and shared bugs pass silently. Independence means separately written code from the spec, not a shared library call.
- Auto-repairing on detection: a checker bug or a lag spike then rewrites good state. Detection pages; a human approves; the repair replays through the log — in that order, always.
منابع:Sources: Jepsen · consistency models · Atlassian · engineering interview handbook