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

Add a subscription manager #548

Merged
merged 16 commits into from
May 27, 2020

Conversation

HCastano
Copy link
Contributor

I was implementing some pubsub APIs in Substrate, outside of sc-rpc, and got to a point where I needed something to execute the subscription Futures. There's nothing provided out of the box for doing that, so after talking with @tomusdrw we figured that it might be useful to have the Substrate subscription manager be a part of the pubsub crate.

What I've done is I've stolen that code and changed it to try and be a little less opinionated. One concern I have though is that I've changed SubscriptionId to now take a generic parameter, N, to avoid only working with u64 ids, and I think this will break people's code.

I'm looking for feedback on whether the general approach is alright, and if the change to SubscriptionId is acceptable.

The idea is to use the `Subscriptions` struct from Substrate, which
is used to drive subscription futures to completion, and modify
it for "general" use.
@parity-cla-bot

This comment has been minimized.


/// Trait used to provide unique subscription ids.
pub trait IdProvider {
// TODO: Maybe have this impl Into<u64>?
Copy link
Contributor

Choose a reason for hiding this comment

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

It should be possible to have String ids as well.

pub struct Manager<I: Default + IdProvider> {
next_id: I,
active_subscriptions: Arc<Mutex<HashMap<I::Id, oneshot::Sender<()>>>>,
executor: TaskExecutor, // Make generic?
Copy link
Contributor

Choose a reason for hiding this comment

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

IMHO It's fine, in most workloads spawning subscription is not going to be a bottleneck so we can safely do a virtual call here.

}

/// Trait used to drive subscription Futures to completion.
pub trait SubscriptionManager {
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure the trait is really needed, I'd rather have Manager with a default generic parameter of u64 subscription IdProvider. Mainly to avoid importing extra traits to call functions.
Do you have some use cases in mind where having it behind trait would be useful?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, my reasoning kinda was that if people wanted a "default" implementation of a SubscriptionManager they'd use the Manager struct, and if they wanted to roll their own they just had to conform to the SubscriptionManager trait. Is it reasonable to expect that people will want to use something other than the "default" implementation? If not, then yeah we don't need to the trait.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since the trait is not really used in the library at all I think a concrete implementation is enough. If someone is building their own solution and needs that abstraction they can introduce a trait themselves and implement it for the type from our library.

pub enum SubscriptionId {
/// U64 number
Number(u64),
pub enum SubscriptionId<N> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Ah, I see what you meant with Into<u64> in the earlier comment. Why not require Into<SubscriptionId> instead? This together with some extra From implementations that we would add here (for numeric types and String as you did now) would allow us to keep that configurable without maiking SubscriptionId generic.

Adds trait bounds that allow conversion between the two, removing
the need for generics in SubscriptionId.
Copy link
Contributor

@tomusdrw tomusdrw 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, just needs some API tweaking and more docs.

@@ -0,0 +1,88 @@
//! Provides an executor for subscription Futures.
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 should expand on this module description a bit. The subscription manager is opinionated way to handle processing subscriptions that are based on Stream coming from the user codebase.
The manager takes care of:

  1. Assigning the ID
  2. Consuming a Stream of events (coming from user code) and transforming it into a subscription notifications to the client.
  3. Spawning a resulting future to executor
  4. Cancelling the stream when the subscription is canceled.

executor: TaskExecutor, // Make generic?
}

impl<I: Default + IdProvider> SubscriptionManager<I> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Default should only be required for new (not for add/executor/etc) and there should be an extra method with_id_provider(TaskExecutor, IdProvider) for non-default cases.

pubsub/src/manager.rs Outdated Show resolved Hide resolved
/// Takes care of assigning unique subscription ids and
/// driving the sinks into completion.
#[derive(Clone)]
pub struct SubscriptionManager<I: Default + IdProvider> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub struct SubscriptionManager<I: Default + IdProvider> {
pub struct SubscriptionManager<I: IdProvider = RandomizedStringProvider> {

We should implement two IdProviders:

  1. NumericIdProvider
  2. RandomizedStringProvider

The first one would simply use AtomicU64 and would return increasing numeric values, the second one - which should also be a default to use if SubscriptionManager does not include generic parameter - should return a randomized strings (guids).
The reason for using randomized strings is to make sure that subscription ids are not easy to guess, so that multiple clients can't cancel each-others subscriptions easily. The first one will be mostly usable for tests or cases where the aforementioned issue is not a problem.

}

#[test]
fn should_convert_from_string_to_subscription_id() {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks for all the test ❤️

Adds two subscription ID providers which can be used
by the SubscriptionManager. One provides a simple numeric
ID, while the other provides a random string.
Copy link
Contributor

@tomusdrw tomusdrw 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! Few tiny things left.

let id: String = iter::repeat(())
.map(|()| rng.sample(Alphanumeric))
.take(self.len)
.collect();
Copy link
Contributor

Choose a reason for hiding this comment

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

Nice and elegant :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

active_subscriptions: Arc<Mutex<HashMap<I::Id, oneshot::Sender<()>>>>,
pub struct SubscriptionManager<I: IdProvider = RandomStringIdProvider> {
id_provider: I,
active_subscriptions: ActiveSubscriptions,
executor: TaskExecutor, // Make generic?
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
executor: TaskExecutor, // Make generic?
executor: TaskExecutor,

/// Creates a new SubscriptionManager.
pub fn new(executor: TaskExecutor) -> Self {
pub fn new_with_id_provider(id_provider: I, executor: TaskExecutor) -> Self {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
pub fn new_with_id_provider(id_provider: I, executor: TaskExecutor) -> Self {
pub fn with_id_provider(id_provider: I, executor: TaskExecutor) -> Self {

AFAIK the overall convention is to skip new_ in with_ constructors.

@@ -62,6 +62,14 @@ impl From<String> for SubscriptionId {
}
}

// TODO: Don't unwrap, and probably implement TryFrom instead of From
use std::convert::TryInto;
impl From<usize> for SubscriptionId {
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 instead of providing this From<usize> implementation we should rather change the NumericIdProvider to convert usize to u64 internally. The reason is that it's highly unlikely for NumericIdProvider to reach the case where this conversion might fail (given it runs on 128-bit processor anyway O.o) and here we are making stronger assumption that usize can be safely converted to SubscriptionId.

Instead of providing a guarantee that we can convert between
`usize` and `u64` we make the assumptions that it's unlikely
that we're running on an architecture larger than 64-bits and
we use a `u64` directly.
@HCastano
Copy link
Contributor Author

I want to write some tests to make sure the SubscriptionManager can add() and cancel() subscriptions before I mark this as "ready for review" (even though you've already reviewed it @tomusdrw , lol).

@HCastano HCastano marked this pull request as ready for review May 27, 2020 15:56
@HCastano HCastano requested a review from tomusdrw May 27, 2020 15:57
@HCastano
Copy link
Contributor Author

Looks like the Windows runner is failing due to an old rustc version which doesn't have matches! stabilized yet

Our Windows CI runner isn't up to date and thinks this
is still a nightly feature
@tomusdrw tomusdrw merged commit 1779dbe into paritytech:master May 27, 2020
@HCastano HCastano deleted the hc-add-subscription-manager branch May 27, 2020 16:37
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.

3 participants