Skip to content

Commit 7cd824d

Browse files
committed
raft: introduce logSlice struct
This is a type-safe wrapper for all kinds of log slices. We will use it for more readable and safe code. Usages will include: wrapping log append requests; unstable struct; possibly surface in a safer API. Signed-off-by: Pavel Kalinnikov <[email protected]>
1 parent a52b6af commit 7cd824d

File tree

2 files changed

+128
-1
lines changed

2 files changed

+128
-1
lines changed

types.go

+75-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414

1515
package raft
1616

17-
import pb "go.etcd.io/raft/v3/raftpb"
17+
import (
18+
"fmt"
19+
20+
pb "go.etcd.io/raft/v3/raftpb"
21+
)
1822

1923
// entryID uniquely identifies a raft log entry.
2024
//
@@ -30,3 +34,73 @@ type entryID struct {
3034
func pbEntryID(entry *pb.Entry) entryID {
3135
return entryID{term: entry.Term, index: entry.Index}
3236
}
37+
38+
// logSlice describes a correct slice of a raft log.
39+
//
40+
// Every log slice is considered in a context of a specific leader term. This
41+
// term does not necessarily match entryID.term of the entries, since a leader
42+
// log contains both entries from its own term, and some earlier terms.
43+
//
44+
// Two slices with a matching logSlice.term are guaranteed to be consistent,
45+
// i.e. they never contain two different entries at the same index. The reverse
46+
// is not true: two slices with different logSlice.term may contain both
47+
// matching and mismatching entries. Specifically, logs at two different leader
48+
// terms share a common prefix, after which they *permanently* diverge.
49+
//
50+
// A well-formed logSlice conforms to raft safety properties. It provides the
51+
// following guarantees:
52+
//
53+
// 1. entries[i].Index == prev.index + 1 + i,
54+
// 2. prev.term <= entries[0].Term,
55+
// 3. entries[i-1].Term <= entries[i].Term,
56+
// 4. entries[len-1].Term <= term.
57+
//
58+
// Property (1) means the slice is contiguous. Properties (2) and (3) mean that
59+
// the terms of the entries in a log never regress. Property (4) means that a
60+
// leader log at a specific term never has entries from higher terms.
61+
//
62+
// Users of this struct can assume the invariants hold true. Exception is the
63+
// "gateway" code that initially constructs logSlice, such as when its content
64+
// is sourced from a message that was received via transport, or from Storage,
65+
// or in a test code that manually hard-codes this struct. In these cases, the
66+
// invariants should be validated using the valid() method.
67+
type logSlice struct {
68+
// term is the leader term containing the given entries in its log.
69+
term uint64
70+
// prev is the ID of the entry immediately preceding the entries.
71+
prev entryID
72+
// entries contains the consecutive entries representing this slice.
73+
entries []pb.Entry
74+
}
75+
76+
// lastIndex returns the index of the last entry in this log slice. Returns
77+
// prev.index if there are no entries.
78+
func (s logSlice) lastIndex() uint64 {
79+
return s.prev.index + uint64(len(s.entries))
80+
}
81+
82+
// lastEntryID returns the ID of the last entry in this log slice, or prev if
83+
// there are no entries.
84+
func (s logSlice) lastEntryID() entryID {
85+
if ln := len(s.entries); ln != 0 {
86+
return pbEntryID(&s.entries[ln-1])
87+
}
88+
return s.prev
89+
}
90+
91+
// valid returns nil iff the logSlice is a well-formed log slice. See logSlice
92+
// comment for details on what constitutes a valid raft log slice.
93+
func (s logSlice) valid() error {
94+
prev := s.prev
95+
for i := range s.entries {
96+
id := pbEntryID(&s.entries[i])
97+
if id.term < prev.term || id.index != prev.index+1 {
98+
return fmt.Errorf("leader term %d: entries %+v and %+v not consistent", s.term, prev, id)
99+
}
100+
prev = id
101+
}
102+
if s.term < prev.term {
103+
return fmt.Errorf("leader term %d: entry %+v has a newer term", s.term, prev)
104+
}
105+
return nil
106+
}

types_test.go

+53
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,56 @@ func TestEntryID(t *testing.T) {
3939
require.Equal(t, tt.want, pbEntryID(&tt.entry))
4040
}
4141
}
42+
43+
func TestLogSlice(t *testing.T) {
44+
id := func(index, term uint64) entryID {
45+
return entryID{term: term, index: index}
46+
}
47+
e := func(index, term uint64) pb.Entry {
48+
return pb.Entry{Term: term, Index: index}
49+
}
50+
for _, tt := range []struct {
51+
term uint64
52+
prev entryID
53+
entries []pb.Entry
54+
55+
notOk bool
56+
last entryID
57+
}{
58+
// Empty "dummy" slice, starting at (0, 0) origin of the log.
59+
{last: id(0, 0)},
60+
// Empty slice with a given prev ID. Valid only if term >= prev.term.
61+
{prev: id(123, 10), notOk: true},
62+
{term: 9, prev: id(123, 10), notOk: true},
63+
{term: 10, prev: id(123, 10), last: id(123, 10)},
64+
{term: 11, prev: id(123, 10), last: id(123, 10)},
65+
// A single entry.
66+
{term: 0, entries: []pb.Entry{e(1, 1)}, notOk: true},
67+
{term: 1, entries: []pb.Entry{e(1, 1)}, last: id(1, 1)},
68+
{term: 2, entries: []pb.Entry{e(1, 1)}, last: id(1, 1)},
69+
// Multiple entries.
70+
{term: 2, entries: []pb.Entry{e(2, 1), e(3, 1), e(4, 2)}, notOk: true},
71+
{term: 1, prev: id(1, 1), entries: []pb.Entry{e(2, 1), e(3, 1), e(4, 2)}, notOk: true},
72+
{term: 2, prev: id(1, 1), entries: []pb.Entry{e(2, 1), e(3, 1), e(4, 2)}, last: id(4, 2)},
73+
// First entry inconsistent with prev.
74+
{term: 10, prev: id(123, 5), entries: []pb.Entry{e(111, 5)}, notOk: true},
75+
{term: 10, prev: id(123, 5), entries: []pb.Entry{e(124, 4)}, notOk: true},
76+
{term: 10, prev: id(123, 5), entries: []pb.Entry{e(234, 6)}, notOk: true},
77+
{term: 10, prev: id(123, 5), entries: []pb.Entry{e(124, 6)}, last: id(124, 6)},
78+
// Inconsistent entries.
79+
{term: 10, prev: id(12, 2), entries: []pb.Entry{e(13, 2), e(12, 2)}, notOk: true},
80+
{term: 10, prev: id(12, 2), entries: []pb.Entry{e(13, 2), e(15, 2)}, notOk: true},
81+
{term: 10, prev: id(12, 2), entries: []pb.Entry{e(13, 2), e(14, 1)}, notOk: true},
82+
{term: 10, prev: id(12, 2), entries: []pb.Entry{e(13, 2), e(14, 3)}, last: id(14, 3)},
83+
} {
84+
t.Run("", func(t *testing.T) {
85+
s := logSlice{term: tt.term, prev: tt.prev, entries: tt.entries}
86+
require.Equal(t, tt.notOk, s.valid() != nil)
87+
if !tt.notOk {
88+
last := s.lastEntryID()
89+
require.Equal(t, tt.last, last)
90+
require.Equal(t, last.index, s.lastIndex())
91+
}
92+
})
93+
}
94+
}

0 commit comments

Comments
 (0)