03
The mainstream approaches
1. Consensus-backed (Raft/Paxos). Nodes run a consensus protocol internally. The elected leader of the consensus group is the leader. This is what etcd, Zookeeper, and Consul provide. Downsides: consensus is expensive to run (~5 nodes, majority acks), overkill for simple "pick one cron runner."
2. Lock + fencing token. Store the lock and a monotonic counter in one atomic DB. Every acquire increments the counter. Downstream systems (DBs, APIs) reject stale tokens. Works great if your downstreams can enforce the check.
3. DB row with heartbeat. A leaders table: (resource_id, node_id, expires_at). Each node tries to insert its row with ON CONFLICT DO NOTHING (or equivalent). Winner updates expires_at every few seconds. Losers poll. Simple; works in Postgres.
4. Sticky by resource. Use consistent hashing to map each resource to a node deterministically. "Resource foo always handled by the node whose ID is the clockwise neighbor of hash(foo)." Implicit leader. Rebalances on node add/remove.
Raft leader election (pseudocode)
# Raft: followers → candidate → leader.
# Random election timeout (150–300ms) prevents split votes.
import random
class RaftNode:
def __init__(self, node_id, peers):
self.id = node_id
self.peers = peers
self.term = 0
self.voted_for = None
self.state = "follower"
self.timeout = random.uniform(0.15, 0.30)
def on_timeout(self):
# Followers promote to candidate when leader silent too long
self.state = "candidate"
self.term += 1
self.voted_for = self.id
votes = 1
for p in self.peers:
if p.request_vote(self.term, self.id): votes += 1
if votes > (len(self.peers) + 1) // 2:
self.state = "leader"
self.start_heartbeats()
else:
self.state = "follower" # lost or split
def request_vote(self, term, candidate_id):
if term > self.term and self.voted_for is None:
self.term, self.voted_for = term, candidate_id
return True
return False