11 تمرین ۱ — بایتها را دستی بشمارExercise 1 — count the bytes by hand
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- فقط رکورد آموزشی: userName=Martin, favoriteNumber=1337, interests=[daydreaming, hacking]. مجموع بایتهای ۸۱/۶۶/۳۳ اعداد آموزشی فصلاند، نه بنچمارک.
- مسیر باینری با کد تولیدشده Protobuf است که runtime آن فیلدهای ناشناخته را نگه میدارد؛ مسیر JSON به آبجکت JSON (یا پیام تازه ساختهشده فیلدبهفیلد) decode میشود که جایی برای بایتهای ناشناخته ندارد.
- فیلد تازه email با تگ تازه ۴ است؛ تگ بازنشسته دوباره استفاده نمیشود.
- Teaching record only: userName=Martin, favoriteNumber=1337, interests=[daydreaming, hacking]. Byte totals 81/66/33 are teaching numbers from the chapter, not benchmarks.
- Binary path uses generated Protobuf code whose runtime retains unknown fields; the JSON path decodes into a JSON object (or a fresh message built field-by-field), which has nowhere to store unknown bytes.
- The new field is email with fresh tag 4, never reusing a retired tag.
چرا این روش و این عددها؟Why this method and these numbers?
اندازهٔ wire را با جمع tag، length و payload میسنجیم چون هر سه واقعاً ارسال میشوند. varint در هر بایت هفت بیت داده دارد، پس برای عدد مثبت ۱۳۳۷ دو بایت لازم است چون از ۱۲۷ بزرگتر و از ۱۶۳۸۳ کوچکتر است. تفاوت ۸۱/۶۶/۳۳ به متن، فاصلهها، encoding و schema همین نمونه وابسته است. حفظ unknown field به runtime و مسیر round-trip وابسته است؛ JSON جای بایت ناشناخته را تضمین نمیکند.
Sum tag, length and payload because all travel on the wire. A varint carries seven data bits per byte: positive 1337 needs two bytes because it exceeds 127 but is below 16384. The 81/66/33 totals depend on this sample’s text, whitespace, encoding and schema. Unknown-field preservation depends on runtime and round-trip path; JSON does not guarantee retention of unknown bytes.
مدل ذهنیMental model
حل گامبهگامStep by step
- نامها روی سیم JSON نامها را مینویسد:
userNameوfavoriteNumberوinterestsداخل همان ۸۱ بایت آموزشی سفر میکنند، پس هر خوانندهای — و هر انسانی با ویرایشگر متن — ساختار را میبیند. هزینه: حجم، ابهام اعداد، و صفر قاعده تکامل مگر خودت registry و قرارداد اضافه کنی. - همان مدل، باینری MessagePack همان مدل بدون schema را باینری میکند: بایتهای نوع و طول جای گیومه و دونقطه مینشینند و به حدود ۶۶ بایت آموزشی میرسیم. نامها همچنان سفر میکنند، اشکالزدایی decoder میخواهد، و تکامل دقیقاً بهاندازه قراردادهای JSON خودت منضبط است — یا بیانضباط.
- یک تگ را دستی decode کن Protobuf نامها را حذف میکند و بهجایش تگ + نوع سیمی مینویسد: بایت
0x0aیعنی فیلد ۱ (0x0a >> 3) با نوع سیمی ۲ (0x0a & 7یعنی طولدار)، و ۱۳۳۷ با varint دوبایتیb9 0aمیرود. جمع: ۳۳ بایت آموزشی. تغییر نام فیلد آزاد است؛ تغییر تگ ممنوع. - دو مسیر، دو سرنوشت
emailرا با تگ ۴ اضافه کن و دو مسیر را اجرا کن. round-trip باینری (کد قدیمی تولیدشده میخواند و بازمینویسد): runtime بایتهای تگ ۴ را دستنخورده عبور میدهد و خواننده جدید نشانی را سالم مییابد. تبدیل JSON یا کپی فیلدبهفیلد: گام میانی فقط فیلدهای ۱ تا ۳ را میشناسد، پس تگ ۴ جایی برای زندگی ندارد و خواننده جدید email گمشده میبیند. سیم یکسان، نتیجه برعکس — تفاوت در قرارداد decoder است نه بایتها.
- Names on the wire JSON writes the names:
userName,favoriteNumber,intereststravel inside the 81 teaching bytes, so any reader — and any human with a text editor — sees the structure. Cost: size, number ambiguity, and zero evolution rules unless you add a registry and conventions yourself. - Same model, binary MessagePack binarizes the same schemaless model: type and length bytes replace quotes and colons, landing near 66 teaching bytes. Names still travel, debugging needs a decoder, and evolution is exactly as disciplined — or undisciplined — as your JSON conventions.
- Decode one tag by hand Protobuf drops the names and writes tag + wire type instead: byte
0x0ais field 1 (0x0a >> 3) with wire type 2 (0x0a & 7, length-prefixed), and 1337 rides as the two-byte varintb9 0a. Total: 33 teaching bytes. Renaming a field is free; changing a tag is forbidden. - Two paths, two fates Add
emailas tag 4 and run the two paths. Binary round-trip (old generated code reads, rewrites): the runtime carries tag 4's bytes through untouched, and the new reader finds the address intact. JSON conversion or field-by-field copy: the middle step only knows fields 1–3, so tag 4 has nowhere to live and the new reader sees a missing email. Same wire, opposite outcomes — the difference is the decoder contract, not the bytes.
مثال محاسبهشدهWorked example
رکورد آموزشی {"userName":"Martin","favoriteNumber":1337,"interests":["daydreaming","hacking"]}: JSON بدون فاصله = ۸۱ بایت، MessagePack حدود ۶۶ بایت، Protobuf برابر ۳۳ بایت (۸ بایت فیلد ۱ + ۳ بایت فیلد ۲ + ۲۲ بایت دو آیتم تکراری). افزودن email (تگ ۴، ۱۳ بایت متن + ۲ بایت قاب) رکورد باینری را به ۴۸ بایت میرساند؛ خواننده باینری قدیمی همچنان فیلدهای ۱ تا ۳ را میفهمد و تگ ۴ را ندیده عبور میدهد.
Teaching record {"userName":"Martin","favoriteNumber":1337,"interests":["daydreaming","hacking"]}: spaceless JSON = 81 bytes, MessagePack ≈ 66 bytes, Protobuf = 33 bytes (8 for field 1 + 3 for field 2 + 22 for the two repeated items). Adding email (tag 4, 13 text bytes + 2 framing bytes) grows the binary record to 48 bytes; the old binary reader still parses fields 1–3 and forwards tag 4 unseen.
دانلود نمودار مستقلDownload standalone diagram
- دو فلش اول: نوشتن رکورد تازه و خواندن آن توسط کد قدیمی — تگ ۴ ناشناخته به دست قدیمی میرسد.
- فلش پررنگ میانی (بازنویسی) قهرمان است: runtime بایتهای ناشناخته را دستنخورده عبور میدهد.
- دو فلش آخر: خواندن دوباره توسط جدید و پاسخ «tag 4 intact» — ایمیل سالم است.
- First two arrows: the fresh write, then the old read — unknown tag 4 lands in old hands.
- The bold middle arrow (write-back) is the hero: the runtime forwards unknown bytes untouched.
- Last two arrows: the new re-read with the reply 'tag 4 intact' — the email survives.
نقطهٔ تصمیم: نقطه تصمیم فلش بازنویسی است: عبور بایت ناشناخته یا بازسازی از دانستهها؟
رد خرابی: اگر runtime ناشناختهها را میانداخت، خواندن دوباره ایمیل گمشده میدید — همان شکل مسیر دوم.
تضمین نشاندادهشده: شکل ترتیب و حفظ را نشان میدهد؛ ثابت نمیکند هر runtimeای نگه میدارد — مال خودت را تست کن.
تضمین نشانندادهشده: درباره معنای تگ ۴ چیزی نمیگوید: حفظ بایت با فهم درست یکی نیست.
Decision point: The decision point is the write-back arrow: forward unknown bytes or rebuild from known fields?
Failure trace: If the runtime dropped unknowns, the re-read would see a missing email — exactly the second path's shape.
Guarantee shown: The figure shows order and preservation; it does not prove every runtime preserves — test your own.
Not shown: It says nothing about tag 4's meaning: kept bytes are not understood bytes.
خودآزماییSelf-check
چرا کد قدیمی بایتهایی را عبور میدهد که نمیفهمد؟
پاسخ: چون سازگاری forward یعنی حمل، نه فهم: ناشناخته را نادیده بگیر ولی بایتهایش را نگه دار.
Why does old code forward bytes it cannot parse?
Answer: Because forward compatibility means carrying, not comprehending: ignore the unknown but keep its bytes.
دانلود نمودار مستقلDownload standalone diagram
- فلش اول: بایتهای ۱ تا ۴ به پراکسی میرسند — هنوز کامل است.
- فلش دوم: پراکسی فقط ۱ تا ۳ را کپی میکند؛ تگ ۴ جایی برای زندگی ندارد.
- فلش چینچین آخر: هشدار پاییندستی «email missing» — گمشدن بیصدا دیر کشف میشود.
- First arrow: bytes 1–4 reach the proxy — still whole.
- Second arrow: the proxy copies only 1–3; tag 4 has nowhere to live.
- Last dashed arrow: the downstream 'email missing' alert — silent loss is found late.
نقطهٔ تصمیم: نقطه تصمیم فلش کپی است: مدل میانی فقط دانستهها را دارد.
رد خرابی: هیچجا خطایی بالا نمیآید؛ مانیتور سبز روی داده گمشده — بدترین نوع خرابی.
تضمین نشاندادهشده: شکل نقطه مرگ تگ ۴ را نشان میدهد؛ راهحل (عبور بایت یا ارتقای پراکسی) بیرون شکل است.
تضمین نشانندادهشده: درباره اینکه کدام کتابخانهها نگه میدارند چیزی نمیگوید — قرارداد هر runtime را جدا بخوان.
Decision point: The decision point is the copy arrow: the middle model holds only known fields.
Failure trace: No error is raised anywhere; a green monitor over lost data — the worst kind of failure.
Guarantee shown: The figure shows where tag 4 dies; the remedy (byte forwarding or proxy upgrade) is outside it.
Not shown: It says nothing about which libraries preserve — read each runtime's contract separately.
خودآزماییSelf-check
با یک تغییر کوچک چه چیزی تگ ۴ را نجات میدهد؟
پاسخ: عبور بایتهای ناشناخته دستنخورده بهجای کپی فیلدبهفیلد — یا بهروزرسانی schema پراکسی.
What one small change saves tag 4?
Answer: Forwarding unknown bytes untouched instead of field-by-field copying — or upgrading the proxy schema.
دانلود SVG مستقلDownload standalone SVG
- اول نوار بالا: نویسنده جدید تگهای ۱ تا ۴ را میفرستد — رکورد کامل، هنوز چیزی گم نشده.
- خواننده قدیمی فقط ۱ تا ۳ را میشناسد ولی runtime آن بایتهای تگ ۴ را جیب میزند؛ جعبه بازنویسی («bytes untouched») قهرمان مسیر A است.
- نوار پایین: پراکسی قدیمی فقط دانستههایش را کپی میکند («copies 1-3»)، پس رکورد تازه بدون تگ ۴ متولد میشود.
- جعبههای آخر: KEPT سبز در برابر LOST قرمز — پیام پانوشت این است که سازگاری سیمی همان حفظ معنا نیست.
- Top lane first: new writer emits tags 1–4 — the full record, nothing lost yet.
- Old reader knows only 1–3 but its runtime pockets tag 4's bytes; the write-back box ('bytes untouched') is the hero of Path A.
- Bottom lane: the old proxy copies only what it knows ('copies 1–3'), so the fresh record is born missing tag 4.
- End boxes: green KEPT versus red LOST — the footer moral is that wire compatibility is not meaning preservation.
نقطهٔ تصمیم: نقطه تصمیم جعبه میانی هر مسیر است: کد قدیمی بایتهای ناشناخته را عبور میدهد یا از فیلدهای شناختهشده بازمیسازد؟
رد خرابی: مسیر B را جعبهبهجعبه دنبال کن: تگ ۴ دقیقاً در زیرنویس «copies 1-3» پراکسی قدیمی میمیرد — رکورد تازه بدون آن متولد میشود، پس «email gone» خواننده جدید دو جعبه زودتر رقم خورده. هیچجا خطایی بالا نمیآید و به همین دلیل گمشدن بیصداست.
تضمین نشاندادهشده: شکل تضمین میکند مسیر از قالب مهمتر است: بایتهای ورودی یکسان، نتیجه برعکس. تضمین نمیکند فلان کتابخانه خاص فیلد ناشناخته را نگه میدارد — این را باید برای هر runtime جدا آزمایش کرد.
تضمین نشانندادهشده: درباره سازگاری معنایی چیزی نمیگوید: تگ ۴ حفظشده اگر شماره تگی روزی دوباره استفاده شود باز هم میتواند اشتباه فهمیده شود.
Decision point: The decision point is the middle box of each path: does the old code forward opaque bytes or rebuild from known fields?
Failure trace: Trace Path B box by box: the old proxy's 'copies 1-3' subtitle is where tag 4 dies — the fresh record is born without it, so the new reader's 'email gone' is decided two boxes earlier. No error is raised anywhere, which is why the loss is silent.
Guarantee shown: The figure guarantees the path matters more than the format: same input bytes, opposite outcomes. It does not guarantee any particular library keeps unknown fields — that must be tested per runtime.
Not shown: It says nothing about semantic compatibility: a preserved tag 4 can still be misinterpreted if a tag number is ever reused.
خودآزماییSelf-check
کدام مسیرهای شکل تگ ۴ را حفظ میکنند — و چرا مسیر JSON نمیتواند؟
پاسخ: فقط round-trip باینری با runtime نگهدارنده. متن JSON جایی برای بایتهای تگ ۴ ندارد، پس هر چیزی که از مدل JSON-شکل بگذرد آن را میبازد.
Which paths in the diagram preserve tag 4 — and why can't the JSON path?
Answer: Only the binary round-trip through a preserving runtime. JSON text has no slot for tag 4's bytes, so anything that passes through a JSON-shaped model loses it.
گزینههای جایگزینAlternatives
- ماندن روی JSON با registry و بازبینی سختگیرانه: اشکالزدایی انسانخوان و تبادل بینسازمانی میماند، به قیمت payload بزرگتر و قواعد تکامل دستی.
- Stay on JSON with a registry and strict review: keeps human-readable debugging and cross-org exchange, at the cost of bigger payloads and hand-enforced evolution rules.
سناریوهای خرابیFailure scenarios
- پراکسی قدیمی رکورد را فیلدبهفیلد بازنویسی میکند: تگ ۴ در همان گام کپی حذف میشود و هر خواننده جدید پاییندستی برای همیشه email گمشده میبیند — گمشدن داده، نه خطای parse.
- استفاده دوباره از شماره تگ ۴ برای فیلد دیگر: ایمیلهای رکوردهای قدیمی بیصدا مقدار فیلد تازه میشوند — خرابیای که هیچ تست سازگاری بعداً نمیگیرد.
- Old proxy rewrites the record field-by-field: tag 4 is dropped at the copy step and every downstream new reader permanently sees a missing email — data loss, not a parse error.
- Reusing tag 4's number for a different field later: old records' emails silently become the new field's values — corruption no compatibility test can catch after the fact.
کد قابل اجرا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.nio.charset.StandardCharsets;
public class TagDemo {
static byte[] varint(long n) {
byte[] tmp = new byte[10];
int i = 0;
while (true) {
int b = (int) (n & 0x7F);
n >>>= 7;
if (n != 0) {
tmp[i++] = (byte) (b | 0x80);
} else {
tmp[i++] = (byte) b;
break;
}
}
byte[] out = new byte[i];
System.arraycopy(tmp, 0, out, 0, i);
return out;
}
static String hex(byte[] a) {
StringBuilder sb = new StringBuilder();
for (byte b : a) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
static byte[] join(byte[]... parts) {
int n = 0;
for (byte[] p : parts) {
n += p.length;
}
byte[] out = new byte[n];
int o = 0;
for (byte[] p : parts) {
System.arraycopy(p, 0, out, o, p.length);
o += p.length;
}
return out;
}
public static void main(String[] args) {
byte[] v = varint(1337);
System.out.println("1337 as varint: " + hex(v));
byte[] rec = join(
new byte[]{0x0a, 0x06}, "Martin".getBytes(StandardCharsets.UTF_8),
new byte[]{0x10}, v,
new byte[]{0x1a, 0x0b}, "daydreaming".getBytes(StandardCharsets.UTF_8),
new byte[]{0x1a, 0x07}, "hacking".getBytes(StandardCharsets.UTF_8));
System.out.println("record bytes: " + rec.length);
int first = rec[0] & 0xFF;
System.out.printf("first tag byte 0x%02x: field %d, wire type %d (2 = length-prefixed)%n",
first, first >> 3, first & 7);
}
}
اجرا: javac TagDemo.java && java TagDemo — فقط استاندارد جاوا (JDK 27)، فوری. بهجای ۱۳۳۷ عدد خودت را بگذار و ببین طول varint در مرزهای ۶۴ و ۸۱۹۲ چطور عوض میشود.
خروجی مورد انتظار: 1337 بهصورت varint برابر b90a؛ بایتهای رکورد: ۳۳؛ بایت اول تگ 0x0a: فیلد ۱، نوع سیمی ۲.
Run: javac TagDemo.java && java TagDemo — Java stdlib only (JDK 27), instant. Change 1337 to your own number and watch the varint length move at the 64 and 8192 boundaries.
Expected output: 1337 as varint: b90a; record bytes: 33; first tag byte 0x0a: field 1, wire type 2 (2 = length-prefixed).
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): proxy قدیمی پیام را field-by-field به پیام جدید کپی میکند؛ چه دادهای گم میشود؟Follow-up (Senior): An old proxy copies recognized fields into a fresh message. What data disappears?
بایتهای تگ ۴ اصلاً به پیام تازه نمیرسند: پراکسی به مدلی decode میکند که فقط فیلدهای ۱ تا ۳ را دارد، بعد از همان مدل دوباره encode میکند. email برای هر خواننده پاییندستی رفته، با اینکه هیچ خطایی رخ نداده. راهحل: بهجای کپی فیلدهای شناختهشده، بایتهای ناشناخته را دستنخورده عبور بده (یا schema پراکسی را بهروز کن).
The bytes for tag 4 never make it into the fresh message: the proxy decodes into a model that only has fields 1–3, then re-encodes from that model. The email is gone for every downstream reader, even though nothing errored. The fix is to forward opaque bytes (or upgrade the proxy's schema) instead of copying recognized fields.
اشتباهات رایجCommon mistakes
- حفظ کردن ۸۱/۶۶/۳۳ بهجای مکانیزم. اندازهها با نام و مقدار فیلدها عوض میشوند؛ مصاحبه قرارداد decoder را نمره میدهد.
- گفتن «Protobuf همیشه فیلد ناشناخته را نگه میدارد». فقط runtime درست در مسیر باینری این کار را میکند — تبدیل JSON و کپیهای دستی حذفشان میکنند.
- یکی دانستن سازگاری سیمی با سازگاری کد. کد قدیمی میتواند بایتهایی را عبور دهد که نمیفهمد — اصل سازگاری forward همین است.
- Memorizing 81/66/33 instead of the mechanism. Sizes change with field names and values; the decoder contract is what the interview grades.
- Saying 'Protobuf always preserves unknown fields'. Only a proper runtime on the binary path does — JSON conversion and hand-rolled copies drop them.
- Treating wire compatibility as code compatibility. Old code can forward bytes it cannot interpret — that is the whole point of forward compatibility.
منابع:Sources: Protobuf · proto3 language guide · Atlassian · engineering interview handbook
22 تمرین ۲ — تکاملِ schema زیرِ فشارExercise 2 — schema evolution under pressure
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- رکورد آموزشی پروفایل کاربر با نام، سن و یک راه تماس (تلفن/email)؛ هر جهت پایین نویسنده و خوانندهاش را صریح میگوید.
- مگر خلافش گفته شود مسیرها باینریاند؛ رندر JSONای Protobuf (تکراریها بهصورت آرایه) قواعد متفاوت و پراتلافتری دارد و هرجا که گاز میگیرد گفته میشود.
- بررسیهای سازگاری registry و پنجرههای انتشار پایین اعداد سیاستی آموزشیاند، نه ردپای اندازهگیریشده.
- Teaching user-profile record with a name, an age and one phone/email contact; every direction below names its writer and reader explicitly.
- Binary paths unless noted; JSON rendering of Protobuf (repeated as arrays) follows different, lossier rules and is called out where it bites.
- Registry compatibility checks and rollout windows below are teaching policy numbers, not measured traces.
چرا این روش و این عددها؟Why this method and these numbers?
سازگاری را جهتدار میسنجیم چون reader تازه و reader قدیم اطلاعات یکسانی ندارند؛ alias تازه نمیتواند reader قدیم را تغییر دهد. در Protobuf tag هویت wire است، برای همین rename باینری با تغییر tag یکسان نیست. تبدیل چند مقدار به یک مقدار ممکن است parse شود ولی بقیه را دور بریزد؛ پس معیار اصلی حفظ معناست. فیلد تازه مسیر rollback میدهد چون مصرفکنندهٔ قدیم هنوز فیلد قبلی را میخواند؛ حذف آن فقط بعد از خروج آخرین reader قدیمی مجاز است.
Compatibility is directional because new and old readers know different schemas; a new alias cannot teach an old reader. Protobuf wire identity is the tag, so binary renaming differs from changing tags. Reading many values as one may parse yet discard values, making semantics the criterion. A new field preserves rollback while old readers use the old field; remove it only after the last old reader retires.
مدل ذهنیMental model
حل گامبهگامStep by step
- افزودن: امن در هر دو سمت افزودن اختیاری در هر دو قالب: Protobuf تگ تازه میدهد — خوانندههای قدیمی بایتهایش را رد میکنند و نگه میدارند (forward)، خوانندههای جدید روی داده قدیمی پیشفرض میگذارند (backward). Avro فیلدی با default خواننده اضافه میکند — خوانندههای جدید رکوردهای قدیمی را پر میکنند، قدیمیها اضافه را نادیده میگیرند. تگهای بازنشسته را در فهرست
reservedبگذار و هرگز دوباره استفاده نکن. - تغییرنام: آزاد در برابر فقط-backward تغییرنام، نامتقارن: تغییرنام در Protobuf هیچ هزینهای ندارد چون نامها هرگز به سیم نمیرسند — همان تگ، هر دو جهت سالم. Avro با نام جفت میکند، پس خواننده جدید به
aliasنام قدیمی نیاز دارد: جدید-قدیمیخوان کار میکند (backward)، ولی خوانندههای قدیمی روبهروی داده تازهنوشته نام تازه را جفت نمیکنند (forward میشکند). هرگز فرض نکن alias هر دو جهت کار میکند. - تعداد: هرگز بازنوع نکن تغییر تعداد درجا، ناامن: تبدیل تکی به
repeatedدر Protobuf به نوع فیلد، packed بودن فرم سیمی و جهتی که از مرز نسخهها میگذرد وابسته است — ارتقای عددی unpackشده ممکن است لنگلنگان کار کند ولی رشتهها و فرمهای packed گم میکنند یا بههم میریزند؛ Avro هیچ قاعده resolutionای ندارد که یک مقدار را به چند مقدار تبدیل کند. parse شدن بیخطا اینجا هیچچیز را درباره مقدارها ثابت نمیکند. - مهاجرت امن با فیلد تازه مهاجرت کن:
emailsرا کنارemailاضافه کن، در پنجره سازگاری هر دو را بنویس، خوانندهها را گروهبهگروه به فیلد تازه ببر، بعد قدیمی را deprecated کن و — فقط وقتی آخرین مصرفکننده قدیمی رفت — حذفش کن. اول خوانندهها، بعد نویسندهها، حذف آخر. - تست و rollback هر گام را پیش از deploy با بررسی سازگاری registry دروازهبانی کن و rollbackای داشته باش که نسخه قبلی schema بهعلاوه باینری قبلی را برمیگرداند؛ در طول پنجره خطاهای خواندن هر نسخه و شمارندههای حذف فیلد ناشناخته را بپا.
- Add: safe on both sides Add optional, both formats: Protobuf assigns a fresh tag — old readers skip and preserve its bytes (forward), new readers default it on old data (backward). Avro adds a field with a reader default — new readers fill old records, old readers ignore the extra. Remove retired tags to a
reservedlist and never reuse them. - Rename: free vs backward-only Rename, asymmetric: Protobuf renames cost nothing because names never reach the wire — same tag, both directions fine. Avro matches by name, so the new reader needs an
aliasof the old name: new-reads-old works (backward), but old readers facing new-written data never pair the fresh name (forward fails). Never assume the alias works both ways. - Cardinality: never retype Cardinality in place, unsafe: Protobuf singular-to-
repeateddepends on the field type, whether the wire form is packed, and which direction crosses versions — numeric unpacked upgrades can limp along while strings and packed forms lose or garble; Avro has no resolution rule that turns one value into many. Parsing without error here proves nothing about the values. - The safe migration Migrate with a NEW field: add
emailsalongsideemail, dual-write both during the compat window, move readers to the new field one cohort at a time, then deprecate and — only after the last old consumer is gone — remove the old field. Readers-first, writers-second, drop-last. - Test and rollback Gate every step with a registry compatibility check before deploy and a rollback that restores the previous schema version plus the previous binary; monitor per-version read errors and unknown-field-drop counters during the window.
طراحی مشخصConcrete design
پروتو: message Profile { string name = 1; int32 age = 2; string email = 3; } — تغییرنام name آزاد، افزودن repeated string emails = 4 (تگ تازه)، تگهای بازنشسته reserve. Avro: رکورد با name و age دارای default و فیلد union تازه emails با پیشفرض خالی؛ تغییرنام فقط با aliases: ["oldName"] روی خواننده جدید. انتشار: بررسی registry ← deploy خوانندههای متحمل ← نوشتن دوگانه قدیم+جدید ← کوچ گروههای خواننده ← deprecated ← حذف پس از مهلت مصرفکنندههای عقبمانده.
Proto: message Profile { string name = 1; int32 age = 2; string email = 3; } — rename name freely, add repeated string emails = 4 (fresh tag), reserve retired tags. Avro: record with name, age with defaults, and the new union field emails defaulting empty; rename via aliases: ["oldName"] on the new reader only. Rollout: registry check → deploy tolerant readers → dual-write old+new → migrate reader cohorts → deprecate → remove after the lagging-consumer deadline.
مثال محاسبهشدهWorked example
| تغییر (نویسنده به خواننده) | Protobuf | Avro |
|---|---|---|
| افزودن اختیاری | B هست، F هست | B هست، F هست (با default) |
| تغییرنام | B هست، F هست | B هست (با alias)، F نیست |
| تکی به چندتایی درجا | وابسته به نوع و جهت: ممنوع | بدون قاعده: ممنوع |
| تکی به چندتایی با فیلد تازه | پس از نوشتن دوگانه هر دو هست | پس از نوشتن دوگانه هر دو هست |
| Change (writer to reader) | Protobuf | Avro |
|---|---|---|
| Add optional | B ok, F ok | B ok, F ok (needs default) |
| Rename | B ok, F ok | B ok (alias), F no |
| One to many, in place | type/packing/direction-dependent: do not | no rule: do not |
| One to many, new field | B ok, F ok after dual-write | B ok, F ok after dual-write |
دانلود نمودار مستقلDownload standalone diagram
- دو گره اول: پیشنویس تغییر و دروازه بررسی registry — ناسازگار برگردد، سازگار جلو برود.
- سه گره میانی: خوانندههای متحمل، نوشتن دوگانه، کوچ گروهبهگروه — ترتیب امن همین است.
- دو خروجی آخر: بازنشستگی فقط پس از رفتن آخرین قدیمی؛ جهش خطا به rollback میرسد.
- First two nodes: draft the change, then the registry gate — incompatible loops back, compatible moves on.
- Middle three nodes: tolerant readers, dual-write, cohort-by-cohort move — this order is the safe one.
- Final two exits: retirement only after the last old reader is gone; an error spike reaches rollback.
نقطهٔ تصمیم: نقطه تصمیم دروازه registry است: بدون حکم سازگار، هیچ deployای.
رد خرابی: حذف زودهنگام فیلد قدیمی یا تغییر درجا — هر دو در این ریل جایی ندارند و به سطر قرمز ماتریس میرسند.
تضمین نشاندادهشده: شکل ترتیب انتشار را نشان میدهد؛ مهلت حذف را تلهمتری مصرفکننده تعیین میکند، نه شکل.
تضمین نشانندادهشده: حکم هر تغییر خاص (نوع، packing، جهت) را نشان نمیدهد — آن کار بررسی registry است.
Decision point: The decision point is the registry gate: no compatible verdict, no deploy.
Failure trace: Early removal of the old field or an in-place retype — neither has a place on this rail and both land on the matrix's red row.
Guarantee shown: The figure shows deploy order; consumer telemetry sets the removal deadline, not the figure.
Not shown: It does not verdict any specific change (type, packing, direction) — that is the registry check's job.
خودآزماییSelf-check
چرا rollback هم schema و هم باینری را برمیگرداند؟
پاسخ: چون خواننده و نویسنده جفتاند: schema تازه با باینری قدیمی (یا برعکس) همان ناسازگاری را برمیگرداند.
Why does rollback restore both the schema and the binary?
Answer: Because reader and writer are a pair: a new schema with an old binary (or vice versa) brings the same incompatibility back.
دانلود SVG مستقلDownload standalone SVG
- اول سربرگ: B یعنی خواننده جدید با نویسنده قدیمی، F یعنی خواننده قدیمی با نویسنده جدید — هر خانه پایین را با همین عینک بخوان.
- سطر سبز: افزودن در هر دو سمت امن است، با تگ تازه در چپ و default خواننده در راست.
- سطر دوشقه: تغییرنام در سمت تگ آزاد است ولی در سمت نام با alias و فقط backward.
- سطر قرمز بهعلاوه پانوشت: تعداد از مسیر فیلد تازه و نوشتن دوگانه میگذرد — تنها parse شدن هرگز ایمنی را ثابت نمیکند.
- Header first: B means the new reader with the old writer, F the old reader with the new writer — every cell below is read in that light.
- Green row: adding is safe on both sides, with a fresh tag on the left and a reader default on the right.
- Split row: rename is free on the tag side but alias-backed and backward-only on the name side.
- Red row plus footer: cardinality goes through a new field and dual-write — parsing alone never proves safety.
نقطهٔ تصمیم: نقطه تصمیم سطر سوم است: هر تغییر تعداد باید از ستون «فیلد تازه + نوشتن دوگانه» بگذرد — هرگز از «درجا».
رد خرابی: سطر قرمز را دنبال کن: تغییر تعداد درجا در هر دو سمت parse میشود ولی مقدارها را خراب میکند — ماتریس دقیقاً همانجا «ممنوع» نشان میدهد و پیام پانوشت («قابلتجزیه بودن همان بیاتلاف بودن نیست») نام تله را میگوید.
تضمین نشاندادهشده: شکل حکمهای صریح با جهت را برای سه تغییر تضمین میکند. همه ترکیبهای نوع و packing را برنمیشمارد — آن کار بررسی registry است.
تضمین نشانندادهشده: درباره رندرهای JSON چیزی نمیگوید، جایی که فیلدهای تکراری و پیشفرضها قواعدی متفاوت از مسیرهای باینری شکل دارند.
Decision point: The decision point is the third row: any cardinality change must leave the 'new field + dual-write' column — never the 'in place' one.
Failure trace: Trace the red row: an in-place cardinality change parses on both sides yet corrupts values — the matrix shows 'do not' exactly there, and the footer moral ('parseable is not lossless') names the trap.
Guarantee shown: The figure guarantees direction-explicit verdicts for the three changes. It does not enumerate every type-and-packing combination — that is what the registry check is for.
Not shown: It says nothing about JSON renderings, where repeated fields and defaults follow different rules than the binary paths shown.
خودآزماییSelf-check
کدام سطر بین دو ستون نامتقارن است و علتش چیست؟
پاسخ: سطر تغییرنام: Protobuf هر دو جهت سبز میماند ولی Avro فقط-backward نشان میدهد. نامتقارنی از این میآید که نامها روی سیم Avro زندگی میکنند (جفتشدن با نام) ولی روی سیم Protobuf نه (جفتشدن با تگ).
Which row is asymmetric between the two columns, and what causes it?
Answer: The rename row: Protobuf stays green both ways while Avro shows backward-only. The asymmetry comes from names living on the Avro wire (matched by name) but not on the Protobuf wire (matched by tag).
گزینههای جایگزینAlternatives
- بازنویسی یکجای داده ذخیرهشده به schema تازه: بعدش برای همیشه یک نسخه، به قیمت downtime مهاجرت و کپیهای آرشیوی که هنوز شکل قدیمی را دارند.
- Stop-the-world rewrite of stored data to the new schema: one version forever after, at the cost of migration downtime and archived copies that still carry the old shape.
سناریوهای خرابیFailure scenarios
- تغییر تکیبهچندتایی درجا روی رشتهها از مسیر رندر JSON: رشتههای تکی قدیمی و آرایههای تازه هر دو جهت parse میشوند ولی مقدارها خراباند — بدترین خرابی چون مانیتورها سبز میمانند.
- حذف فیلد قدیمی وقتی مصرفکنندههای عقبمانده هنوز میخوانندش: decodeهایشان پیشفرض میگذارد یا داده موردنیاز را میاندازد — حذف را با شاهد عقبماندگی مصرفکننده زمانبندی کن، نه با تقویم.
- In-place singular-to-repeated on strings via JSON rendering: old single strings and new arrays parse in both directions yet values corrupt — the worst failure because monitors stay green.
- Removing the old field while lagging consumers still read it: their decodes default or drop data they still need — schedule removal by consumer-lag evidence, not by calendar.
کد قابل اجراRunnable code
اجراشده و تأییدشده در همین محیطExecuted and verified in this environment
این کد آموزشی و سادهسازیشده است؛ فقط برای آشنایی با ایده. کد واقعی پروداکشن ملاحظات بسیار بیشتری دارد و هیچکس اینشکلی کد پروداکشن نمیزند.Simplified teaching code for building intuition only — not production code; nobody ships production code like this.
public class CompatDemo {
// Teaching model of the chapter's evolution rules (not the real
// libraries): prints the verdict matrix and measures packed vs
// unpacked repeated-int32 bytes to show why packing matters.
static int varintLen(int n) {
int l = 1;
while ((n >>>= 7) != 0) {
l++;
}
return l;
}
public static void main(String[] args) {
int[] vals = {25, 31, 44};
int unpacked = 0;
int packedBody = 0;
for (int v : vals) {
unpacked += 1 + varintLen(v);
packedBody += varintLen(v);
}
int packed = 1 + varintLen(packedBody) + packedBody;
System.out.println("repeated int32 x3 unpacked bytes: " + unpacked);
System.out.println("repeated int32 x3 packed bytes: " + packed);
String[][] rows = {
{"add optional", "B ok F ok", "B ok F ok (default)"},
{"rename", "B ok F ok", "B ok (alias) F no"},
{"one-to-many in place", "depends: do not", "no rule: do not"},
};
for (String[] r : rows) {
System.out.println(r[0] + " | protobuf " + r[1] + " | avro " + r[2]);
}
if (unpacked != 6 || packed != 5) {
throw new AssertionError("packing math moved");
}
System.out.println("packing check: unpacked=6 packed=5, verdicts as taught");
}
}
اجرا: javac CompatDemo.java && java CompatDemo — فقط استاندارد جاوا (JDK 27)، فوری. این برنامه مدل آموزشی قواعد فصل است، نه خود کتابخانهها؛ بررسی واقعی با Schema Registry محلی آزمایشگاه انجام میشود.
خروجی مورد انتظار: تکراری x3: unpacked برابر ۶ بایت و packed برابر ۵ بایت؛ سپس سه سطر حکم (افزودن، تغییرنام، تعداد درجا) و تأیید packing check.
Run: javac CompatDemo.java && java CompatDemo — Java stdlib only (JDK 27), instant. This program is a teaching model of the chapter's rules, not the libraries themselves; the real check runs against a lab Schema Registry.
Expected output: repeated int32 x3 unpacked bytes: 6; repeated int32 x3 packed bytes: 5; add optional | protobuf B ok F ok | avro B ok F ok (default); rename | protobuf B ok F ok | avro B ok (alias) F no; one-to-many in place | protobuf depends: do not | avro no rule: do not; packing check: unpacked=6 packed=5, verdicts as taught.
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (سینیور): consumerهای قدیمی ماهها عقباند؛ چه زمانی tag یا فیلد قدیمی را حذف میکنی؟Follow-up (Senior): Old consumers are months behind. When can you remove the old field or tag?
فقط وقتی تلهمتری ثابت کند هیچ خوانندهای نیازش ندارد: صفر خواندن تگ/فیلد قدیمی در یک پنجره نگهداشت کامل (پنجره آموزشی: ماهها عقبماندگی مصرفکننده)، registry آن را deprecated کرده، و آرشیوها یا مهاجرت کردهاند یا به خوانندههای کد قدیمی قرنطینه شدهاند. تا آن روز تگ reserved میماند و خواننده متحمل.
Only when telemetry proves no reader needs it: zero reads of the old tag/field across a full retention window (teaching window: months of consumer lag), the registry marks it deprecated, and archives are either migrated or quarantined to old-code readers. Until then the tag stays reserved and the reader stays tolerant.
اشتباهات رایجCommon mistakes
- فرض اینکه alias در Avro هر دو جهت کار میکند. فقط به خواننده جدید نام قدیمی را میآموزد؛ خوانندههای قدیمی از نام تازه بیخبر میمانند.
- گفتن تکیبهچندتایی «سازگار است چون parse میشود». قابلتجزیه بودن بدون نگاشت بیاتلاف مقدار، مانیتور سبز روی داده خراب است.
- افزودن فیلد Avro بدون default و مقصر دانستن registry. قاعده صریح است: بدون default، سازگاری backward نیست.
- Assuming an Avro alias works both ways. It only teaches the NEW reader the old name; old readers stay ignorant of the new one.
- Calling singular-to-repeated 'compatible because it parses'. Parseability without a lossless value mapping is a green monitor over corrupt data.
- Adding an Avro field without a default and blaming the registry. The rule is explicit: no default, no backward compatibility.
منابع:Sources: Protobuf · proto3 language guide · Avro 1.12 · schema resolution
33 تمرین ۳ — از تئوری تا عملیاتExercise 3 — from theory to operations
بازگشت به صورت تمرینBack to the exercise · ارزیابی و سطحAssessment and level
پاسخ ۶۰ثانیهایThe 60-second answer
صورت مسئله و فرضهاProblem and assumptions
- گردشکار آموزشی سهگامی سفارش: رزرو موجودی، شارژ کارت، ارسال مرسوله. کرش هرجا ممکن است، از جمله میان اثر و نوشتن لاگ — بدترین حالت.
- درگاه پرداخت کلید idempotency میپذیرد؛ ارسالکننده طرح پایه روی شناسه گام ما تکراریزدایی میکند. ارسالکننده بدون کلید سناریوی پیگیری است.
- اعداد هزینه recovery پایین کرانهای آموزشی همین گردشکارند، نه ردپای اندازهگیریشده.
- Teaching three-step order workflow: reserve stock, charge card, ship parcel. Crash may strike anywhere, including between the effect and the log append — the worst case.
- Payment gateway accepts an idempotency key; the base-design shipper dedups on our step id. The keyless shipper is the follow-up scenario.
- Recovery-cost numbers below are teaching bounds for this workflow, not measured traces.
چرا این روش و این عددها؟Why this method and these numbers?
نتیجهٔ step باید ثبت شود تا replay همان تصمیم را ادامه دهد؛ اما crash پس از charge و پیش از ثبت نتیجه هنوز مبهم است. کلید ثابتِ همان step به gateway اجازه میدهد retry را همان عملیات بداند؛ شناسهٔ تازه در هر retry این خاصیت را از بین میبرد. نسخهٔ workflow در invocation ثبت میشود تا replay بعد از deploy کد جدید مسیر دیگری نرود. تعداد step و هزینهٔ recovery، ویژگی سناریو هستند و باید با تعداد invocation و مدت retention مقیاس داده شوند.
Persist each step result so replay follows the original decision. A crash after charging but before logging is still ambiguous; a stable step key lets the gateway recognize the retry, while a new key per retry defeats it. Bind workflow version to invocation so deployments do not change replay paths. Recovery cost scales with invocation count, step count and retention, not a universal constant.
مدل ذهنیMental model
حل گامبهگامStep by step
- شناسه پایدار شناسه هر گام از سه تکه ساخته میشود: شناسه سفارش + نام گام + نسخه کد (
order:1001:ship:v1). در retry همان شناسه تکرار میشود، پس اثر دوباره با همان کلید به مقصد میرسد و تکراریزدایی ممکن میشود. - اول لاگ، بعد اعلام نتیجه هر گام موفق پیش از اعلام موفقیت در لاگ پایدار نوشته میشود. اگر کرش میان اثر و لاگ بیفتد، گام «باز» میماند و دوباره اجرا میشود — برای همین اثرها باید idempotent باشند.
- بازپخش و تحویل تکراری restart تاریخچه را میخواند و با همان شناسهها قدم میزند: reserve و charge رد میشوند (نتیجه قدیمی برمیگردد) و فقط ship باز اجرا میشود. تحویل تکراری نتیجه ship هم فقط reuse است، نه مرسوله دوم.
- جبران روبهجلو اگر ship پس از charge ناممکن شود، تاریخ به عقب برنمیگردد: گام تازه refund شارژ را برمیگرداند و release موجودی را آزاد میکند. هر جبران خودش شناسه پایدار و نتیجه ثبتشده دارد.
- نسخهبندی و هزینه تغییر ترتیب گامها، شاخهبندی یا payload با نسخه تازه (v2) منتشر میشود؛ invocationهای در جریان با v1 تمام میشوند چون تاریخچه v1 فقط با کد v1 قدم زده میشود. هزینه recovery: بازخوانی تاریخچه (کران آموزشی: تعداد گامها) بهعلاوه جاروی مغایرتگیری برای ارسالکننده بدون کلید.
- Stable ids Each step id has three parts: order id + step name + code version (
order:1001:ship:v1). Retries repeat the same id, so a repeated effect reaches the destination under the same key and dedups. - Log first, then report Each successful step's result lands in the durable log before success is reported. A crash between effect and log leaves the step 'open' to run again — which is why effects must be idempotent.
- Replay and duplicates The restart loads history and re-walks with the same ids: reserve and charge are skipped (old results returned) and only ship re-runs. Duplicate delivery of the ship result is also just reuse, never a second parcel.
- Forward compensation If shipping becomes impossible after charging, history does not rewind: a fresh refund step returns the charge and a release step frees the stock. Each compensation carries its own stable id and logged result.
- Versioning and cost Step-order, branch or payload changes ship as a new version (v2); in-flight invocations finish on v1 because v1 history is only ever walked by v1 code. Recovery cost: re-reading history (teaching bound: number of steps) plus a reconciliation sweep for the keyless shipper.
طراحی مشخصConcrete design
اجزا: orchestrator (زمانبندی، مالک دروازه نسخه)، لاگ پایدار (write-ahead، تاریخچه هر invocation)، اجراکنندههای گام (رزرو/شارژ/ارسال، هرکدام کلیددار)، جبرانکنندهها (بازپرداخت، آزادسازی، لغو مرسوله). مسیر نوشتن: اجرای گام ← نوشتن نتیجه ← اعلام. مسیر خواندن (بازپخش): بارگذاری تاریخچه ← قدمزدن دوباره کد با همان شناسهها ← ثبتشده رد، باز اجرا. کلیدها: order:{id}:reserve:{v} و charge و ship.
Components: orchestrator (schedules, owns the version gate), durable log (write-ahead, per-invocation history), step executors (reserve/charge/ship, each keyed), compensation handlers (refund, release, cancel-parcel). Write path: execute step → append result → report. Read path (replay): load history → re-walk code with same ids → skip logged, run open. Keys: order:{id}:reserve:{v}, order:{id}:charge:{v}, order:{id}:ship:{v}.
مثال محاسبهشدهWorked example
اجرای آموزشی order-1001 (خروجی واقعی برنامه): reserve و charge اجرا و ثبت میشوند؛ کرش پیش از ship میزند؛ restart هر دو را با skip, reuse بازپخش میکند؛ ship یک بار اجرا و ثبت میشود؛ تحویل تکراری ship همان parcel-9 را استفاده میکند. حالت پایانی: ۳ کلید ثبتشده، ۱ مرسوله، ۱ شارژ — هزینه بازپخش ۲ خواندن تاریخچه، ۰ اثر تکراری.
Teaching run order-1001 (real program output): reserve and charge execute and log; the crash hits before ship; the restart replays both as skip, reuse; ship executes once and logs; a duplicate ship delivery reuses parcel-9. End state: 3 logged keys, 1 parcel, 1 charge — replay cost 2 history reads, 0 repeated effects.
دانلود نمودار مستقلDownload standalone diagram
- نوار اول: شارژ، ثبت نتیجه C، بعد ship — و کرش پیش از ثبت نتیجه S.
- نوار دوم با retry شروع میشود؛ موتور از لاگ میپرسد: C انجامشده، S باز.
- فقط ship با همان شناسه دوباره میرود و «parcel-9 (deduped)» برمیگردد؛ پایان «order complete».
- First band: charge, log result C, then ship — and the crash lands before result S is logged.
- Band two opens with the retry; the engine asks the log: C done, S open.
- Only ship goes again under the same id and 'parcel-9 (deduped)' returns; the end reads 'order complete'.
نقطهٔ تصمیم: نقطه تصمیم پرسش از لاگ است: کدام گام باز است؟ فقط همان اجرا میشود.
رد خرابی: اگر نتیجه C ثبت نشده بود، شارژ هم دوباره میرفت — و کلید idempotency درگاه جلوی برداشت دوم را میگرفت.
تضمین نشاندادهشده: شکل قرارداد بازپخش را نشان میدهد: رد ثبتشدهها، اجرای بازها.
تضمین نشانندادهشده: اثر دقیقاً یکبار را نشان نمیدهد — آن به کلید مقصد بیرونی وابسته است.
Decision point: The decision point is the log lookup: which step is open? Only that one runs.
Failure trace: Had result C never been logged, charging would re-run too — and the gateway idempotency key would block the second debit.
Guarantee shown: The figure shows the replay contract: skip the logged, run the open.
Not shown: It does not show exactly-once effects — those depend on the outside destination's key.
خودآزماییSelf-check
چرا موتور پس از کرش شارژ را دوباره اجرا نمیکند؟
پاسخ: چون نتیجه C در لاگ پایدار است؛ بازپخش ثبتشده را رد میکند و نتیجه قبلی را برمیگرداند.
Why doesn't the engine re-run the charge after the crash?
Answer: Because result C sits in the durable log; replay skips logged steps and returns the old result.
دانلود نمودار مستقلDownload standalone diagram
- مسیر اصلی پنج حالت دارد: Triggered، Running، Result Logged، Version Gate، Done — همه در نوار main.
- کرش از Running به Retrying میرود (همان ستون) و با همان شناسه برمیگردد؛ پایان بودجه یعنی Failed.
- دروازه نسخه تاریخچه قدیمی را به کد قدیمی میخ میکند؛ بنبست به Compensating و بعد Compensated پایانه میرسد.
- The main path holds five states: Triggered, Running, Result Logged, Version Gate, Done — all on the main lane.
- A crash leaves Running for Retrying (same column) and returns under the same id; a spent budget means Failed.
- The version gate pins old history to old code; a dead end reaches Compensating, then terminal Compensated.
نقطهٔ تصمیم: دو نقطه تصمیم: دروازه نسخه (کدام کد قدم میزند؟) و خروج از Running (retry یا جبران؟).
رد خرابی: جبران ناموفق در همان حلقه retry میماند تا بودجه؛ invocation «منتظر اقدام» میشود، نه رها.
تضمین نشاندادهشده: شکل نشان میدهد هر invocation دقیقاً در یک پایانه میایستد: Done، Failed یا Compensated.
تضمین نشانندادهشده: ترتیب پیامهای واقعی روی سیم را نشان نمیدهد — آن کار نمودار توالی است.
Decision point: Two decision points: the version gate (which code walks?) and the exit from Running (retry or compensate?).
Failure trace: A failed compensation stays in the retry loop until the budget; the invocation becomes 'awaiting action', never abandoned.
Guarantee shown: The figure shows every invocation ends in exactly one terminal: Done, Failed or Compensated.
Not shown: It does not show the actual message order on the wire — that is the sequence diagram's job.
خودآزماییSelf-check
چرا Compensated در نوار terminal است ولی Done در main؟
پاسخ: هر دو پایانهاند ولی معنای متفاوت: Done موفقیت مسیر اصلی است، Compensated پایان مسیر جبران پس از بنبست.
Why is Compensated on the terminal lane while Done stays on main?
Answer: Both are terminal but mean different things: Done is main-path success, Compensated ends the compensation path after a dead end.
دانلود SVG مستقلDownload standalone SVG
- سطر main: پنج جعبه با guard روی هر یال — فقط گام ثبتشده جلو میرود.
- نوار آبی: قانون retry — همان شناسه، استفاده از نتیجه ثبتشده.
- پایین: دروازه نسخه (بازپخش v1 با کد v1) و مسیر جبران به Compensated پایانه.
- دو سطر پانوشت: تغییرها نسخه تازه میخواهند؛ بدون کلید ارسال، retry کور ممنوع و مغایرتگیری لازم.
- Main row: five boxes with a guard on each edge — only a logged step advances.
- Blue strip: the retry rule — same step id, reuse of the logged result.
- Below: the version gate (v1 replay on v1 code) and the compensation path to terminal Compensated.
- Two footer lines: changes need a new version; without a ship key, blind retry is banned and reconciliation is required.
نقطهٔ تصمیم: نقطه تصمیم هر یال است: guard برقرار و نتیجه ثبت شده؟
رد خرابی: کرش میان اثر و لاگ را روی فلش ship دنبال کن: گام باز میماند، دوباره با همان شناسه میرود.
تضمین نشاندادهشده: شکل کل استدلال را یکجا جمع میکند: شناسه، لاگ، جبران، نسخه.
تضمین نشانندادهشده: زمانبندی دقیق کرش و ترتیب پیامها را نشان نمیدهد — آنها در دو نمودار گاماند.
Decision point: The decision point is every edge: guard holds and result logged?
Failure trace: Trace a crash between effect and log on the ship arrow: the step stays open and goes again under the same id.
Guarantee shown: The figure gathers the whole argument on one page: ids, log, compensation, versions.
Not shown: It does not show exact crash timing or message order — those live in the two step diagrams.
خودآزماییSelf-check
کدام دو جمله پانوشت، دو پیگیری مصاحبه را جواب میدهند؟
پاسخ: «تغییرها نسخه تازه» (نسخهبندی invocationهای در جریان) و «بدون کلید، retry کور ممنوع» (تضمین ضعیفشده ارسال).
Which two footer lines answer the two interview follow-ups?
Answer: 'Changes need a new version' (versioning in-flight invocations) and 'no key, no blind retry' (the weakened shipping guarantee).
گزینههای جایگزینAlternatives
- کوروگرافی رویدادی بدون orchestrator: هر سرویس به رویداد قبلی واکنش نشان میدهد. هزینه: تاریخچه پراکنده، بازپخش و نسخهبندی سختتر، جبران بدون مالک واحد.
- Event choreography with no orchestrator: each service reacts to the previous event. Cost: scattered history, harder replay and versioning, compensation with no single owner.
سناریوهای خرابیFailure scenarios
- کرش میان اثر ship و نوشتن لاگ: ship دوباره اجرا میشود. با تکراریزدایی روی شناسه، مرسوله دوم ساخته نمیشود؛ بدون کلید، جاروی مغایرتگیری مرسولههای بیصاحب را به سفارش وصل میکند.
- بازپخش تاریخچه v1 با کد v2 جابهجاشده: دروازه نسخه جلوی قدمزدن اشتباه را میگیرد و invocation را به کد v1 میخ میکند.
- شکست خود جبران (مثلاً refund ناموفق): جبران retry میشود با همان شناسه؛ پس از پایان بودجه، هشدار انسانی با حالت دقیق لاگ — invocation نه «نیمهتمام رها» بلکه «منتظر اقدام» است.
- Crash between the ship effect and the log write: ship re-runs. With dedup on the step id no second parcel is made; without a key, the reconciliation sweep attaches orphan parcels to the order.
- Replaying v1 history against reordered v2 code: the version gate blocks the wrong walk and pins the invocation to v1 code.
- The compensation itself fails (e.g. refund rejected): it retries under the same id; after the budget, a human alert with the exact log state — the invocation is 'awaiting action', never 'abandoned halfway'.
کد قابل اجرا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.LinkedHashMap;
import java.util.Map;
import java.util.TreeSet;
import java.util.function.Supplier;
public class WorkflowDemo {
// Durable log: step id -> result. Teaching stand-in for a write-ahead log.
static Map<String, String> log = new LinkedHashMap<>();
static String runStep(String id, Supplier<String> fn) {
if (log.containsKey(id)) {
System.out.println("replay " + id + ": skip, reuse " + log.get(id));
return log.get(id);
}
String r = fn.get();
log.put(id, r); // append BEFORE reporting success
System.out.println("exec " + id + ": " + r + " (logged)");
return r;
}
public static void main(String[] args) {
String wid = "order-1001";
runStep(wid + ":reserve:v1", () -> "stock-42 reserved");
runStep(wid + ":charge:v1", () -> "pay-7 charged");
System.out.println("-- crash before ship; restart replays --");
runStep(wid + ":reserve:v1", () -> "stock-42 reserved");
runStep(wid + ":charge:v1", () -> "pay-7 charged");
runStep(wid + ":ship:v1", () -> "parcel-9 created");
System.out.println("-- duplicate delivery of ship result --");
runStep(wid + ":ship:v1", () -> "parcel-9 created");
System.out.println("durable log keys: " + new TreeSet<>(log.keySet()));
}
}
اجرا: javac WorkflowDemo.java && java WorkflowDemo — فقط استاندارد جاوا (JDK 27)، فوری. فرایند را ذهنی پیش از خط ship بکش: خطهای بازپخش دقیقاً همان چیزی است که restart چاپ میکند.
خروجی مورد انتظار: اجرای reserve و charge با ثبت لاگ؛ کرش پیش از ship؛ بازپخش هر دو با استفاده مجدد از نتیجه ثبتشده؛ اجرای ship با ثبت؛ تحویل تکراری ship با استفاده مجدد؛ کلیدهای لاگ پایدار: هر سه گام نسخه v1.
Run: javac WorkflowDemo.java && java WorkflowDemo — Java stdlib only (JDK 27), instant. Kill the process mentally before the ship line: the replay lines are exactly what a restart prints.
Expected output: exec order-1001:reserve:v1: stock-42 reserved (logged); exec order-1001:charge:v1: pay-7 charged (logged); -- crash before ship; restart replays --; replay order-1001:reserve:v1: skip, reuse stock-42 reserved; replay order-1001:charge:v1: skip, reuse pay-7 charged; exec order-1001:ship:v1: parcel-9 created (logged); -- duplicate delivery of ship result --; replay order-1001:ship:v1: skip, reuse parcel-9 created; durable log keys: [order-1001:charge:v1, order-1001:reserve:v1, order-1001:ship:v1].
آزمون صحت و معیار عملیاتیCorrectness checks and operational criteria
پاسخ سؤالهای پیگیریFollow-up answers
پیگیری (استف): provider ارسال idempotency key ندارد؛ تضمینت چه تغییری میکند؟Follow-up (Staff): The shipping provider has no idempotency key. How does that change your guarantee?
تضمین از «اثر دقیقاً یکبار» به «حداکثر یک مرسوله بیصاحب + مغایرتگیری» میلغزد: retry کور ممنوع، چون هر تلاش ممکن است مرسوله تازه بسازد. بهجایش: ship را یک بار صدا بزن، نتیجه نامشخص را «نامعلوم» ثبت کن، و جاروی مغایرتگیری (تطبیق مرسولهها با شناسه سفارش در سمت خودت) اثر را قطعی کند. هزینه recovery همان جاروست.
The guarantee slides from 'exactly-once effects' to 'at most one orphan parcel + reconciliation': blind retries are banned, since each attempt may mint a fresh parcel. Instead: call ship once, record an unclear outcome as 'unknown', and let a reconciliation sweep (matching parcels to your order id on your side) settle the effect. Recovery cost is that sweep.
اشتباهات رایجCommon mistakes
- قول «exactly-once» برای اثر بیرونی. لاگ پایدار اجرای دقیقاً یکبار میدهد، نه اثر دقیقاً یکبار — اثر به کلید مقصد وابسته است.
- استفاده از عدد تصادفی یا ساعت سیستم در کد workflow. بازپخش باید قطعی باشد؛ همان ورودی، همان قدمها.
- جابهجایی آزاد ترتیب گامها روی invocationهای در جریان. تاریخچه قدیمی فقط با منطق قدیمی قدم زده میشود — تغییر، نسخه تازه میخواهد.
- Promising 'exactly-once' for outside effects. The durable log delivers exactly-once execution, not exactly-once effects — effects depend on the destination's key.
- Using random numbers or the system clock in workflow code. Replay must be deterministic; same input, same walk.
- Freely reordering steps over in-flight invocations. Old history is only ever walked by old logic — change needs a new version.
منابع:Sources: AWS · safe retries with idempotent APIs · Atlassian · engineering interview handbook