-
Notifications
You must be signed in to change notification settings - Fork 157
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
RFC: Client request pipelining #1068
Comments
👋 Thanks for opening this issue! Get help or engage by:
|
I have a general understanding of your concept. Additionally, would you be able to provide the method signatures for the revised |
No, As for the signature, it would be: pub async fn client_write(
&self,
app_data: C::D,
) -> Result<ClientWriteResponse<C>, RaftError<C::NodeId, ClientWriteError<C::NodeId, C::Node>>>
where C::ReplyConsumer: Future<Output = Result<
ClientWriteResponse<C>,
RaftError<C::NodeId, ClientWriteError<C::NodeId, C::Node>>
>>;
pub fn client_write_ff(&self, app_data: C::D); The only suboptimal thing is that you can call |
|
The idea is to pass additional information "hidden" in
The user won't get the instance. The In the other case where the user implements |
Make sense. The abstraction for queuing replies is an elegant design. 👍 |
@schreter The instance of The pub type AppDataResult<C> = Result<(LogIdOf<C>, ROf<C>), ForwardToLeader<C>>;
#[derive(Debug, Clone, thiserror::Error)]
#[error("The response consumer has been closed.")]
pub struct Closed;
pub trait ReplyConsumer<C>: OptionalSend + 'static
where C: RaftTypeConfig
{
fn from_request(
app_data: C::D,
) -> (
C::D,
Self,
Option<impl Future<Output = Result<AppDataResult<C>, Closed>>>,
)
where Self: Sized;
// ...
} Make sence? |
You are right, we need two objects, one that we send to consume the reply (e.g., send it over pub trait ReplyConsumer<C>: OptionalSend + 'static
where C: RaftTypeConfig
{
/// The type generated for the send side, which may be a `Future`.
/// (the name is preliminary, it's suboptimal)
type SendResult;
fn from_request(
app_data: C::D,
) -> (
C::D,
Self,
Self::SendResult,
)
where Self: Sized;
// ...
}
impl Raft {
// ...
pub async fn client_write(
&self,
app_data: C::D,
) -> Result<ClientWriteResponse<C>, RaftError<C::NodeId, ClientWriteError<C::NodeId, C::Node>>>
where C::ReplyConsumer::SendResult: Future<Output = Result<
ClientWriteResponse<C>,
RaftError<C::NodeId, ClientWriteError<C::NodeId, C::Node>>
>>;
pub fn client_write_ff(&self, app_data: C::D) -> C::ReplyConsumer::SendResult;
// ...
} I.e., the Then, pub async fn client_write(
&self,
app_data: C::D,
) -> Result<ClientWriteResponse<C>, RaftError<C::NodeId, ClientWriteError<C::NodeId, C::Node>>>
where C::ReplyConsumer::SendResult: Future<Output = Result<
ClientWriteResponse<C>,
RaftError<C::NodeId, ClientWriteError<C::NodeId, C::Node>>
>> {
self.client_write_ff(app_data).await
} |
Make sense |
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: databendlabs#1068
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: databendlabs#1068
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: databendlabs#1068
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: databendlabs#1068
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: databendlabs#1068
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: #1068
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: databendlabs#1068
…t write response This commit introduces the `Responder` trait that defines the mechanism by which `RaftCore` sends responses back to the client after processing write requests. Applications can now customize response handling by implementing their own version of the `RaftTypeConfig::Responder` trait. The `Responder::from_app_data(RaftTypeConfig::D)` method is invoked to create a new `Responder` instance when a client write request is received. Once the write operation is completed within `RaftCore`, `Responder::send(WriteResult)` is called to dispatch the result back to the client. By default, `RaftTypeConfig::Responder` retains the existing functionality using a oneshot channel, ensuring backward compatibility. This change is non-breaking, requiring no modifications to existing applications. - Fix: databendlabs#1068
`Raft<C>::client_write_ff() -> C::Responder::Receiver` submit a client request to Raft to update the state machine, returns an application defined response receiver `Responder::Receiver` to receive the response. `_ff` means fire and forget. It is same as [`Raft::client_write`] but does not wait for the response. When using this method, it is the application's responsibility for defining mechanism building and consuming the `Responder::Receiver`. - Part of databendlabs#1068
`Raft<C>::client_write_ff() -> C::Responder::Receiver` submit a client request to Raft to update the state machine, returns an application defined response receiver `Responder::Receiver` to receive the response. `_ff` means fire and forget. It is same as [`Raft::client_write`] but does not wait for the response. When using this method, it is the application's responsibility for defining mechanism building and consuming the `Responder::Receiver`. - Part of #1068
`Raft<C>::client_write_ff() -> C::Responder::Receiver` submit a client request to Raft to update the state machine, returns an application defined response receiver `Responder::Receiver` to receive the response. `_ff` means fire and forget. It is same as [`Raft::client_write`] but does not wait for the response. When using this method, it is the application's responsibility for defining mechanism building and consuming the `Responder::Receiver`. - Part of databendlabs#1068
Currently,
client_write()
sends the request toopenraft
viampsc
channel, which spools (potentially many) requests. Theopenraft
core works on batches of requests and produces batches of responses. However, on theclient_write()
side, each task has to explicitly await theclient_write()
.To improve things, one could think about multiple solutions.
What immediately comes to mind is to use another
mpsc
channel, where the consumer of replies can consume them at its own pace (also pipelined). However, this would probably require major rework and I don't know whether we can integrate it with existing API. So I don't think this is the way to go.What we already have, is a semi-abstraction of reply handling - sending the reply via
oneshot
channel. So my suggestion would be the following:Instead of hard-coding a
oneshot
channel to send the reply back, theRaftMsg::ClientWriteRequest
would require a new typeReplyConsumerType: ReplyConsumer
or so on the config fortx
.The
ReplyConsumer
would be something like this:so it can be built from
AppData
. The only additional change inopenraft
would be to usetx.request_completed()
instead oftx.send()
to send a reply toClientWriteRequest
.The default implementation of
ReplyConsumer
would internally create and useoneshot
channel and implement aFuture
(basically, wrapping the request as is done today). This would require zero change to the user code (beyond specifying the default type forReplyConsumer
).If the user uses the default implementation for
ReplyConsumerType
, thenclient_write()
would be simply enabled bywhere ReplyConsumerType: Future
. There would be no change to the client. Of course, the user is also free to create a different implementation which implementsFuture
, soclient_write()
can be used also with custom implementation.A second, synchronous API
client_write_ff()
(or so,ff
for fire-and-forget) would not require implementingFuture
, instead it would rely on the implementation ofReplyConsumer
doing the "right thing" to send the reply for further pipelined processing (e.g., via posting to a user-specificmpsc
channel spooling replies).With this fairly small change, we could enable fully-pipelined processing also in the caller of
client_write()
w/o resorting to a workaround with task-per-request.Thoughts?
The text was updated successfully, but these errors were encountered: