-
Notifications
You must be signed in to change notification settings - Fork 168
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
raft: add entryID and logSlice types #145
Conversation
24e7750
to
3efb5cd
Compare
@ahrtr @nvanbenschoten This addresses one of the feedback points from #139 review. PTAL. Commit-by-commit review might be the most convenient. |
3efb5cd
to
c6d6aad
Compare
@nvanbenschoten Addressed your comments, thank you. PTAL. |
@nvanbenschoten @ahrtr Things start stacking up on top of this PR. Please let me know if there is major objection to this clean-up. If not, I would like to merge and iterate/rebase. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great!
Thanks! |
038ad09
to
e23983f
Compare
5e4ca8e
to
42a3fef
Compare
@nvanbenschoten I've changed this PR a little since the last review. Mainly, I reworked the @ahrtr @serathius Could you please take a final look at this PR, and merge if it looks good? Commit-by-commit review can be most convenient. I have follow-up work that propagates safe struct usage in other places (which already helped uncover bugs in tests), but I am not including them in this PR to keep it small and simple. Thank you. |
type logSlice struct { | ||
// term is the leader term containing the given entries in its log. | ||
term uint64 | ||
// prev is the ID of the entry immediately preceding the entries. | ||
prev entryID | ||
// entries contains the consecutive entries representing this slice. | ||
entries []pb.Entry | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am thinking it might be better to replace the following fields in Message with this struct.
Lines 402 to 415 in 2f10997
Term uint64 `protobuf:"varint,4,opt,name=term" json:"term"` | |
// logTerm is generally used for appending Raft logs to followers. For example, | |
// (type=MsgApp,index=100,logTerm=5) means the leader appends entries starting | |
// at index=101, and the term of the entry at index 100 is 5. | |
// (type=MsgAppResp,reject=true,index=100,logTerm=5) means follower rejects some | |
// entries from its leader as it already has an entry with term 5 at index 100. | |
// (type=MsgStorageAppendResp,index=100,logTerm=5) means the local node wrote | |
// entries up to index=100 in stable storage, and the term of the entry at index | |
// 100 was 5. This doesn't always mean that the corresponding MsgStorageAppend | |
// message was the one that carried these entries, just that those entries were | |
// stable at the time of processing the corresponding MsgStorageAppend. | |
LogTerm uint64 `protobuf:"varint,5,opt,name=logTerm" json:"logTerm"` | |
Index uint64 `protobuf:"varint,6,opt,name=index" json:"index"` | |
Entries []Entry `protobuf:"bytes,7,rep,name=entries" json:"entries"` |
This can be addressed in separate PRs. In order to be backward compatible, we might want to do it across multiple releases. e.g. add logSlice into protobuf firstly, and then replace the fields in Message in a following release.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, we should probably leave this for some future consideration. The scope of this work currently is internal cleanup that doesn't affect public API / wire protocol.
There are a few additional considerations:
logSlice
is intended as a code-level data structure. It's usually a bad practice to use protobuf-generated structs as data structures because we have limited control of its content, and also this is part of the public API.- The
LogTerm/Index
fields are used by a bunch of other message types (not onlyMsgApp
). We can't easily deprecate them, rather it would have to be a newLogSlice
field or something. A broader refactoring could be: split theMessage
into message-type-specific sub-structs.
There is some argument for moving in a slightly opposite direction from protobufs:
raft
doesn't need to know everypb.Entry
, it only internally cares about IDs of the entries, and config changes. Exposing application-specific entry contents to this package poses security and privacy risks. There could be a world in whichraft
is purely a code-level algorithm/protocol, and messages are packaged at application level.raft
necessitates dependency on protobuf, which is not the most efficient format: it has to be marshaled/unmarshaled. Some applications may choose other wire formats, for instanceflatbuffers
.
A generic comment, I prefer not to add too much small commits, as most of them are mechanical changes, e.g. |
// Users of this struct can assume the invariants hold true. Exception is the | ||
// "gateway" code that initially constructs logSlice, such as when its content | ||
// is sourced from a message that was received via transport, or from Storage, | ||
// or in a test code that manually hard-codes this struct. In these cases, the | ||
// invariants should be validated using the valid() method. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain more about the exceptions? I think we should add a generic verification to verify the invariant properties in each test if possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree with that. This PR adds a valid()
check to TestLogMaybeAppend
, which is at the moment the only user of the new struct. More tests will be converted in a follow-up PR. The valid()
check in other tests helped to find a few places where these invariants are violated (so the tests are testing incorrect behaviour).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The "exception" is basically any place which receives a log slice from an untrusted source (network, storage, etc). Example: see the handleAppendEntries
call:
Lines 1748 to 1750 in 2c8980b
// TODO(pav-kv): construct logSlice up the stack next to receiving the | |
// message, and validate it before taking any action (e.g. bumping term). | |
a := logSlice{term: m.Term, prev: entryID{term: m.LogTerm, index: m.Index}, entries: m.Entries} |
Once the invariants are validated, this struct can be passed around internally, and all the code can assume invariants are true. For instance, we know that the i
-th entry in entries
slice necessarily has entries[i].index == prev.index + 1 + i
, so we don't have to additionally check for this.
I think the comment already explains this well, but please suggest a better phrasing if it doesn't.
I prefer to isolate mechanical changes to separate commits, so that all changes have a small scope and are obviously correct. Such a PR "tells a story" rather than dumps a large change onto a reviewer. Small changes make it possible to review one thing at a time, and not have to do mental work to decompose it. I do this both for the reviewer and for myself, to self-review and convince myself too. I usually communicate in the PR that commit-by-commit may be convenient / increase confidence in this being a mechanical change. This is not mandatory to review things this way though - if you prefer the other way around feel free to review the whole change rather than individual commits. Separate commits also help easily narrowing the scope of the PR when reviewers ask so. Then it becomes a simple commit removal, rather than rewriting the whole change manually. |
A lot of code in this repo manipulates (term, index) tuples as ints. Getting it wrong can be costly, since entry IDs are centrepiece of raft safety. This commit introduces a type that will be used to replace pairs of ints, to make the code more readable and safe. Signed-off-by: Pavel Kalinnikov <[email protected]>
Signed-off-by: Pavel Kalinnikov <[email protected]>
Signed-off-by: Pavel Kalinnikov <[email protected]>
The lastTerm() method is used in pair with lastIndex() almost in every occasion. Returning the whole pair from a method makes it cleaner and cheaper. The lastIndex() and lastTerm() methods in the worst case fall back to storage, so doing it once is better than twice. Signed-off-by: Pavel Kalinnikov <[email protected]>
Signed-off-by: Pavel Kalinnikov <[email protected]>
Signed-off-by: Pavel Kalinnikov <[email protected]>
Signed-off-by: Pavel Kalinnikov <[email protected]>
42a3fef
to
8d19377
Compare
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]>
8d19377
to
2c8980b
Compare
@ahrtr Addressed actionable comments. Thanks for the review! |
raft.go
Outdated
if prev.index < r.raftLog.committed { | ||
// TODO(pav-kv): construct logSlice up the stack next to receiving the | ||
// message, and validate it before taking any action (e.g. bumping term). | ||
a := logSlice{term: m.Term, prev: entryID{term: m.LogTerm, index: m.Index}, entries: m.Entries} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Probably we should add a function something like message2LogSlice
to encapsulate the details.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added a logSliceFromMsgApp
func, because it should specifically be a MsgApp
message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
because it should specifically be a
MsgApp
message.
In that case, we should add a verification each time when the function is called in case it's misused.. The verification should only be executed in test. Let me add a generic verification framework/utility in a separate PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM with a minor comment
Great work. Thanks.
Signed-off-by: Pavel Kalinnikov <[email protected]>
2c8980b
to
bd5b421
Compare
This PR introduces type-safe wrappers for log entry IDs and raft log slices. We will use them for a more readable and safe code. Some usage is introduced in this PR, see individual commits.
A lot of code in this repo manipulates
(term, index)
tuples as separate raw ints. Likewise, entry slices are handled without association with the leader term. Getting this usage wrong can be costly, since both entry IDs and log slices are centrepiece of raft safety.Related to: #142, #144