AgentSkillsCN

babuza-raft

关键:用于 Raft 共识逻辑、集群管理以及会话处理。触发关键词: babuza、babuza 框架、babuza 入门指南、babuza 教程、babuza 示例、 Raft 实现、Raft 共识、babuza Raft、Raft 集群运维、 Raft 会话、线性化读取、Raft 引导启动、Raft 加入集群、Raft 领导节点。

SKILL.md
--- frontmatter
name: babuza-raft
description: |
  CRITICAL: Use for Raft consensus logic, cluster management, and sessions. Triggers on:
  babuza, babuza framework, babuza getting started, babuza tutorial, babuza example,
  raft implementation, raft consensus, babuza raft, raft cluster operations,
  raft session, linearizable read, raft bootstrap, raft join, raft leader

Babuza Raft Skill

Package: github.com/fanaujie/babuza/raft

Core consensus layer wrapping etcd Raft with cluster bootstrap and management APIs.

You are an expert at the babuza/raft package. Help users by:

  • Writing code: Generate Go code for cluster bootstrapping, membership changes, and consistency guarantees.
  • Answering questions: Explain Raft internals, consistency models, and recovery procedures.

Documentation

Refer to the local files for detailed documentation:

  • ./references/raft-lifecycle.md - API reference, configuration options, and lifecycle details.

Key Patterns

1. Bootstrapping a New Cluster

Start a fresh cluster with initial peers.

go
import (
    "github.com/fanaujie/babuza/ibabuza"
    "github.com/fanaujie/babuza/pkg/builder"
    "github.com/fanaujie/babuza/raft"
)

func bootstrapCluster(stateMachine ibabuza.BaseStateMachine) (*raft.Raft, error) {
    // 1. Build components
    component := builder.NewBabuzaComponentBuilder(&builder.BabuzaComponentConfig{
        ClusterId:      1,
        StorageRootDir: "/tmp/node1",
        TransportType:  builder.HttpTransport,
        WalType:        builder.BabuzaWal,
        SessionType:    builder.NoOpSession,
        SnapshotType:   builder.DurableSnapshot,
    }).Build()

    // 2. Configure Node
    cfg := raft.DefaultBabuzaConfig(1, 1, "127.0.0.1:9001") // ClusterID, PeerID, Addr

    // 3. Configure Peers
    peers := raft.NewPeersConfiguration()
    peers.AddPeer(1, "127.0.0.1:9001", false) // id, addr, isLearner

    // 4. Create Bootstrap Context
    bootstrap, err := raft.NewBootstrapRaftCluster(
        cfg, *peers, stateMachine,
        component.Cluster, component.RaftNode, component.SessionManager,
        component.SnapshotManager, component.WalManager, component.Transport,
        component.Logger, component.MetricsController,
    )
    if err != nil {
        return nil, err
    }

    // 5. Start Raft
    return raft.NewRaft(cfg, bootstrap, nil)
}

2. Joining an Existing Cluster

Join a node to an existing cluster.

go
func joinCluster(stateMachine ibabuza.BaseStateMachine) (*raft.Raft, error) {
    component := builder.NewBabuzaComponentBuilder(&builder.BabuzaComponentConfig{
        ClusterId:      1,
        StorageRootDir: "/tmp/node2",
        TransportType:  builder.HttpTransport,
        WalType:        builder.BabuzaWal,
        SessionType:    builder.NoOpSession,
        SnapshotType:   builder.DurableSnapshot,
    }).Build()

    // Configure Node with Join = true
    cfg := raft.DefaultBabuzaConfig(1, 2, "127.0.0.1:9002")
    cfg.Join = true

    // Self as learner initially
    peers := raft.NewPeersConfiguration()
    peers.AddPeer(2, "127.0.0.1:9002", true) // isLearner = true

    bootstrap, err := raft.NewBootstrapRaftCluster(
        cfg, *peers, stateMachine,
        component.Cluster, component.RaftNode, component.SessionManager,
        component.SnapshotManager, component.WalManager, component.Transport,
        component.Logger, component.MetricsController,
    )
    if err != nil {
        return nil, err
    }

    return raft.NewRaft(cfg, bootstrap, nil)
}

3. Linearizable Read

Read data with strong consistency (ensures leadership and applied index).

go
func (s *Service) Get(key string) (string, error) {
    // 1. Wait for Read Index
    if err := s.raft.LinearizableRead(context.Background()); err != nil {
        return "", err
    }

    // 2. Query Local State Machine safely
    val, err := s.raft.GetStateMachine().Query(key)
    if err != nil {
        return "", err
    }
    return val.(string), nil
}

