-
Notifications
You must be signed in to change notification settings - Fork 185
Open
Description
Summary
Make ClientWriteResponse.data field optional (Option<C::R>) to better reflect Raft semantics where only normal log entries have application response data.
Current Behavior
The ClientWriteResponse structure currently requires a data field for all responses:
pub struct ClientWriteResponse<C: RaftTypeConfig> {
pub log_id: LogIdOf<C>,
pub data: C::R, // always required
pub membership: Option<Membership<C>>,
}This forces state machine implementations to return dummy/default values for entries that have no meaningful response data:
async fn apply<I>(&mut self, entries: I) -> Result<(), StorageError<C>> {
for (entry, responder) in entries {
let response = match entry.payload {
EntryPayload::Blank => {
// Must create dummy response even though blank entries have no data
ClientResponse(None)
}
EntryPayload::Normal(ref data) => {
// Only normal entries have actual response data
let result = self.process(data);
ClientResponse(Some(result))
}
EntryPayload::Membership(ref mem) => {
// Must create dummy response even though membership changes have no data
ClientResponse(None)
}
};
responder.send(response);
}
Ok(())
}Proposed Change
Change the data field to be optional:
pub struct ClientWriteResponse<C: RaftTypeConfig> {
pub log_id: LogIdOf<C>,
pub data: Option<C::R>, // optional
pub membership: Option<Membership<C>>,
}Updated state machine implementation:
async fn apply<I>(&mut self, entries: I) -> Result<(), StorageError<C>> {
for (entry, responder) in entries {
let response = match entry.payload {
EntryPayload::Blank => {
// No dummy value needed
None
}
EntryPayload::Normal(ref data) => {
// Only create response for normal entries
let result = self.process(data);
Some(result)
}
EntryPayload::Membership(ref mem) => {
// No dummy value needed
None
}
};
responder.send(response);
}
Ok(())
}Updated client code:
let result = raft.client_write(request).await?;
// Clients can distinguish "no data" from "data is None"
match result.data {
Some(data) => {
// Normal entry with application response
process_response(data);
}
None => {
// Blank or membership entry - check membership field
if let Some(membership) = result.membership {
handle_membership_change(membership);
}
}
}Benefits
- Semantic clarity: The API explicitly represents that response data is optional, matching Raft protocol semantics
- Cleaner state machine code: Eliminates the need to construct dummy/default response values for blank and membership entries
- Better client experience: Clients can clearly distinguish between "no response data" and "response data is present but may be None/empty"
- More idiomatic Rust: Using
Option<T>to represent "may not have a value" is the idiomatic way
Implementation
Required changes:
-
Core API (
openraft/src/raft/message/client_write.rs):- Change
pub data: C::Rtopub data: Option<C::R> - Update
response()method to returnOption<&C::R>
- Change
-
Internal implementation (
openraft/src/storage/v2/apply_responder_inner.rs):- Change
send()method signature fromsend(self, response: C::R)tosend(self, response: Option<C::R>) - Update response construction to wrap
responseindatafield
- Change
-
All state machine implementations:
- Update to return
Option<C::R>instead ofC::Rtoresponder.send() - Remove dummy value construction for blank/membership entries
- Update to return
-
Client code:
- Update to handle
Option<C::R>when accessingresponse.data
- Update to handle
Migration Guide
For state machine implementations:
// Before:
responder.send(ClientResponse(None)); // dummy for blank/membership
// After:
responder.send(None); // no dummy needed// Before:
responder.send(response); // response: C::R
// After:
responder.send(Some(response)); // response: C::RFor client code:
// Before:
let data = result.data; // data: C::R
// After:
let data = result.data?; // data: C::R, or handle None
// or
if let Some(data) = result.data { ... }Version
This is a breaking change suitable for 0.10.0 release.
Metadata
Metadata
Assignees
Labels
No labels