Skip to content

Commit 17f5596

Browse files
committed
log: clean-up log conflict search
Use the new entryID type. Move the preceding entry check into the conflict search method rather than do it outside. Signed-off-by: Pavel Kalinnikov <[email protected]>
1 parent 2a0c44f commit 17f5596

File tree

2 files changed

+89
-54
lines changed

2 files changed

+89
-54
lines changed

log.go

+55-39
Original file line numberDiff line numberDiff line change
@@ -105,27 +105,24 @@ func (l *raftLog) String() string {
105105
// maybeAppend returns (0, false) if the entries cannot be appended. Otherwise,
106106
// it returns (last index of new entries, true).
107107
func (l *raftLog) maybeAppend(a logSlice, committed uint64) (lastnewi uint64, ok bool) {
108-
if !l.matchTerm(a.prev) {
108+
match, ok := l.findConflict(a)
109+
if !ok {
109110
return 0, false
110111
}
111-
// TODO(pav-kv): propagate logSlice down the stack. It will be used all the
112-
// way down in unstable, for safety checks, and for useful bookkeeping.
113-
114-
lastnewi = a.prev.index + uint64(len(a.entries))
115-
ci := l.findConflict(a.entries)
116-
switch {
117-
case ci == 0:
118-
case ci <= l.committed:
119-
l.logger.Panicf("entry %d conflict with committed entry [committed(%d)]", ci, l.committed)
120-
default:
121-
offset := a.prev.index + 1
122-
if ci-offset > uint64(len(a.entries)) {
123-
l.logger.Panicf("index, %d, is out of range [%d]", ci-offset, len(a.entries))
124-
}
125-
l.append(a.entries[ci-offset:]...)
112+
if match.index < a.lastIndex() && match.index < l.committed {
113+
l.logger.Panicf("entry %d is already committed [committed(%d)]", match.index+1, l.committed)
126114
}
127-
l.commitTo(min(committed, lastnewi))
128-
return lastnewi, true
115+
116+
// Fast-forward to the first mismatching or missing entry.
117+
// NB: prev.index <= match.index <= a.lastIndex(), so the sub-slicing is safe.
118+
a.entries = a.entries[match.index-a.prev.index:]
119+
a.prev = match
120+
121+
// TODO(pav-kv): pass the logSlice down the stack, for safety checks and
122+
// bookkeeping in the unstable structure.
123+
l.append(a.entries...)
124+
l.commitTo(min(committed, a.lastIndex()))
125+
return a.lastIndex(), true
129126
}
130127

131128
func (l *raftLog) append(ents ...pb.Entry) uint64 {
@@ -139,29 +136,48 @@ func (l *raftLog) append(ents ...pb.Entry) uint64 {
139136
return l.lastIndex()
140137
}
141138

142-
// findConflict finds the index of the conflict.
143-
// It returns the first pair of conflicting entries between the existing
144-
// entries and the given entries, if there are any.
145-
// If there is no conflicting entries, and the existing entries contains
146-
// all the given entries, zero will be returned.
147-
// If there is no conflicting entries, but the given entries contains new
148-
// entries, the index of the first new entry will be returned.
149-
// An entry is considered to be conflicting if it has the same index but
150-
// a different term.
151-
// The index of the given entries MUST be continuously increasing.
152-
func (l *raftLog) findConflict(ents []pb.Entry) uint64 {
153-
for i := range ents {
154-
if id := pbEntryID(&ents[i]); !l.matchTerm(id) {
155-
if id.index <= l.lastIndex() {
156-
// TODO(pav-kv): can simply print %+v of the id. This will change the
157-
// log format though.
158-
l.logger.Infof("found conflict at index %d [existing term: %d, conflicting term: %d]",
159-
id.index, l.zeroTermOnOutOfBounds(l.term(id.index)), id.term)
160-
}
161-
return id.index
139+
// findConflict finds the last entry in the given log slice that matches the
140+
// log. The next entry either mismatches, or is missing.
141+
//
142+
// If the slice partially/fully matches, this method returns true. The returned
143+
// entryID is the ID of the last matching entry. It can be s.prev if it is the
144+
// only matching entry. It is guaranteed that the returned entryID.index is in
145+
// the [s.prev.index, s.lastIndex()] range.
146+
//
147+
// All the entries up to the returned entryID are already present in the log,
148+
// and do not need to be appended again. The caller can safely fast-forward an
149+
// append request to the next entry after it.
150+
//
151+
// Returns false if the given slice mismatches the log entirely, i.e. the s.prev
152+
// entry has a mismatching entryID.term. In this case an append request can not
153+
// proceed.
154+
func (l *raftLog) findConflict(s logSlice) (entryID, bool) {
155+
// TODO(pav-kv): add a fast-path here. If s.term == raftLog.lastTerm, we can
156+
// skip the match checks entirely. We can double-check only the last entry
157+
// match, to be sure, but it is not necessary if raft invariants are true.
158+
if !l.matchTerm(s.prev) {
159+
return entryID{}, false
160+
}
161+
162+
// TODO(pav-kv): every matchTerm call in the linear scan below can fall back
163+
// to fetching an entry from storage. This is inefficient, we can improve it.
164+
// NB: logs that don't match at one index, don't match at all indices above.
165+
// So we can use binary search to find the fork.
166+
match := s.prev
167+
for i := range s.entries {
168+
id := pbEntryID(&s.entries[i])
169+
if l.matchTerm(id) {
170+
match = id
171+
continue
162172
}
173+
if id.index <= l.lastIndex() {
174+
// TODO(pav-kv): should simply print %+v of the id.
175+
l.logger.Infof("found conflict at index %d [existing term: %d, conflicting term: %d]",
176+
id.index, l.zeroTermOnOutOfBounds(l.term(id.index)), id.term)
177+
}
178+
return match, true
163179
}
164-
return 0
180+
return match, true // all entries match
165181
}
166182

167183
// findConflictByTerm returns a best guess on where this log ends matching

log_test.go

+34-15
Original file line numberDiff line numberDiff line change
@@ -25,30 +25,49 @@ import (
2525

2626
func TestFindConflict(t *testing.T) {
2727
previousEnts := index(1).terms(1, 2, 3)
28+
ids := make([]entryID, 1, len(previousEnts)+1) // dummy (0, 0) at index 0
29+
for i := range previousEnts {
30+
ids = append(ids, pbEntryID(&previousEnts[i]))
31+
}
2832
for _, tt := range []struct {
29-
ents []pb.Entry
30-
want uint64
33+
prev entryID
34+
ents []pb.Entry
35+
notOk bool
36+
want entryID
3137
}{
3238
// no conflict, empty entries
33-
{ents: nil, want: 0},
39+
{ents: nil, want: ids[0]},
40+
// prev does not match the log
41+
{prev: entryID{term: 10, index: 1}, notOk: true},
3442
// no conflict
35-
{ents: index(1).terms(1, 2, 3), want: 0},
36-
{ents: index(2).terms(2, 3), want: 0},
37-
{ents: index(3).terms(3), want: 0},
43+
{prev: ids[0], ents: index(1).terms(1, 2, 3), want: ids[3]},
44+
{prev: ids[1], ents: index(2).terms(2, 3), want: ids[3]},
45+
{prev: ids[2], ents: index(3).terms(3), want: ids[3]},
3846
// no conflict, but has new entries
39-
{ents: index(1).terms(1, 2, 3, 4, 4), want: 4},
40-
{ents: index(2).terms(2, 3, 4, 5), want: 4},
41-
{ents: index(3).terms(3, 4, 4), want: 4},
42-
{ents: index(4).terms(4, 4), want: 4},
43-
// conflicts with existing entries
44-
{ents: index(1).terms(4, 4), want: 1},
45-
{ents: index(2).terms(1, 4, 4), want: 2},
46-
{ents: index(3).terms(1, 2, 4, 4), want: 3},
47+
{prev: ids[0], ents: index(1).terms(1, 2, 3, 4, 4), want: ids[3]},
48+
{prev: ids[1], ents: index(2).terms(2, 3, 4, 4), want: ids[3]},
49+
{prev: ids[2], ents: index(3).terms(3, 4, 4), want: ids[3]},
50+
{prev: ids[3], ents: index(4).terms(4, 4), want: ids[3]},
51+
// passes prev check, but conflicts with existing entries
52+
{prev: ids[0], ents: index(1).terms(4, 4), want: ids[0]},
53+
{prev: ids[1], ents: index(2).terms(1, 4, 4), want: ids[1]},
54+
{prev: ids[2], ents: index(3).terms(2, 2, 4, 4), want: ids[2]},
55+
// prev does not match
56+
{prev: entryID{term: 4, index: 1}, ents: index(2).terms(4, 4), notOk: true},
57+
{prev: entryID{term: 5, index: 2}, ents: index(3).terms(5, 6), notOk: true},
58+
// out of bounds
59+
{prev: entryID{term: 3, index: 10}, ents: index(11).terms(3), notOk: true},
60+
// just touching the right bound, but still out of bounds
61+
{prev: entryID{term: 3, index: 4}, ents: index(5).terms(3, 3, 4), notOk: true},
4762
} {
4863
t.Run("", func(t *testing.T) {
4964
log := newLog(NewMemoryStorage(), discardLogger)
5065
log.append(previousEnts...)
51-
require.Equal(t, tt.want, log.findConflict(tt.ents))
66+
app := logSlice{term: 100, prev: tt.prev, entries: tt.ents}
67+
require.NoError(t, app.valid())
68+
match, ok := log.findConflict(app)
69+
require.Equal(t, !tt.notOk, ok)
70+
require.Equal(t, tt.want, match)
5271
})
5372
}
5473
}

0 commit comments

Comments
 (0)