4. Cluster Membership Changes

Add or remove nodes dynamically.

go
import "github.com/fanaujie/babuza/ibabuza/babuzapb"

// Add a new voting peer - returns ProposedResult
peerAttr := babuzapb.RaftPeerAttribute{
    PeerID:         3,
    RaftListenAddr: "192.168.1.3:9001",
    IsLearner:      false,
}
proposed := r.AddVotingPeer(ctx, raft.ClientSession{}, peerAttr)
defer proposed.Release()
result := proposed.WaitForApplyResult()
if result.Error != nil {
    // handle error
}

// Remove a peer
proposed := r.RemovePeer(ctx, raft.ClientSession{}, peerID)
defer proposed.Release()
result := proposed.WaitForApplyResult()

// Transfer Leadership
transferResult := r.TransferLeader(ctx, targetPeerID)
transferResult.Wait()

5. Checking Cluster Status

go
status := r.Status()
fmt.Printf("Node ID: %d\n", status.LocalPeerID)
fmt.Printf("Leader ID: %d\n", status.LeaderID)
fmt.Printf("Term: %d\n", status.RaftTerm)
fmt.Printf("State: %s\n", status.State)  // Follower, Candidate, Leader
fmt.Printf("Applied: %d\n", status.RaftAppliedIndex)
fmt.Printf("Committed: %d\n", status.RaftCommittedIndex)
fmt.Printf("Is Leader: %v\n", status.IsLeader())

Status Fields

FieldDescription
LocalPeerIDThis node's ID (NOT ID)
LeaderIDCurrent leader's ID (NOT Lead)
RaftTermCurrent Raft term (NOT Term)
StateRaft state: Follower/Candidate/Leader (NOT RaftState)
RaftAppliedIndexLast applied log index (NOT Applied)
RaftCommittedIndexLast committed log index (NOT Commit)
IsLeader()Returns true if this node is the leader

API Reference Table

MethodDescription
NewBootstrapRaftClusterPrepares the environment for starting Raft.
NewRaftStarts the Raft node.
ProposeSubmits a command to the log. Returns ProposedResult.
LinearizableReadEnsures the local state is up-to-date with the leader.
AddVotingPeerAdds a voting peer. Returns ProposedResult.
AddLearnerAdds a learner (non-voting) peer. Returns ProposedResult.
PromoteLearnerPromotes a learner to voter. Returns ProposedResult.
RemovePeerRemoves a peer from cluster. Returns ProposedResult.
TransferLeaderTransfers leadership to another node.
StatusReturns current cluster status.
ShutdownGracefully shuts down Raft. Returns ShutdownResult.

ProposedResult Pattern

All write operations return ProposedResult:

go
proposed := r.Propose(ctx, raft.ClientSession{}, data)
defer proposed.Release()  // IMPORTANT: Always release!

result := proposed.WaitForApplyResult()
if result.Error != nil {  // Error is a FIELD, not a method
    // handle error
}
// Use result.Response for the return value

When Writing Code

  1. Sessions: Use raft.ClientSession{} for proposals. For exactly-once semantics, configure session manager (LRU/Expire).
  2. Context: Use context.WithTimeout for Raft operations to avoid hanging on network partitions.
  3. Release ProposedResult: Always call proposed.Release() after using a Propose result.
  4. Error Field: Check result.Error (field), not result.Error() (not a method).
  5. Status Fields: Use LocalPeerID, LeaderID, RaftTerm, State - not the abbreviated names.

Related Skills

  • babuza-session - Session manager configuration (LRU, Expire, NoOp)
  • babuza-transport - Network transport configuration (TCP, HTTP, gRPC)
  • babuza-snapshot - Snapshot storage configuration (Durable, S3, Volatile)
  • babuza-wal - Write-Ahead Log configuration (Babuza, Badger, Pebble)
  • babuza-statemachine - State machine implementation
  • babuza-metrics - Observability configuration (Prometheus, OpenTelemetry)

When Answering Questions

  1. Consistency: Explain that LinearizableRead avoids stale reads by confirming leadership with a quorum.
  2. Safety: Membership changes use AddLearner -> PromoteLearner for safety, or AddVotingPeer for direct add.
  3. Recovery: Mention RecoverAsStandalone for disaster recovery scenarios.