A production-ready Raft consensus algorithm implementation for enabling distributed, fault-tolerant replication across multiple Helios nodes.
This implementation provides:
- Leader Election: Automatic leader election with configurable timeouts
- Log Replication: Consistent log replication across all nodes
- Safety: Guarantees consistency even with network partitions
- Snapshotting: Automatic log compaction to manage disk usage
- Persistence: Durable state storage for crash recovery
- Production-Ready: Optimized for real-world deployment
┌─────────────────────────────────────────────────────────────┐
│ Raft Cluster │
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Node 1 │ │ Node 2 │ │ Node 3 │ │
│ │(Leader) │◄───────►│(Follower)│◄───────►│(Follower)│ │
│ └────┬────┘ └─────────┘ └─────────┘ │
│ │ │
│ │ Log Replication │
│ ▼ │
│ ┌─────────────┐ │
│ │ ATLAS Store │ │
│ │ (FSM) │ │
│ └─────────────┘ │
└─────────────────────────────────────────────────────────────┘
Main Raft implementation managing:
- State machine (Follower/Candidate/Leader)
- Term management
- Vote handling
- Log management
- Peer communication
Replicated log with:
- Persistent storage
- Index-based retrieval
- Log compaction
- Consistency checks
Handles:
- RequestVote RPCs
- Vote counting
- Election timeouts
- Term transitions
Manages:
- State persistence (currentTerm, votedFor)
- Snapshot creation/restoration
- Log compaction
- Crash recovery
RPC communication:
- AppendEntries
- RequestVote
- InstallSnapshot
Application interface:
- Command application
- Snapshot creation
- State restoration
package main
import (
"context"
"fmt"
"time"
"github.com/helios/helios/internal/raft"
)
func main() {
// Configure node
config := raft.DefaultConfig()
config.NodeID = "node-1"
config.ListenAddr = "127.0.0.1:9001"
config.DataDir = "./data/node-1"
// Create transport
transport := raft.NewLocalTransport(config.ListenAddr)
// Create state machine
fsm := raft.NewMockFSM()
// Create apply channel
applyCh := make(chan raft.ApplyMsg, 100)
// Create Raft node
node, err := raft.New(config, transport, fsm, applyCh)
if err != nil {
panic(err)
}
// Add peers
node.AddPeer("node-2", "127.0.0.1:9002")
node.AddPeer("node-3", "127.0.0.1:9003")
// Start node
ctx := context.Background()
if err := node.Start(ctx); err != nil {
panic(err)
}
// Wait for leader election
time.Sleep(500 * time.Millisecond)
// Check if leader
term, isLeader := node.GetState()
fmt.Printf("Node state: term=%d, isLeader=%v\n", term, isLeader)
// If leader, apply command
if isLeader {
command := []byte(`{"op":"set","key":"foo","value":"bar"}`)
index, term, err := node.Apply(command, 5*time.Second)
if err != nil {
fmt.Printf("Failed to apply command: %v\n", err)
} else {
fmt.Printf("Command applied: index=%d, term=%d\n", index, term)
}
}
// Listen for applied commands
go func() {
for msg := range applyCh {
if msg.CommandValid {
fmt.Printf("Applied command at index %d: %s\n",
msg.CommandIndex, string(msg.Command))
}
}
}()
// Run forever
select {}
}package atlas
import (
"context"
"encoding/json"
"github.com/helios/helios/internal/atlas/store"
"github.com/helios/helios/internal/raft"
)
// AtlasFSM implements raft.FSM for ATLAS store
type AtlasFSM struct {
store *store.Store
}
func NewAtlasFSM(store *store.Store) *AtlasFSM {
return &AtlasFSM{store: store}
}
func (a *AtlasFSM) Apply(commandBytes interface{}) interface{} {
// Parse command
var cmd struct {
Op string `json:"op"`
Key string `json:"key"`
Value []byte `json:"value"`
TTL int64 `json:"ttl"`
}
if err := json.Unmarshal(commandBytes.([]byte), &cmd); err != nil {
return err
}
// Execute command
switch cmd.Op {
case "set":
return a.store.Set(cmd.Key, cmd.Value, time.Duration(cmd.TTL))
case "delete":
return a.store.Delete(cmd.Key)
default:
return fmt.Errorf("unknown command: %s", cmd.Op)
}
}
func (a *AtlasFSM) Snapshot() ([]byte, error) {
// Create snapshot of ATLAS store
return a.store.Snapshot()
}
func (a *AtlasFSM) Restore(snapshot []byte) error {
// Restore ATLAS store from snapshot
return a.store.Restore(snapshot)
}
// RaftAtlas wraps ATLAS with Raft consensus
type RaftAtlas struct {
store *store.Store
raft *raft.Raft
applyCh chan raft.ApplyMsg
ctx context.Context
cancelCtx context.CancelFunc
}
func NewRaftAtlas(config *raft.Config, transport raft.Transport) (*RaftAtlas, error) {
// Create ATLAS store
store := store.New()
// Create FSM
fsm := NewAtlasFSM(store)
// Create apply channel
applyCh := make(chan raft.ApplyMsg, 1000)
// Create Raft node
node, err := raft.New(config, transport, fsm, applyCh)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
ra := &RaftAtlas{
store: store,
raft: node,
applyCh: applyCh,
ctx: ctx,
cancelCtx: cancel,
}
return ra, nil
}
func (ra *RaftAtlas) Start() error {
// Start Raft
if err := ra.raft.Start(ra.ctx); err != nil {
return err
}
// Process applied commands
go ra.processApplied()
return nil
}
func (ra *RaftAtlas) processApplied() {
for {
select {
case <-ra.ctx.Done():
return
case msg := <-ra.applyCh:
if msg.CommandValid {
// Command already applied to FSM by Raft
// Just log or update metrics here
}
}
}
}
func (ra *RaftAtlas) Set(key string, value []byte, ttl time.Duration) error {
// Check if leader
_, isLeader := ra.raft.GetState()
if !isLeader {
return fmt.Errorf("not the leader")
}
// Create command
cmd := map[string]interface{}{
"op": "set",
"key": key,
"value": value,
"ttl": ttl.Milliseconds(),
}
cmdBytes, err := json.Marshal(cmd)
if err != nil {
return err
}
// Apply through Raft
_, _, err = ra.raft.Apply(cmdBytes, 5*time.Second)
return err
}
func (ra *RaftAtlas) Get(key string) ([]byte, error) {
// Reads can go directly to local store
// (Raft ensures it's consistent)
return ra.store.Get(key)
}
func (ra *RaftAtlas) Delete(key string) error {
// Check if leader
_, isLeader := ra.raft.GetState()
if !isLeader {
return fmt.Errorf("not the leader")
}
// Create command
cmd := map[string]interface{}{
"op": "delete",
"key": key,
}
cmdBytes, err := json.Marshal(cmd)
if err != nil {
return err
}
// Apply through Raft
_, _, err = ra.raft.Apply(cmdBytes, 5*time.Second)
return err
}
func (ra *RaftAtlas) Shutdown() error {
ra.cancelCtx()
return ra.raft.Shutdown()
}config := &raft.Config{
NodeID: "node-1",
ListenAddr: "127.0.0.1:9001",
DataDir: "./data/raft",
HeartbeatTimeout: 50 * time.Millisecond, // Leader heartbeat interval
ElectionTimeout: 150 * time.Millisecond, // Base election timeout
SnapshotInterval: 5 * time.Minute, // Snapshot check interval
SnapshotThreshold: 10000, // Log entries before snapshot
MaxEntriesPerAppend: 100, // Max entries per AppendEntries RPC
}- Randomized election timeouts prevent split votes
- Automatic re-election on leader failure
- Majority vote required
- Consistent ordering across all nodes
- Automatic retry on failure
- Conflict resolution for network partitions
- Log Matching Property: logs are consistent across nodes
- Leader Completeness: committed entries never lost
- State Machine Safety: all nodes apply same commands in same order
- Automatic log compaction
- Configurable thresholds
- Efficient state transfer for slow followers
- Tolerates minority node failures
- Automatic recovery from crashes
- Network partition handling
Run the included tests:
go test -v ./internal/raft/...Tests include:
- Basic leader election
- Log replication
- Leader failover
- Snapshot creation/restoration
- Network partition recovery
- 3 nodes: Tolerates 1 failure (recommended minimum)
- 5 nodes: Tolerates 2 failures (recommended for production)
- 7 nodes: Tolerates 3 failures (high availability)
Odd numbers are preferred to avoid split-brain scenarios.
// Low latency (local network)
config.HeartbeatTimeout = 50 * time.Millisecond
config.ElectionTimeout = 150 * time.Millisecond
// High latency (WAN)
config.HeartbeatTimeout = 200 * time.Millisecond
config.ElectionTimeout = 1 * time.Second
// High throughput
config.MaxEntriesPerAppend = 500
config.SnapshotThreshold = 50000Monitor these metrics:
- Leader election count
- Log replication latency
- Snapshot size and frequency
- Node state transitions
- RPC failure rate
- Start all nodes simultaneously for initial cluster formation
- Configure persistent storage with sufficient disk space
- Use stable network addresses for peer communication
- Monitor leader stability and election frequency
- Plan for rolling upgrades to maintain availability
// Create new Raft node
func New(config *Config, transport Transport, fsm FSM, applyCh chan ApplyMsg) (*Raft, error)
// Start the Raft consensus algorithm
func (r *Raft) Start(ctx context.Context) error
// Shutdown gracefully shuts down the node
func (r *Raft) Shutdown() error
// GetState returns current term and leader status
func (r *Raft) GetState() (uint64, bool)
// Apply submits a command to the cluster
func (r *Raft) Apply(command []byte, timeout time.Duration) (uint64, uint64, error)
// AddPeer adds a peer to the cluster
func (r *Raft) AddPeer(id, address string) error
// RemovePeer removes a peer from the cluster
func (r *Raft) RemovePeer(id string) errortype FSM interface {
// Apply a command to the state machine
Apply(command interface{}) interface{}
// Create a snapshot of current state
Snapshot() ([]byte, error)
// Restore state from a snapshot
Restore(snapshot []byte) error
}Part of the Helios project. See main LICENSE file.
- Raft Paper - In Search of an Understandable Consensus Algorithm
- Raft Visualization - Interactive visualization of Raft
Contributions welcome! Please ensure all tests pass and add tests for new features.
For issues or questions, please open an issue on the Helios GitHub repository.