-
Notifications
You must be signed in to change notification settings - Fork 172
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
middleware refactoring #793
Conversation
core/src/middleware.rs
Outdated
/// Called when a new JSON-RPC comes to the server. | ||
fn on_request(&self) -> Self::Instant; | ||
/// Called when a new JSON-RPC request comes to the server. | ||
fn on_request(&self, remote_addr: std::net::SocketAddr, headers: &http::HeaderMap) -> Self::Instant; |
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.
This is a bit weird as the WS
will have the same remote addr and headers for the entire connection
Then for HTTP
it will be useful for each "request".
So what we could do here:_
on_connect(&self, remote_addr: std::net::SocketAddr, headers: &http::HeaderMap)
on_request(&self)
Then justify the documentation that on_connect
for can be used to inspect headers and so on for stateless protocols, we could also move these to different traits as I could be useful to separate middleware impls because HTTP/WS is quite different.
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've been mulling this over; I'm personally leaning towards separate middleware for Http and Ws, since it feels like we have to hack it around to have functions which map nicely to both.
Pros of separate middlware:
- precise traits that better express what's actually happening. (http middleware doesn't need on_connect, ws middleware doesn't need headers in on_request, that sort of thing)
- (maybe this helps perf or something?)
Cons:
- we need to have two similar traits and sets of blanket impls.
- users/we can't write generic middleware that applies to both (though I doubt that's an issue, and one could always write some wrapper
GenericMiddleware
thing that implements both where there is overlap if needed)
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, I agree it's better to have two different traits.
You most likely want to handle them differently.
ws-server/src/server.rs
Outdated
@@ -427,252 +468,79 @@ async fn background_task( | |||
|
|||
tracing::debug!("recv {} bytes", data.len()); | |||
|
|||
let request_start = middleware.on_request(); | |||
let request_start = middleware.on_request(remote_addr, &headers); |
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.
So, it looks like those headers will only ever contain Host and Origin from above; what about any custom headers?
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.
it's missing from soketto but I don't want to make this PR any larger.
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.
Gotcha; makes sense!
@@ -304,8 +324,8 @@ where | |||
} | |||
} | |||
|
|||
async fn background_task( | |||
server: SokettoServer<'_, BufReader<BufWriter<Compat<tokio::net::TcpStream>>>>, | |||
struct BackgroundTask<'a, M> { |
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.
Good idea to make this a struct!
where | ||
M: Middleware, | ||
{ | ||
async fn handshake<M: Middleware>(socket: tokio::net::TcpStream, mode: HandshakeResponse<'_, M>) -> Result<(), Error> { |
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 out of scope of this PR but it feels like having an accept_handshake
and reject_handshake
call (or similar) would make more sense, since this call basically just switches on an enum and has very little shared between them?
@@ -37,22 +39,22 @@ | |||
pub trait Middleware: Send + Sync + Clone + 'static { | |||
/// Intended to carry timestamp of a request, for example `std::time::Instant`. How the middleware | |||
/// measures time, if at all, is entirely up to the implementation. | |||
type Instant: Send + Copy; | |||
type Instant: std::fmt::Debug + Send + Sync + Copy; |
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.
Another offtopic thought (just noting for future reference); before 1.0 is it worth making this more generic than Instant
so that it can, if needed, be a way for users to pass context between middleware calls and such?
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 elaborate a bit what you mean with an example?
It's possible to copy it between calls and stuff right now
fn on_response(&self, result: &str, started_at: Self::Instant); | ||
|
||
/// Called when a client disconnects | ||
fn on_disconnect(&self, remote_addr: std::net::SocketAddr); |
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.
this is silly but I reckon that it might be useful to log when peers disconnect
fn on_request(&self, remote_addr: SocketAddr, headers: &Headers) -> Self::Instant; | ||
|
||
/// Called on each JSON-RPC method call, batch requests will trigger `on_call` multiple times. | ||
fn on_call(&self, method_name: &str, params: Params); |
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.
do we want method kind
here as well i.e, if the call is a "subscription", "method call" or "notification"?
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.
Is it easily accessbile? I guess it wouldn't do any harm to have!
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 don't do it because it requires to lookup the method name in the HashMap.
should be quite easy to do.
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.
Mmm, if it's a performance hit to do so (even when no middleware) then I'm happy not at the mo!
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.
we do this lookup when doing the call so I didn't want to do it because the timing might be off.
but that it is probably picking hairs and I can do on_call
after looking up the method
} | ||
|
||
#[derive(Debug, Clone)] | ||
struct CallData<'a, M: Middleware> { |
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.
this should be cheap to copy
use jsonrpsee::ws_client::WsClientBuilder; | ||
use jsonrpsee::ws_server::{RpcModule, WsServerBuilder}; | ||
|
||
#[derive(Clone)] | ||
struct Timings; | ||
|
||
impl middleware::Middleware for Timings { | ||
impl middleware::WsMiddleware for Timings { |
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 love the middleware split!
I'm still wondering about type Instant
. If I want to share some state between middleware calls, or capture the timings between say on_connect
and on_disconnect
or some random other thing, currently we can't do it.
But we could if we did something like:
trait WsMiddleware {
type State: Default + Clone;
fn on_connect(&Self, state: &mut State, ...otherParams);
fn on_request(&self, state: &mut State, ...otherParams);
//... and so on; mutable per-connection/request state given to each call
}
Then we can set/read the state somewhat arbitrarily in any call that's interested, eg for timings:
struct ConnectionTimings;
struct ConnectionTimingsState(Option<Instant>);
impl WsMiddleware for ConnectionTimings {
type State = ConnectionTimingsState;
fn on_connect(&self, state: &mut State, ...) {
*state = Some(Instant::now());
}
fn on_disconnect(&self, state: &mut State, ...) {
println!("Disconnected; connection open for {}", state.0.unwrap().elapsed());
}
}
Or something that logs the time of each call and prints them all off at the end or whatever else.
A default "no middleware" impl can have State = ()
which should be free.
Just a thought, since we are expanding middleware to be more useful; what do you think?
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.
(this could very well be a separate issue too if you liked the idea; no need to do it all here necessarily).
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.
Sounds reasonable.
I guess it would be flexible as users can "measure" whatever they want as all methods on middleware traits are supposed to have &mut State
such how long a connection lives, individual calls, batches and so on takes?
So why not.
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.
That was my take on it yeah; it'd just give the flexibility to keep track of whatever was desired across the lifetime of a single connection :)
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.
As we agreed, let's think about this in a followup issue/pr!
Edit: issue added: #816
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! 👍 As a follow-up we could extend the middleware with method_kind
and custom user context/state to extend the type Instant
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 to me!
Closing #758, #747 and #699
It's a quite significant refactoring where the
sink
is only used for subscriptions as the rest of the calls just returnsMethodResponse
which is a type that has the final JSON-RPC response and status flag whether the calls was successful or not.In order to intercept the messages to log the full responses in the middleware.
Further, it also modifies the
SubscriptionMethod
to be a future which returns when the subscription call has been answered which in turn forces plenty noise i.e, I had to change lots of code.In addition I noticed thatSubscriptionSink::send(&mut self, msg: M)
is not needed so I changed it toSubscriptionSink::send(&self, msg: M)
also thenpipe_from_stream
doesn't need&mut self
anymore.rustc may benefit from that to some optimizations....
It splits the middleware into two traits for WS and HTTP.