How to Implement a Custom Raft Log Storage Solution Using Dragonboat's LogDB Interface
You can implement a custom Raft log storage solution in Dragonboat by implementing the ILogDB interface defined in the raftio package, creating a factory that satisfies the LogDBFactory interface, and injecting it into NodeHostConfig.Expert.LogDBFactory before starting your NodeHost.
Dragonboat is a high-performance Go implementation of the Raft consensus protocol. While it ships with a built-in Pebble-based storage backend, production environments often require specialized storage solutions—whether for compliance, performance optimization, or integration with existing infrastructure. By leveraging the LogDB abstraction, you can replace the default storage engine with any custom backend, from in-memory caches to distributed databases or cloud object storage.
Understanding the LogDB Architecture
The ILogDB Interface Contract
The core contract for custom storage implementations is the ILogDB interface, defined in raftio/logdb.go (lines 59-110). This interface mandates methods for persisting Raft state, managing log entries, and handling snapshots. Key methods include:
SaveRaftState(updates []pb.Update, shardID uint64) error– Persists Raft state and log entriesIterateEntries(ents []pb.Entry, size uint64, shardID, replicaID, low, high, maxSize uint64)– Retrieves log entries within a specific rangeReadRaftState(shardID, replicaID, lastIdx uint64)– Reads the current persistent stateCompactEntriesTo(shardID, replicaID, idx uint64)– Removes entries up to a specific indexSaveSnapshotsandGetSnapshot– Handle snapshot persistence
The LogDBFactory Extension Point
Dragonboat instantiates storage through a factory pattern defined in config/config.go (lines 487-495). The LogDBFactory interface requires:
Create(cfg NodeHostConfig, cb LogDBCallback, dirs []string, wals []string) (raftio.ILogDB, error)Name() string
If you do not provide a custom factory, Dragonboat defaults to the built-in Pebble implementation (previously RocksDB) via defaultLogDB in the Prepare method (lines 83-86 of config/config.go).
Step-by-Step Implementation Guide
Step 1: Implement the ILogDB Interface
Create a struct that satisfies all methods of raftio.ILogDB. The implementation must be thread-safe, as Dragonboat may invoke these methods concurrently. Here is a minimal in-memory skeleton:
type customLogDB struct {
mu sync.RWMutex
state map[raftio.NodeInfo]raftio.RaftState
logs map[raftio.NodeInfo][]pb.Entry
snaps map[raftio.NodeInfo]pb.Snapshot
}
func (c *customLogDB) Name() string { return "custom" }
func (c *customLogDB) Close() error { return nil }
func (c *customLogDB) BinaryFormat() uint32 { return raftio.PlainLogDBBinVersion }
// Implement remaining methods: SaveRaftState, IterateEntries, ReadRaftState, etc.
Step 2: Create the LogDBFactory
Implement the factory that Dragonboat will call during NodeHost initialization:
type customFactory struct{}
func (f customFactory) Create(cfg config.NodeHostConfig, cb config.LogDBCallback, dirs []string, wals []string) (raftio.ILogDB, error) {
return &customLogDB{
state: make(map[raftio.NodeInfo]raftio.RaftState),
logs: make(map[raftio.NodeInfo][]pb.Entry),
snaps: make(map[raftio.NodeInfo]pb.Snapshot),
}, nil
}
func (f customFactory) Name() string { return "custom" }
Step 3: Configure NodeHost to Use Your Factory
Wire the factory into your NodeHost configuration before starting:
cfg := config.NodeHostConfig{
DeploymentID: 1,
WALDir: "./wal",
NodeHostDir: "./data",
}
// Attach the custom LogDB factory
cfg.Expert.LogDBFactory = customFactory{}
if err := cfg.Prepare(); err != nil {
log.Fatalf("config error: %v", err)
}
nh, err := dragonboat.NewNodeHost(cfg, config.DefaultRAFTAddress)
if err != nil {
log.Fatalf("failed to start NodeHost: %v", err)
}
Note: While the deprecated cfg.LogDBFactory field still works, the modern approach uses cfg.Expert.LogDBFactory as implemented in config/config.go.
Complete Working Example: In-Memory LogDB
Below is a complete, runnable in-memory implementation demonstrating all required methods. This example is suitable for testing or ephemeral clusters where durability is not required.
package main
import (
"sync"
"github.com/lni/dragonboat/v4/config"
"github.com/lni/dragonboat/v4/raftio"
"github.com/lni/dragonboat/v4/raftpb"
)
type memLogDB struct {
mu sync.RWMutex
state map[raftio.NodeInfo]raftio.RaftState
logs map[raftio.NodeInfo][]raftpb.Entry
snaps map[raftio.NodeInfo]raftpb.Snapshot
}
func (m *memLogDB) Name() string { return "memlogdb" }
func (m *memLogDB) Close() error { return nil }
func (m *memLogDB) BinaryFormat() uint32 { return raftio.PlainLogDBBinVersion }
func (m *memLogDB) ListNodeInfo() ([]raftio.NodeInfo, error) {
m.mu.RLock()
defer m.mu.RUnlock()
var res []raftio.NodeInfo
for ni := range m.state {
res = append(res, ni)
}
return res, nil
}
func (m *memLogDB) SaveBootstrapInfo(shardID, replicaID uint64, b raftpb.Bootstrap) error {
return nil
}
func (m *memLogDB) GetBootstrapInfo(shardID, replicaID uint64) (raftpb.Bootstrap, error) {
return raftpb.Bootstrap{}, raftio.ErrNoBootstrapInfo
}
func (m *memLogDB) SaveRaftState(updates []raftpb.Update, shardID uint64) error {
m.mu.Lock()
defer m.mu.Unlock()
for _, ud := range updates {
ni := raftio.GetNodeInfo(ud.ShardID, ud.ReplicaID)
m.logs[ni] = append(m.logs[ni], ud.Entries...)
if ud.State != nil {
m.state[ni] = raftio.RaftState{
State: *ud.State,
FirstIndex: ud.FirstIndex,
EntryCount: uint64(len(m.logs[ni])),
}
}
}
return nil
}
func (m *memLogDB) IterateEntries(ents []raftpb.Entry, size uint64,
shardID, replicaID, low, high, maxSize uint64) ([]raftpb.Entry, uint64, error) {
m.mu.RLock()
defer m.mu.RUnlock()
ni := raftio.GetNodeInfo(shardID, replicaID)
log := m.logs[ni]
if low >= uint64(len(log)) {
return nil, 0, nil
}
var total uint64
var out []raftpb.Entry
for i := low; i < high && i < uint64(len(log)); i++ {
e := log[i]
if total+uint64(e.Size()) > maxSize {
break
}
out = append(out, e)
total += uint64(e.Size())
}
return out, total, nil
}
func (m *memLogDB) ReadRaftState(shardID, replicaID, lastIdx uint64) (raftio.RaftState, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.state[raftio.GetNodeInfo(shardID, replicaID)], nil
}
func (m *memLogDB) RemoveEntriesTo(shardID, replicaID, idx uint64) error { return nil }
func (m *memLogDB) CompactEntriesTo(shardID, replicaID, idx uint64) (<-chan struct{}, error) {
ch := make(chan struct{}, 1)
ch <- struct{}{}
return ch, nil
}
func (m *memLogDB) SaveSnapshots(updates []raftpb.Update) error { return nil }
func (m *memLogDB) GetSnapshot(shardID, replicaID uint64) (raftpb.Snapshot, error) {
m.mu.RLock()
defer m.mu.RUnlock()
return m.snaps[raftio.GetNodeInfo(shardID, replicaID)], nil
}
func (m *memLogDB) RemoveNodeData(shardID, replicaID uint64) error { return nil }
func (m *memLogDB) ImportSnapshot(snap raftpb.Snapshot, replicaID uint64) error { return nil }
Key Source Files and Reference Implementations
When building a production-grade custom storage backend, study these files in the lni/dragonboat repository:
raftio/logdb.go– Defines theILogDBinterface and related types (NodeInfo,RaftState,ErrNoBootstrapInfo). This is the primary contract your implementation must satisfy.config/config.go– Contains theLogDBFactoryinterface definition (lines 487-495) and theNodeHostConfig.Expert.LogDBFactoryfield used to inject your factory.internal/logdb/logdb.go– Reference implementation of the built-in Pebble-backed LogDB. Study this for patterns on batching, compaction, and error handling.plugin/tee/tee.go– Example of a factory wrapper (TanPebbleLogDBFactory) that demonstrates how to compose multiple storage backends.docs/storage.md– High-level documentation summarizing the custom storage integration process.
Summary
Implementing a custom Raft log storage solution using Dragonboat's LogDB interface requires three main steps:
- Implement
ILogDB– Create a thread-safe struct satisfying all methods inraftio/logdb.go, includingSaveRaftState,IterateEntries, andReadRaftState. - Create a Factory – Build a
LogDBFactoryimplementation that instantiates your custom LogDB, providing theCreateandNamemethods as defined inconfig/config.go. - Configure NodeHost – Assign your factory to
NodeHostConfig.Expert.LogDBFactory(or the deprecatedLogDBFactoryfield) before callingNewNodeHost.
By following this pattern, you can integrate any storage backend—from in-memory caches to distributed databases—while Dragonboat handles the Raft consensus logic.
Frequently Asked Questions
What methods are mandatory when implementing the ILogDB interface?
You must implement all methods defined in raftio/logdb.go, including SaveRaftState for persisting Raft updates, IterateEntries for log retrieval, ReadRaftState for reading persistent state, CompactEntriesTo for log compaction, and SaveSnapshots/GetSnapshot for snapshot management. The interface also requires ListNodeInfo, SaveBootstrapInfo, and lifecycle methods like Close and Name.
Can I use multiple different storage backends simultaneously in one NodeHost?
Yes, you can implement a composite factory that routes different shards to different backends, or use a wrapper pattern like the TanPebbleLogDBFactory shown in plugin/tee/tee.go. However, each individual shard must use a consistent storage backend throughout its lifetime to maintain Raft safety guarantees.
Is the in-memory implementation shown in the examples suitable for production use?
No, the in-memory memLogDB example is intended for testing and demonstration only. Production implementations must provide durable persistence, proper crash recovery, and efficient compaction. Study the reference implementation in internal/logdb/logdb.go for production-grade patterns using Pebble.
What is the difference between LogDBFactory and Expert.LogDBFactory?
LogDBFactory is the deprecated field in NodeHostConfig for specifying custom storage factories. The modern approach uses Expert.LogDBFactory (defined in config/config.go), which provides better encapsulation of advanced configuration options. Both currently work, but Expert.LogDBFactory is the recommended path for new implementations.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →