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: Add tagged notifications #52

Merged
merged 9 commits into from
May 13, 2023
Merged

feat: Add tagged notifications #52

merged 9 commits into from
May 13, 2023

Conversation

notgull
Copy link
Member

@notgull notgull commented Apr 3, 2023

Supersedes #40 by adding tags to the event. Similarly to #40, this PR:

  • Adds a T parameter to Event and EventListener that is () by default.
  • Breaking: Makes it so wait() and friends return T or Option<T> instead of () and bool.
  • Makes it so polling EventListener as a future returns T instead of ().

Unlike #40, I wanted to use a more sustainable method for notifying the event. Introducing the Notification trait. Its API looks like this:

trait Notification {
    type Tag;
    fn is_additional(&self) -> bool;
    fn count(&self) -> usize;
    fn fence(&self);
    fn next_tag(&mut self) -> T;
}

This roughly corresponds to the implicit parameters to the current event.notify_<is_additional>_<relaxed>(count) function. is_additional sets the additional property to true, and relaxed makes the fence do nothing instead of emitting a SeqCst fence. The next_tag function emits the tag that gets attached to the notification.

I wanted to give this some power, so I created the IntoNotification trait as well.

trait IntoNotification {
    type Tag;
    type Notify: Notification<Tag = Self::Tag>;
    fn into_notification(self) -> Self::Notify;
    
    fn additional(self) -> Additional<Self::Notify> { .. }
    fn relaxed(self) -> Relaxed<Self::Notify> { .. }
    fn tag<T: Clone>(self, tag: T) -> Tag<Self::Notify, T> { .. }
    fn tag_with<T, F: FnMut() -> T>(self, f: F) -> TagWith<Self::Notify, F> { .. }
}

impl<N: Notification> IntoNotification for N { .. }
impl IntoNotification for usize { .. }

This way, if we make notify() take an impl IntoNotification instead of usize, it still works as intended, as the implementation for usize::into_notification is non-additional and non-relaxed:

event.notify(3);

You can also use this system to add combinators (.additional(), .relaxed()), such that:

// Old code:
event.notify_additional(3);

// New code:
event.notify(3.additional());

Then, to add tags to a notification, you would do:

// Assume tag is a Vec<usize>
event.notify(3.tag(vec![1, 2, 3]));

// Alternate method
event.notify(3.tag_with(|| vec![1, 2, 3]));

Discussion questions:

  • Should Notification and IntoNotification be sealed? If it is, it means we can add new methods to it without worrying. If it isn't, it means that users can implement their own notification types. However, that may cause behavior we don't expect.

This will be used to replace the notify_* family of functions.
I missed that this is needed for notification propogation.
Sets up tagged events to use the Notification trait.
@notgull
Copy link
Member Author

notgull commented Apr 4, 2023

Blocked on taiki-e/portable-atomic#93, since portable_atomic_util::Arc doesn't implement Unpin and thus can't be projected until it does. Fixed!

Fixes the following issues:

- Failed to build on MSRV due to Unpin bound
- Failed to build on no_std, as I didn't import Box
- Failed to build on portable-atomic, as Arc didn't implement Unpin
@fogti
Copy link
Member

fogti commented Apr 4, 2023

Should Notification and IntoNotification be sealed? If it's not, it means we can add new methods to it without worrying. If it is, it means that users can implement their own notification types. However, that may cause behavior we don't expect.

I think you've messed up the negation of "sealed" here.

Also, if we don't seal it, it might be a good idea to split up the IntoNotification trait up, so that the functions which users should be able to customize are separated from the rest (where customization wouldn't make sense).

This PR seals these two traits to prevent breaking changes from causing
semver breaks. It also adds documentation to both traits.
@notgull
Copy link
Member Author

notgull commented Apr 5, 2023

I think you've messed up the negation of "sealed" here.

Also, if we don't seal it, it might be a good idea to split up the IntoNotification trait up, so that the functions which users should be able to customize are separated from the rest (where customization wouldn't make sense).

Thanks for the catch!

I've decided to seal it anyways, since you can do whatever you want using the combinators.

@notgull notgull requested a review from fogti April 11, 2023 21:35
@@ -12,8 +13,10 @@ use alloc::boxed::Box;
use core::num::NonZeroUsize;
use core::ptr;

pub(super) type GenericTags<T> = Box<dyn FnMut() -> T + Send + Sync + 'static>;
Copy link
Member

Choose a reason for hiding this comment

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

is it really a good idea to require an allocation for usage? (why not make it generic in such a way that T is a function itself, which would be more general and probably also faster)

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunately it's necessary. This structure is stored directly inside of Queue<T>, so we can't use the function as a parameter without it propagating all the way to Event and EventListener.

In addition, this is on a cold path, so it's unlikely to happen in the first place.

Copy link
Member

Choose a reason for hiding this comment

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

hm so why can't we parameterize Event and EventListener with that anyways and create type aliases for this case? It would blow up the types a bit, but I don't think it would be bad in practical application

Copy link
Member Author

Choose a reason for hiding this comment

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

The closure stored in these boxes isn't a user provided closure; it's this closure. Until we have TAIT, we can't actually get a type for this closure, therefore we have to box it for now.

Copy link
Member

Choose a reason for hiding this comment

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

Ohhh... but why do we have to use such a closure and not package it into a custom struct and idk... have it implement Iterator or such (or why not just a counter and a generating function?)

Copy link
Member Author

Choose a reason for hiding this comment

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

You're right. I've replaced the boxed closure with a Vec of tags. It should have the same effect.

@notgull notgull requested a review from fogti April 20, 2023 00:51
This should hopefully reduce the amount of allocation on the cold no_std
path
@notgull
Copy link
Member Author

notgull commented May 10, 2023

The MSRV failure looks spurious to me; running cargo +1.39 build and cargo +1.39 build --no-default-features both work on my machine.

@fogti
Copy link
Member

fogti commented May 13, 2023

I'm not like 100% sure about the design, but it looks ok to me now.

@notgull notgull merged commit 38f6e7c into master May 13, 2023
@notgull notgull deleted the notgull/tagged-events-2 branch May 13, 2023 22:33
This was referenced May 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

2 participants