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

Support duplicated requests #317

Closed
akiradeveloper opened this issue Oct 16, 2023 · 3 comments
Closed

Support duplicated requests #317

akiradeveloper opened this issue Oct 16, 2023 · 3 comments
Assignees
Milestone

Comments

@akiradeveloper
Copy link
Owner

akiradeveloper commented Oct 16, 2023

$6.3 of the Raft author's thesis describes at-least-semantics which is not suitable for real use.

The figure below well explains the problem: What if the leader is dead before response and the client "re"send the request to other node? The expectation is that the new leader responds with ok as leader transition has been done "transparently".

スクリーンショット 2023-10-16 9 30 14

I have no clue about the implementation detail so I made a question to community but no answer yet. So here I describe the implementation detail which is now in my mind. 

My idea exploits replication. It wraps RaftStorage with thin layer so to attach new attributes (client_id, seq_num) in each entry. This way, the log entry will be logically (clock, command, client_id, seq_num) but the persistent layer only holds the two in front. (because it is too nonsense to have such temporary attributes in disks) Then the attributes are replicated to followers.

Now, servers find the relationship index -> (client_id, seq_num). When a server applies command(index) then it can memoize (client_id, seq_num) -> response but only the first time. In the second time, it "skips" application and responds with the memoized response.

@akiradeveloper
Copy link
Owner Author

akiradeveloper commented Oct 16, 2023

// This saved in RaftStorage
struct Entry0 {
  // (Term, Index)
  clock: Clock,
  // Command to execute in RaftApp
  command: Bytes,
}

// This is replicated among servers
struct Entry {
  clock: Clock,
  command: Bytes,
  // request of this command
  client_id: String,
  // the sequence number of this command
  seq_num: u64,
}

@akiradeveloper akiradeveloper changed the title Support at-least-once semantics Support duplicated requests Oct 16, 2023
@akiradeveloper
Copy link
Owner Author

akiradeveloper commented Oct 17, 2023

A table (client_id, seq_num) -> response is maintained in the servers but can erase once responded to the client.

here is a pseudo code:

in applying Entry
  command_key = (client_id, seq_num)
  if not resp_table.contains(command_key)
    resp = app.exec(command)
    resp_table.insert(command_key, resp)
  if let Some(completion) = completions.contains(command_index)
    completion.complete_with(resp)
    send(self, RemoveResponse { command_key })
in committing RemoveResponse { command_key }
  resp_table.remove(command_key)

@akiradeveloper
Copy link
Owner Author

Done.

The writer request to the cluster now has a request_id which should be unique to the request.

message WriteRequest {
  bytes message = 1;
  // unique identifier of this request
  // duplicated requests with the same unique identifier are only executed once.
  string request_id = 2;
}

The server can distinguish two requests with the request_id and if the two have the same one, they are executed only once.

For the deduplication, the application now has a response cache

    response_cache: spin::Mutex<HashMap<String, Bytes>>,

which memoizes the response data with the request_id as a key.

This was referenced Oct 25, 2023
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

No branches or pull requests

1 participant