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

feat(rpc module): stream API for SubscriptionSink #639

Merged
merged 24 commits into from
Jan 21, 2022

Conversation

niklasad1
Copy link
Member

@niklasad1 niklasad1 commented Jan 5, 2022

Fixes #627

This PR introduces a new API for the SubscriptionSink to add a stream to send items over the subscription when "new items" get produced, if the connection gets closed it will terminate the task because it has additional channel linked to actual connection.

@niklasad1 niklasad1 changed the title WIP: feat(rpc module): add_stream to subscription sink WIP: feat(rpc module): stream API for SubscriptionSink Jan 10, 2022
/// Connection ID
pub conn_id: ConnectionId,
/// Channel to know whether the connection is closed or not.
pub close: async_channel::Receiver<()>,
Copy link
Member Author

Choose a reason for hiding this comment

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

NOTE: this panics if count > usize::MAX / 2

but if we reach that we likely have other problems such as OOM :)

@niklasad1 niklasad1 changed the title WIP: feat(rpc module): stream API for SubscriptionSink feat(rpc module): stream API for SubscriptionSink Jan 13, 2022
Copy link
Contributor

@dvdplm dvdplm left a comment

Choose a reason for hiding this comment

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

This looks pretty good to me, what is it you want to do more here?

core/src/server/rpc_module.rs Outdated Show resolved Hide resolved
core/src/server/rpc_module.rs Outdated Show resolved Hide resolved
tests/tests/integration_tests.rs Outdated Show resolved Hide resolved
Comment on lines +396 to +397
assert_eq!(Some(()), rx.next().await, "subscription stream should be terminated after the client was dropped");
assert_eq!(Some(()), rx.next().await, "subscription stream should be terminated after the client was dropped");
Copy link
Contributor

Choose a reason for hiding this comment

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

I felt like I understood the test until I got here. I thought rx would produce None after the client was dropped and so it'd be assert!(rx.next().await.is_none()). What am I missing? :/

Copy link
Member Author

@niklasad1 niklasad1 Jan 13, 2022

Choose a reason for hiding this comment

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

haha, maybe it's more clear as you described it but the test actually sends a message on the channel when the subscription terminated.

the reason why is because the tx is kept in the RpcModule and can't be dropped in the subscribe callback.

@niklasad1
Copy link
Member Author

This looks pretty good to me, what is it you want to do more here?

It should be complete but the grumbles is maybe the API (naming) and cloning a bunch async_channel::Receiver...

let (conn_tx, conn_rx) = oneshot::channel::<()>();
let c = conn.expect("conn must be Some; this is bug");
Copy link
Contributor

Choose a reason for hiding this comment

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

If it bust be Some, why don't we restrict it on parameter level?

Copy link
Member Author

Choose a reason for hiding this comment

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

it's way better but I think it requires additional callback types right? see my comment below.

thoughts?

Copy link
Member Author

@niklasad1 niklasad1 Jan 16, 2022

Choose a reason for hiding this comment

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

Ok, I had a look at the code to solve this properly at parameter level one need to lookup at the actual callback to avoid to pass down a bunch of unused parameters.

  // server code
  let result = match RpcModule::as_callback(&req.method) {
     None => {
        send_error(sink, msg),
        continue
     }
     Some(MethodKind::Sync(cb)) => (cb)(id, params, sink, conn_id),
     Some(MethodKind::Async(cb)) => (cb)(id, params, sink, resources).await,
     // should be used for subscriptions...
     // servers that don't support subscriptions should throw an error here...
     Some(MethodKind::Subscription) => (cb)(id, params, sink, resources, conn_state),
  };

  // modify RpcModule::register_subscription 
  pub fn register_subscription<F>(
     &mut self,
     subscribe_method_name: &'static str,
     notif_method_name: &'static str,
     unsubscribe_method_name: &'static str,
     callback: F,
 ) -> Result<(), Error> {
   .... 
   ....
   self.methods.mut_callbacks().insert(
      subscribe_method_name,
      MethodCallback::new_subscription(Arc::new(move |id, params, method_sink, conn| {
        ... 
     }
  );
 }

Copy link
Member Author

Choose a reason for hiding this comment

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

one benefit is not to have to clone this "introduced channel connection_state" for every method instead only for subscriptions where it's actually used... not sure I liked how the old code abstracted this away

Copy link
Contributor

Choose a reason for hiding this comment

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

I already thought about splitting async calls from subscriptions when doing my refactoring earlier, and for regular method calls returning values instead of passing in the sink as a parameter, I reckon that would make things much more readable and straight forward, and potentially make the final binary smaller. So if you want to go that route and add another enum variant I think that's cool, and I can do a PR that switches method calls to have a return value later on :).

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, I already added the enum variant in this PR but just a hacky draft to check that it works and to show you and David what I had in mind :)

Copy link
Member Author

Choose a reason for hiding this comment

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

sounds great to get rid of the sink for the synchronous calls.

core/src/server/rpc_module.rs Outdated Show resolved Hide resolved
S: Stream<Item = T> + Unpin,
T: Serialize,
{
let mut close_stream = self.close.clone();
Copy link
Member Author

@niklasad1 niklasad1 Jan 18, 2022

Choose a reason for hiding this comment

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

this is hack; to workaround borrowing issues when we call self.send below.

we may refactor this to split the SubscripionSink to several types or something.... because the close_stream is mutable borrowed and then we can't borrow any of the other fields here or move out of self.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need plenty of comments in this method, it requires some pretty deep understanding of the architecture and I think we should capture it here.

@niklasad1 niklasad1 marked this pull request as ready for review January 19, 2022 09:28
@niklasad1 niklasad1 requested a review from a team as a code owner January 19, 2022 09:28
@@ -786,9 +746,40 @@ impl SubscriptionSink {
self.inner_send(msg).map_err(Into::into)
}

/// Consume the sink by passing a stream to be sent via the sink.
pub async fn add_stream<S, T>(mut self, mut stream: S)
Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about calling this into_stream? I think "add" implies there could be more than one and that it doesn't quite relay the information about the important changes that this call makes to the sink.

Copy link
Member Author

@niklasad1 niklasad1 Jan 19, 2022

Choose a reason for hiding this comment

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

I don't like add_stream either, but into_stream is not really great either it doesn't return the stream....

maybe run_stream, from_stream, spawn_stream or something else?!

Copy link
Contributor

Choose a reason for hiding this comment

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

Fair; I'd have liked as_stream but as_ is "taken" with different semantics so can't do that.

Of your suggestions I like from_stream the best.

Copy link
Collaborator

Choose a reason for hiding this comment

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

So, this method consumes a stream, feeding the items into the subscription?

I guess I'd go with something like consume_stream or read_from_stream. into_, as_, and from_ all sortof feel like I should expect some result back from this call to me!

Copy link
Member Author

@niklasad1 niklasad1 Jan 19, 2022

Choose a reason for hiding this comment

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

So, this method consumes a stream, feeding the items into the subscription?

yes

I guess we could return a type that impls Sink/SinkExt instead here to make it more readable and flexible i.e, to deal with errors and so on.

Copy link
Contributor

Choose a reason for hiding this comment

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

streamify()?
I think consume_stream is so-so. Yes, we do consume it, but that's not really the point. Rather we're "hooking up" the stream to the sink and leave it there for the duration of the subscription.
with_stream?

Copy link
Collaborator

@jsdw jsdw Jan 20, 2022

Choose a reason for hiding this comment

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

pipe, maybe? we're piping a stream into the subscription.

Copy link
Contributor

Choose a reason for hiding this comment

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

sink.pipe_from_stream? I quite like pipe!

Copy link
Member Author

Choose a reason for hiding this comment

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

I like pipe_from_stream, let's settle for that?

S: Stream<Item = T> + Unpin,
T: Serialize,
{
let mut close_stream = self.close.clone();
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we need plenty of comments in this method, it requires some pretty deep understanding of the architecture and I think we should capture it here.

ws-server/src/server.rs Show resolved Hide resolved
}
Err(err) => {
tracing::warn!("Subscription got error: {:?} terminating task", err);
break Err(err);
Copy link
Member Author

@niklasad1 niklasad1 Jan 20, 2022

Choose a reason for hiding this comment

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

this is most likely a bug when serializing T fails, should we have a flag for this when to exit, just log an continue, or something else?

The error is not really possible to handle but if you require each element to be processed in order, ignore might break expectations so I just terminated the "task" when an error is detected.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think you made the right choice. We could also panic I guess but let's terminate for now.

Copy link
Collaborator

@jsdw jsdw left a comment

Choose a reason for hiding this comment

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

Looks good to me overall, and I like the simplification in substrate!

Copy link
Contributor

@maciejhirsz maciejhirsz left a comment

Choose a reason for hiding this comment

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

Nice, good job separating subscriptions!

@dvdplm dvdplm merged commit 9bd2127 into master Jan 21, 2022
@dvdplm dvdplm deleted the na-hacky-sink-add-stream branch January 21, 2022 13:10
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.

[rpc module]: SubscriptionSink more user-friendly API for sending items from stream.
4 participants