raftdemo

package
v0.2.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 3, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

Documentation

Overview

Package raftdemo runs a real leader election in one process, so that the raft widget beside it animates a protocol rather than a script.

What it is

Raft's election half and nothing else: terms and votes, no log, no replicated state machine. The only thing this cluster agrees on is who leads it right now, which is the same reduction candacenet's warden daemon makes and for the same reason — see app/warden/README.md, "Leader election (Raft-style, no log)". Nothing here imports warden: pkg/ may not depend on services/, so a widget example that reached for the daemon's election would put the example on the wrong side of that line. The design is read from it; the code is not.

Every node is a goroutine, and nothing is shared

A cluster of N nodes is N goroutines, plus one for the network and one for the fleet view. Each node goroutine owns its own nodeState — its term, its vote, its role, its liveness bitmasks — and no other goroutine can read or write it. Peers do not call each other; they send messages, and a message that arrives at a full inbox is dropped the way a packet is. There is no mutex in this package and there is nothing for one to guard: every datum has exactly one owner and the owners talk.

That is not an aesthetic choice about concurrency primitives. Raft *is* a message-passing protocol, so a lock standing in for it would be modelling the thing with the wrong shape. The state transitions are pure functions — one per message kind, registered in a table — and the goroutine around them does nothing but deliver, apply and publish, which is why the protocol can be specified without starting a single goroutine.

What comes out

One Snapshot per heartbeat round, on every channel a caller got from Cluster.Subscribe. Its fields are the fleet view the leader itself reports, exactly as warden piggybacks its authoritative cluster view on each heartbeat: while a leader exists the view is the leader's and is marked authoritative, and while none does the view is the observer's own aggregate and is not.

One snapshot is one heartbeat round, which is what makes the widget beside this package honest: the card re-arms its pulses on the snapshot counter, so a pulse crossing an edge is a heartbeat that actually crossed a channel.

Determinism

Election timeouts are jittered — without jitter a cluster of equal timers splits its vote forever — and the jitter comes from a per-node PCG stream seeded from Config.Seed and the node's index. A specification that fixes the seed fixes the jitter; it does not fix the scheduler, so the engine is deterministic enough to demonstrate and not deterministic enough to assert a particular winner. No package-level clock or random source is read anywhere, so two clusters in one test binary do not perturb each other.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrNodeCount is a node count outside [1, maxNodes].
	ErrNodeCount = errors.New("raftdemo: the node count is out of range")

	// ErrHeartbeat is a heartbeat interval that is not positive.
	ErrHeartbeat = errors.New("raftdemo: the heartbeat interval is not positive")

	// ErrElectionTimeout is an election timeout that does not clear the
	// heartbeat interval by enough for a leader to keep its followers quiet.
	ErrElectionTimeout = errors.New("raftdemo: the election timeout does not exceed twice the heartbeat interval")

	// ErrElectionJitter is the livelock: a multi-node cluster whose nodes all
	// time out at exactly the same moment splits its vote every term.
	ErrElectionJitter = errors.New("raftdemo: a cluster of more than one node needs a positive election jitter")

	// ErrUnknownNode names a node this cluster does not have.
	ErrUnknownNode = errors.New("raftdemo: no such node")

	// ErrAlreadyRunning is a second call to Run. One cluster runs once: its
	// goroutines are started against the context Run was given, so a second
	// call would start a second set against a second context and leave two
	// networks delivering into one set of inboxes.
	ErrAlreadyRunning = errors.New("raftdemo: the cluster is already running")
)

The faults this package reports. Each is a sentinel because a caller that wants to tell "you asked for too many nodes" from "you named a node that does not exist" should not have to read an English sentence to do it.

Functions

This section is empty.

Types

type Cluster

type Cluster struct {
	// contains filtered or unexported fields
}

Cluster is one election running in one process.

It holds channels and nothing else — no protocol state, no fleet view, no subscriber list. Each of those belongs to exactly one goroutine started by Cluster.Run, and this struct is how the rest of the program reaches them.

func New

func New(config Config) (*Cluster, error)

New builds a cluster and starts nothing.

Every fault in the configuration is reported here, so a *Cluster that exists is one whose pace can actually elect a leader. The goroutines belong to Cluster.Run because they belong to the context Run is given.

func (*Cluster) Config

func (cluster *Cluster) Config() Config

Config is the configuration this cluster was built from.

func (*Cluster) Crash

func (cluster *Cluster) Crash(ctx context.Context, name string) error

Crash fails one node: it stops sending, drops everything that arrives, and keeps the term and vote it had — which is what a process whose state is on disk does when it dies.

func (*Cluster) Names

func (cluster *Cluster) Names() []string

