Skip to content

Commit

Permalink
Introduce MonotonicLogStore interface
Browse files Browse the repository at this point in the history
This commit introduces an interface which acts as a handler for a leaky
abstraction in the structure of underlying log stores. In order to
properly handle post-snapshot-restore cleanup for log stores
generically, we need some awareness of whether the underlying store
permits gaps.

Boltdb allows for gaps in log store indices, but to handle them it
requires a freelist, which is written on every commit. This is costly,
particularly when the freelist is large. By completely resetting the
LogStore after snapshot, we grow the size of the freelist, which would
result in performance degradation.

The MonotonicLogStore interface is implemented by LogStores with
guarantees of sequential/monotonic indices, like raft-wal, but reverts
to the old behavior for boltdb.

This also requires special handling within LogStore wrappers (like
LogCache), to ensure that the type assertion is passed to the underlying
store.
  • Loading branch information
mpalmi committed Mar 12, 2023
1 parent 0fc4f86 commit ec7349b
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 1 deletion.
15 changes: 14 additions & 1 deletion log.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,13 +122,26 @@ type LogStore interface {
// StoreLog stores a log entry.
StoreLog(log *Log) error

// StoreLogs stores multiple log entries.
// StoreLogs stores multiple log entries. Implementers of StoreLogs with
// guarantees of monotonically increasing sequential indexes should make use
// of the MonotonicLogStore interface.
StoreLogs(logs []*Log) error

// DeleteRange deletes a range of log entries. The range is inclusive.
DeleteRange(min, max uint64) error
}

// MonotonicLogStore is an optional interface for LogStore implementations with
// gapless index requirements. If they return true, the LogStore must have an
// efficient implementation of DeleteLogs, as this called after every snapshot
// restore when gaps are not allowed. We avoid deleting all records for
// LogStores that do not implement MonotonicLogStore because this has a major
// negative performance impact on the BoltDB store that is currently the most
// widely used.
type MonotonicLogStore interface {
IsMonotonic() bool
}

func oldestLog(s LogStore) (Log, error) {
var l Log

Expand Down
10 changes: 10 additions & 0 deletions log_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@ func NewLogCache(capacity int, store LogStore) (*LogCache, error) {
return c, nil
}

// IsMonotonic implements the MonotonicLogStore interface. This is a shim to
// expose the underyling store as monotonically indexed or not.
func (c *LogCache) IsMonotonic() bool {
if store, ok := c.store.(MonotonicLogStore); ok {
return store.IsMonotonic()
}

return false
}

func (c *LogCache) GetLog(idx uint64, log *Log) error {
// Check the buffer for an entry
c.l.RLock()
Expand Down
35 changes: 35 additions & 0 deletions raft_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2417,6 +2417,41 @@ func TestRaft_GetConfigurationNoBootstrap(t *testing.T) {
}
}

func TestRaft_LogStoreIsMonotonic(t *testing.T) {
c := MakeCluster(1, t, nil)
defer c.Close()

// Should be one leader
leader := c.Leader()
c.EnsureLeader(t, leader.localAddr)

// Test the monotonic type assertion on the InmemStore.
_, ok := leader.logs.(MonotonicLogStore)
assert.False(t, ok)

var log LogStore

// Wrapping the non-monotonic store as a LogCache should make it pass the
// type assertion, but the underlying store is still non-monotonic.
log, _ = NewLogCache(100, leader.logs)
mcast, ok := log.(MonotonicLogStore)
require.True(t, ok)
assert.False(t, mcast.IsMonotonic())

// Now create a new MockMonotonicLogStore using the leader logs and expect
// it to work.
log = &MockMonotonicLogStore{s: leader.logs}
mcast, ok = log.(MonotonicLogStore)
require.True(t, ok)
assert.True(t, mcast.IsMonotonic())

// Wrap the mock logstore in a LogCache and check again.
log, _ = NewLogCache(100, log)
mcast, ok = log.(MonotonicLogStore)
require.True(t, ok)
assert.True(t, mcast.IsMonotonic())
}

func TestRaft_CacheLogWithStoreError(t *testing.T) {
c := MakeCluster(2, t, nil)
defer c.Close()
Expand Down
41 changes: 41 additions & 0 deletions testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,47 @@ func (m *MockSnapshot) Persist(sink SnapshotSink) error {
func (m *MockSnapshot) Release() {
}

// MockMonotonicLogStore is a stubbed LogStore wrapper for testing the
// MonotonicLogStore interface.
type MockMonotonicLogStore struct {
s LogStore
}

// IsMonotonic implements the MonotonicLogStore interface.
func (m *MockMonotonicLogStore) IsMonotonic() bool {
return true
}

// FirstIndex implements the LogStore interface.
func (m *MockMonotonicLogStore) FirstIndex() (uint64, error) {
return m.s.FirstIndex()
}

// LastIndex implements the LogStore interface.
func (m *MockMonotonicLogStore) LastIndex() (uint64, error) {
return m.s.LastIndex()
}

// GetLog implements the LogStore interface.
func (m *MockMonotonicLogStore) GetLog(index uint64, log *Log) error {
return m.s.GetLog(index, log)
}

// StoreLog implements the LogStore interface.
func (m *MockMonotonicLogStore) StoreLog(log *Log) error {
return m.s.StoreLog(log)
}

// StoreLogs implements the LogStore interface.
func (m *MockMonotonicLogStore) StoreLogs(logs []*Log) error {
return m.s.StoreLogs(logs)
}

// DeleteRange implements the LogStore interface.
func (m *MockMonotonicLogStore) DeleteRange(min uint64, max uint64) error {
return m.s.DeleteRange(min, max)
}

// This can be used as the destination for a logger and it'll
// map them into calls to testing.T.Log, so that you only see
// the logging for failed tests.
Expand Down

0 comments on commit ec7349b

Please sign in to comment.