smol-rs / async-broadcast

Async broadcast channels

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Way to obtain new Receivers without needing to drain one

tv42 opened this issue · comments

commented

Hi. I have a use case where consumers of an event stream may come and go.
Right now I'm holding a Receiver just to be able to clone it, but that means I have to go out of my way to drain it, so the queue doesn't fill up. And Receiver::recv/try_recv is a &mut self method, so now I need an Arc<Mutex<Receiver>>, even if cloning didn't need that &mut.

Would it be possible to have an API where I can drop/disable even the last Receiver, but still obtain new ones, somehow?

commented

Strawman proposal:

let b = async_broadcast::new(10);
let new_sender = b.sender();
let new_receiver = b.receiver();

broadcast is now just a wrapper function:

pub fn broadcast<T>(cap: usize) -> (Sender<T>, Receiver<T>) {
    let b = new(cap);
    let s = b.sender();
    let r = b.receiver();
    (s, r)
}

Right now I'm holding a Receiver just to be able to clone it, but that means I have to go out of my way to drain it, so the queue doesn't fill up.

It's very interesting that the first issue we get is the one that I myself am facing in zbus (the project for which I resurrected this forgotten project). I'm already on it actually (#1), with a different solution than the one you proposed. I just need to figure out the proper math/algorithm for "rewinding" the receivers when overflow occurs.

Strawman proposal:

let b = async_broadcast::new(10);
let new_sender = b.sender();
let new_receiver = b.receiver();

So the first sender and receiver are created independently, after the channel creation? Hmm.. I guess that could work for your case at least. In our case (in zbus), there is also the issue of some streams sticking around forever and they might never pull so the channel will end up being full. The solution I'm working on in #1 should work for both cases. If I fail, I'll re-consider your proposal for sure and see if somehow it can be made to work for zbus too.

commented

I don't think I actually want what's in #1. That seems to add a mode where sends are lossy, and I don't see that as desirable at all. What I believe I want is a reliable FIFO broadcast mechanism that doesn't require extra work to have 0 receivers at a given moment; 0 receivers can't be holding up the queue!

Naturally, I'm 100% ok looking for that elsewhere, this is open source after all. But async-broadcast otherwise seems to tick all the boxes, and my workaround of always having one receiver, and explicitly draining that special one after every send, seems to be enough to get the functionality.

I don't think I actually want what's in #1. That seems to add a mode where sends are lossy, and I don't see that as desirable at all.

Just to be clear, it's only lossy for the receiver that doesn't pull (or pull fast enough). If in your case, the actual receivers (that come and go) don't just stick around but actively pull, they won't lose any messages.

What I believe I want is a reliable FIFO broadcast mechanism that doesn't require extra work to have 0 receivers at a given moment; 0 receivers can't be holding up the queue!

For sure but does it make sense for the sender (or the channel) have to exist when there is no receivers? Would it be a lot of work in the client code to keep Option<(Sender, Receiver)> around and initialize it when there is a sender and receiver and set to None when no receiver is left (i-e channel is closed)? Just thinking out loud here.

I'm open to your proposal (and actually I might need something like that in combination with #1 myself in zbus) but I think it should be implemented on top of the current API rather. The current API is very consistent with channels (especially our own async-channel) and broadcast APIs provided by various crates so I'll be a bit hesitant to change it.

As an alternative, how about a constructor just for the Sender along with methods on the sender to create a receiver?

Naturally, I'm 100% ok looking for that elsewhere, this is open source after all.

That's great but let's first discuss and see if there is a solution that work and we both feel good about. If we fail, then sure. :)

commented

The "or pull fast enough" is what makes it, by definition, lossy.

Option<(Sender, Receiver)> would mean needing to tie dropping of last receiver into the thing that sets this to None. I'd rather contain that problem in a subsystem, whether it's inside async-broadcast or not.

As an alternative, how about a constructor just for the Sender along with methods on the sender to create a receiver?

Yes, that would be all I need.

Especially if it'll work as &self without &mut, like current Sender::broadcast. If it's &mut then I need to wrap it in Arc<Mutex<>> to coordinate between sender and receiver-maker, which is a little silly because I know there's locks inside of the sender.

Yes, that would be all I need.

Cool. I'll give it a try. If you could test it out when the PR is up, that would be great.

Especially if it'll work as &self without &mut, like current Sender::broadcast.

Yeah, I don't think we need &mut there but let's see.

@tv42 Please have a look at #4 and let me know what you think and if you can give it some testing, that would be great too.

@tv42 Could you please let me know if #4 is what you'll expect? If the API looks good to you, I'll merge and release soon. You can test it after the release but would be great if you could do that before (in case there is any ergonomic issues with the API).

@tv42 Could you please let me know if #4 is what you'll expect? If the API looks good to you, I'll merge and release soon.

I need to make some fixes so I merged it already for my convenience but please do let me know what you think of the API, it's not set it stone.

@tv42 Just FYI, based on feedback from @yoshuawuyts, I modified the API (#10). You don't have a special constructor anymore but rather API to activate and deactivate a receiver.

commented

@zeenix Sorry it'll likely take a few days before I'm likely to have time to look at this again.

@zeenix Sorry it'll likely take a few days before I'm likely to have time to look at this again.

Ok, np. In the meantime, the API has already been released. I tested it and using in my own code. I hope it works for you too.