Skip to content
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

vsr: fix races in client_replies #1515

Merged
merged 1 commit into from Feb 5, 2024
Merged

vsr: fix races in client_replies #1515

merged 1 commit into from Feb 5, 2024

Conversation

matklad
Copy link
Member

@matklad matklad commented Feb 5, 2024

  • it might be the case that writing bitset is not set, but there's a write for the slot in the queue: replies can wait on read to complete.
  • when a faulty read completes, it might clobber faulty bit, unset by a a write which was scheduled after the read.

The specific series of events here:

  1. Replica receives a RequestReply and starts a reply read
  2. The read completes with a failure, replica sets the faulty bit
  3. Replica receives RequestReply starts a reply read
  4. Replica receives Reply and starts a reply write - the write unsets faulty bit - the write doesn't start, because there's a read executing
  5. The read completes, setting the faulty bit again
  6. Replica receives RequestReply
    • It doesn't start reply read, because there's an in-progress write that can resolve a read.
    • But the faulty bit is set, tripping up an assertion.

The root issue here is the race between a read and a write for the same reply. Remove the race by explicitly handling the interleaving:

  • When submitting a read, resolve it immediately if there's a pending write (this was already handled by read_reply_sync)
  • When submitting a write, resolve any pending reads for the same reply.
  • Remove the code to block the write while the read is in-progress, as this is no longer possible.

Note that it is still possible that a read and a write for the same slot race, if they target different replies. In this case, there won't be clobbering, as, when the read completes, we double-check freshness by consulting client_sessions.

SEED: 2517747396662708227
Closes: #1511

@matklad
Copy link
Member Author

matklad commented Feb 5, 2024

Alternative fix is available at #1513, though I like this version more.

sentientwaffle
sentientwaffle previously approved these changes Feb 5, 2024
@@ -243,6 +243,15 @@ pub fn ClientRepliesType(comptime Storage: type) type {
client_replies.write_reply_next();
}

if (callback == null) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could rewrite this into const callback = read.callback orelse { ... to avoid callback.? later.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be a bit more complicated as the read's released here already, but, on balance, yeah, this seems better.

while (reads.next()) |read| {
if (read.slot.index == write.slot.index) return;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe in write_reply_callback we could iterate the reads and assert that none of them are to the slot that we just wrote?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There actually can be races after the state sync.

TIL: in our testing storage, we forbid write-write races, but allow write-read races. Which ... seems fine? At least in this case.

- it might be the case that `writing` bitset is not set, but there's a
  write for the slot in the queue: replies can wait on read to complete.
- when a faulty read completes, it might clobber faulty bit, unset by a
  a write which was scheduled after the read.

The specific series of events here:

1. Replica receives a RequestReply and starts a reply read
2. The read completes with a failure, replica sets the faulty bit
3. Replica receives RequestReply starts a reply read
4. Replica receives Reply and starts a reply write
    - the write unsets faulty bit
    - the write doesn't start, because there's a read executing
4. The read completes, setting the faulty bit _again_
5. Replica receives RequestReply
   - It _doesn't_ start reply read, because there's an in-progress write
     that can resolve a read.
   - But the faulty bit is set, tripping up an assertion.

The root issue here is the race between a read and a write for the same
reply. Remove the race by explicitly handling the interleaving:

* When submitting a read, resolve it immediately if there's a pending
  write (this was already handled by `read_reply_sync`)
* When submitting a write, resolve any pending reads for the same reply.
* Remove the code to block the write while the read is in-progress, as
  this is no longer possible.

Note that it is still possible that a read and a write for the same slot
race, if they target different replies. In this case, there won't be
clobbering, as, when the read completes, we double-check freshness by
consulting `client_sessions`.

SEED: 2517747396662708227
Closes: #1511
@matklad matklad added this pull request to the merge queue Feb 5, 2024
Merged via the queue into main with commit d1ce66c Feb 5, 2024
25 checks passed
@matklad matklad deleted the matklad/raced2 branch February 5, 2024 23:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Crash: 2517747396662708227
2 participants