Names is every node's name, in index order. The slice is a copy: a caller enumerating the cluster cannot renumber it.

func (*Cluster) Recover

func (cluster *Cluster) Recover(ctx context.Context, name string) error

Recover restarts a crashed node as a follower of whatever term it left in.

func (*Cluster) Run

func (cluster *Cluster) Run(ctx context.Context) error

Run starts every goroutine and returns when the context ends and all of them have stopped.

It blocks, so a host runs it in a goroutine of its own and a specification can wait on its return to know the cluster is actually gone rather than merely asked to go. Cancellation is the caller's own instruction, so it is not reported back as an error; the only error Run has is being called twice.

func (*Cluster) Subscribe

func (cluster *Cluster) Subscribe(ctx context.Context) (<-chan Snapshot, error)

Subscribe returns a channel carrying every view minted from now on, plus the current one if there is one.

The channel is closed when the cluster stops. A subscription whose context ends is dropped at the next view rather than immediately, which costs at most one heartbeat and saves a goroutine per subscriber.

type Config

type Config struct {
	// Nodes is how many members the cluster has. Quorum is Nodes/2+1, so an
	// even count buys no extra fault tolerance over the odd one below it.
	Nodes int

	// Heartbeat is how often a leader broadcasts, and therefore how often one
	// snapshot reaches a subscriber: one heartbeat round is one snapshot is one
	// round of pulses in the widget.
	Heartbeat time.Duration

	// ElectionTimeout is how long a follower waits to hear from a leader before
	// standing for election. It must exceed twice Heartbeat or a healthy leader
	// cannot keep its followers from campaigning over it.
	ElectionTimeout time.Duration

	// ElectionJitter is the width of the random interval added to
	// ElectionTimeout, per node, per wait. It must be positive for any cluster
	// larger than one node: equal timers are the classic Raft livelock, where
	// every follower campaigns at the same instant, every term splits its vote,
	// and no leader is ever elected.
	ElectionJitter time.Duration

	// Seed seeds the per-node jitter streams. Equal seeds give equal jitter
	// sequences; they do not give an equal scheduler, so a run is reproducible
	// in its timeouts and not in its winner.
	Seed int64
}

Config is one cluster's shape and pace.

Every field is required: there is no zero value that means "work it out". A heartbeat interval a library chose would decide how fast the picture beside it moves, and an election timeout a library chose would decide how long an outage looks like a pause. DefaultConfig is where the demo's own answers live.

func DefaultConfig

func DefaultConfig() Config

DefaultConfig is the demo's own pace: three nodes, a heartbeat slow enough that one round of pulses is watchable, and an election timeout far enough above it that a healthy leader is never campaigned over.

func (Config) Validate

func (config Config) Validate() error

Validate reports the first fault in a configuration.

It is called by New rather than by the caller, so a cluster that exists is a cluster whose configuration was checked — and the checks are the ones whose violation is a cluster that runs and never elects, which is the failure that looks like a hang rather than like an error.

type Snapshot

type Snapshot struct {
	// Sequence is this view's position in the stream, from 1. It rises by one
	// per view and never repeats, which is what the widget re-arms its motion
	// on: one snapshot is one heartbeat round is one round of pulses.
	Sequence uint64

	// Term is the highest term any node has reached.
	Term uint64

	// Leader is the name of the node leading that term, or empty while there
	// is none.
	Leader string

	// LeaderKnown is whether a leader exists.
	LeaderKnown bool

	// Authoritative is whether this view came from the leader itself. A leader
	// tracks peer liveness and attaches its own view to each heartbeat; a
	// leaderless cluster has nobody entitled to speak for it, so the view is
	// the observer's own aggregate and says so.
	Authoritative bool

	// HasQuorum is whether the live membership still reaches a majority.
	HasQuorum bool

	// Voters is the cluster's size.
	Voters int

	// AliveVoters is how many members are currently reachable, counted by the
	// leader when there is one and by the observer when there is not.
	AliveVoters int

	// LeaderClaims is how many nodes claim to lead the current term.
	//
	// It is here because it is the one number that makes the safety property
	// checkable from outside: a node votes at most once per term and a leader
	// needs a majority, so two leaders of one term would need two majorities
	// that overlap on a node that voted twice. This is never above one, and a
	// specification can say so rather than take the argument on faith.
	LeaderClaims int
}

Snapshot is the cluster's fleet view at one moment, and it is the whole of what this engine tells anybody.

It is deliberately the shape the raft widget's declared stream event carries. Not because the engine knows about a widget — it names no region, no wire name and no field spelling — but because a fleet view is a small closed set of facts and there is no honest disagreement about which ones they are.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL