File: leader-election/leader_election.py

Date: 2026-05-29

Time: 08:58

leader-election/leader_election.py

Purpose

This file implements the Bully Algorithm for leader election in distributed systems — a classic protocol from DDIA Chapter 8 (The Trouble with Distributed Systems) and Chapter 9 (Consistency and Consensus). It provides both the node-level protocol logic (BullyNode) and a deterministic simulation harness (BullyElectionCluster) that drives the protocol in a single-threaded, tick-based loop. The simulation avoids real networking and timers entirely, making the algorithm testable and observable.

Key Components

Message

A plain data object representing a protocol message. Four message types exist:

| Type | Meaning |

|------|---------|

| ELECTION | "I'm starting an election" — sent to all higher-ID nodes |

| ALIVE | "I'm here, back off" — response from a higher-ID node to an ELECTION |

| COORDINATOR | "I won, I'm the leader" — broadcast by the winner to all nodes |

| HEARTBEAT | "I'm still alive as leader" — periodic liveness signal |

Messages carry senderid, receiverid, term (monotonic election counter), and timestamp (logical tick).

BullyNode

The core protocol state machine. Each node is in one of three states: "follower", "candidate", or "leader".

Key methods:

BullyElectionCluster

The simulation harness. Owns a dict of BullyNode instances keyed by node ID, a logical clock, and election history.

Key methods:

Patterns

Tick-based discrete simulation. No threads, no async, no real timers. Time advances via explicit tick(t) calls, making the protocol fully deterministic and reproducible. This is the standard pattern across this repo's distributed systems implementations.

Message-passing state machine. Nodes communicate exclusively through Message objects returned from receive_message and tick. There's no shared mutable state between nodes — the cluster harness is the message bus.

Eager delivery loop. BullyElectionCluster.tick() delivers messages in a while all_messages loop, processing all cascading responses within a single tick. This models synchronous message delivery — messages sent at time t are received at time t.

Split-brain as post-hoc correction. Rather than trying to prevent split-brain through the protocol alone, the cluster detects it after each tick and resolves it by forcing lower-ID leaders to re-elect. This is a pragmatic simulation choice — real systems would rely on the protocol's properties plus network timeouts.

Dependencies

Imports: None — this is a zero-dependency, pure-Python implementation.

Imported by: leader-election/testleaderelection.py — the test suite exercises election, failure, recovery, and split-brain scenarios.

Flow

A typical election proceeds as:

1. A follower's heartbeat timer expires → tick() calls start_election().

2. start_election sends ELECTION messages to all nodes with higher IDs.

3. If a higher-ID node receives ELECTION, it responds with ALIVE and starts its own election.

4. The original candidate sees ALIVE, sets gotalive = True, and will step down on next tick.

5. The highest available node finds no higher nodes (or no ALIVE responses arrive) and calls declare_victory.

6. declarevictory broadcasts COORDINATOR to all nodes, which accept the new leader and update leader_id.

7. The leader begins sending HEARTBEAT messages every heartbeat_interval ticks.

Recovery flow: When recover_node is called, the node immediately starts an election. If it has the highest ID, it will preempt the current leader — this is the "bully" behavior.

Invariants

Error Handling

There is essentially none — and that's intentional. This is a simulation, not production code. Invalid node IDs in messages are silently dropped (self.nodes.get(msg.receiver_id) returns None). No exceptions are raised. The design assumes the harness is the only caller and will provide valid inputs.

Topics to Explore

Beliefs