Skip to main content

krabka_docgen/
scenarios.rs

1//! Render Krabka's own deterministic consensus simulator into a Mermaid
2//! sequence-diagram "slideshow" markdown page.
3//!
4//! The diagrams here are not hand-drawn. [`failure_scenarios_md`] runs
5//! [`krabka_raft::scenarios::scenarios`] in process and turns each recorded
6//! [`ScenarioTrace`] into a Zola `{% mermaid() %}` paired shortcode. The reader
7//! thus sees exactly what the real KIP-595/996 algorithm did.
8
9use {
10    krabka_raft::{
11        scenarios,
12        scenarios::{ScenarioTrace, TraceAction},
13    },
14    std::fmt::Write as _,
15};
16
17/// The intro paragraph that opens the page.
18const 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/// Render the complete failure-scenarios slideshow page body, without front
27/// matter.
28#[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
39/// Append one scenario: the heading, the summary, the invariant, the diagrams,
40/// and the outcome.
41fn 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    // For the split-brain scenario, lead with the hand-authored "what goes
47    // wrong without quorum" contrast, then the honest generated diagram.
48    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
62/// Render the generated Mermaid sequence diagram for `trace`.
63fn 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
97/// Hand-authored illustrative contrast for the split-brain scenario.
98///
99/// It shows a *naive* leader election WITHOUT a quorum requirement. Two nodes
100/// both believe that they are leader, and their logs diverge. This is the only
101/// non-generated diagram on the page. The contrast makes the generated,
102/// code-backed diagram below it easier to read.
103const 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        // Each of the three scenarios renders one generated `{% mermaid() %}`
156        // sequence diagram, plus the split-brain scenario leads with one
157        // hand-authored contrast diagram — four mermaid blocks in total. This
158        // pins `render_diagram`'s output specifically: if it stopped emitting,
159        // only the single hand-authored contrast block would remain.
160        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        // A generated diagram declares its participants and draws message
167        // arrows — content the hand-authored contrast for a single scenario
168        // cannot account for on its own (e.g. the reordered/duplicate scenarios).
169        check!(md.matches("participant N").count() >= 6);
170        check!(md.contains("->>"));
171    }
172}