krabka_docgen/
scenarios.rs1use {
10 krabka_raft::{
11 scenarios,
12 scenarios::{ScenarioTrace, TraceAction},
13 },
14 std::fmt::Write as _,
15};
16
17const INTRO: &str = "\
19These diagrams are **generated**, not drawn. Each one is produced by running \
20Krabka's own deterministic `KRaft` consensus simulator — the same pure, \
21no-IO state machine the broker runs in production — and recording every \
22message, timeout, partition, and leader election it takes. Because the \
23simulator is deterministic, the diagrams below reflect the *real* algorithm, \
24step for step, rather than an idealized cartoon of it.\n";
25
26#[must_use]
29pub fn failure_scenarios_md() -> String {
30 let mut out = String::new();
31 out.push_str(INTRO);
32 out.push('\n');
33 for trace in scenarios::scenarios() {
34 render_scenario(&mut out, &trace);
35 }
36 out
37}
38
39fn render_scenario(out: &mut String, trace: &ScenarioTrace) {
42 let _ = writeln!(out, "## {}\n", trace.title);
43 let _ = writeln!(out, "{}\n", trace.summary);
44 let _ = writeln!(out, "**Invariant:** {}\n", trace.invariant);
45
46 if trace.id == "split_brain_prevented" {
49 out.push_str(SPLIT_BRAIN_CONTRAST);
50 out.push('\n');
51 let _ = writeln!(
52 out,
53 "**✓ With Krabka's quorum + pre-vote** — the generated trace from the simulator:\n"
54 );
55 }
56
57 render_diagram(out, trace);
58
59 let _ = writeln!(out, "**Outcome:** {}\n", trace.outcome);
60}
61
62fn render_diagram(out: &mut String, trace: &ScenarioTrace) {
64 out.push_str("{% mermaid() %}\n");
65 out.push_str("sequenceDiagram\n");
66 for id in &trace.nodes {
67 let _ = writeln!(out, " participant N{id}");
68 }
69 for step in &trace.steps {
70 match &step.action {
71 TraceAction::Deliver { src, dst, event } => {
72 let _ = writeln!(out, " N{src}->>N{dst}: {event}");
73 }
74 TraceAction::Partition { node } => {
75 let _ = writeln!(out, " Note over N{node}: ✂ partitioned");
76 }
77 TraceAction::Heal { node } => {
78 let _ = writeln!(out, " Note over N{node}: 🔗 healed");
79 }
80 TraceAction::Elected { node, epoch } => {
81 let _ = writeln!(out, " Note over N{node}: 👑 leader epoch {epoch}");
82 }
83 TraceAction::Timeout { node, kind } => {
84 let _ = writeln!(out, " Note over N{node}: ⏰ {kind} timeout");
85 }
86 TraceAction::Append { node, count } => {
87 let _ = writeln!(out, " Note over N{node}: ✏ append {count} record(s)");
88 }
89 TraceAction::Drop { src, dst, event } => {
90 let _ = writeln!(out, " N{src}--xN{dst}: {event} (dropped)");
91 }
92 }
93 }
94 out.push_str("{% end %}\n\n");
95}
96
97const SPLIT_BRAIN_CONTRAST: &str = "\
104**❌ Without quorum — what split-brain looks like:**
105
106{% mermaid() %}
107sequenceDiagram
108 participant N1
109 participant N2
110 participant N3
111 Note over N1: 👑 leader epoch 1
112 Note over N1,N3: ✂ network splits {N1} | {N2,N3}
113 Note over N1: still thinks it is leader
114 N1->>N1: accepts write A (epoch 1)
115 Note over N2: ⏰ election timeout
116 Note over N2: 👑 leader epoch 1 (no quorum check!)
117 N2->>N3: accepts write B (epoch 1)
118 Note over N1,N3: 💥 two leaders, logs diverge (A vs B)
119{% end %}
120
121Krabka prevents this: a candidate must win a **majority** of votes before it \
122can lead, and KIP-996 **pre-vote** stops a partitioned node from disrupting a \
123healthy leader. With three voters, the minority side (one node) can never \
124reach the two-vote majority, so it cannot elect itself.
125";
126
127#[cfg(test)]
128mod tests {
129 use {super::*, assert2::check};
130
131 #[test]
132 fn renders_sequence_diagrams_and_split_brain() {
133 let md = failure_scenarios_md();
134 check!(md.contains("sequenceDiagram"));
135 check!(md.contains("{% mermaid() %}"));
136 check!(md.contains("{% end %}"));
137 check!(md.contains("split-brain") || md.contains("Split-brain"));
138 }
139
140 #[test]
141 fn renders_all_three_scenarios() {
142 let md = failure_scenarios_md();
143 for needle in [
144 "Reordered message delivery",
145 "Duplicate message delivery",
146 "**Invariant:**",
147 "**Outcome:**",
148 ] {
149 assert2::assert!(md.contains(needle));
150 }
151 }
152
153 #[test]
154 fn emits_a_generated_diagram_per_scenario() {
155 let md = failure_scenarios_md();
161 let mermaid_blocks = md.matches("{% mermaid() %}").count();
162 check!(
163 mermaid_blocks == 4,
164 "expected 3 generated diagrams + 1 contrast, got {mermaid_blocks}"
165 );
166 check!(md.matches("participant N").count() >= 6);
170 check!(md.contains("->>"));
171 }
172}