You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It would be nice for select() to be more flexible (add and remove receivers, have optional receivers) without losing its static-ness.
Proposed solution
Improve the composability of existing receivers and create a new one:
Add a channel or receiver that merges messages from multiple receivers (or recycle Merger to do that) and can dynamically add and remove receivers, and just waits forever (it's never ready) when there are no receivers
Maybe add a NopReceiver that does the same as OptionalReceiver(None)
Use cases
I think many cases where users need to handle lots of tasks manually to cope with more dynamic situations, like the resampler, could be greatly simplified if they could be handled automatically by using a more flexible select.
Maybe I should have made this a discussion first. If anyone sees any controversy in this, we can convert it to a discussion. If there are no big objections, we can turn the TODO list into issues as we plan to address them.
I think many cases where users need to handle lots of tasks manually to cope with more dynamic situations, like the resampler, could be greatly simplified if they could be handled automatically by using a more flexible select.
I guess I'm just having trouble imagining what kind of possibilities would come out of these features. It sounds like they're to deal with scenarios where channels that we read from close for some reason. Is that accurate or are you thinking of other scenarios as well?
Is the resampler the only known use-case for these features? I'm not very familiar with it, so maybe that would be just a good first test: it would be nice to see how they would simplify the resampler. Would it help eliminate the two *helper layers?
So we just make a basic interface without implementation for the features proposed in this issue, then see how it affects the resampler, just to make sure we limit feature creep.
Or maybe even a simpler version if that sounds like too much work.
It sounds like they're to deal with scenarios where channels that we read from close for some reason.
Well, continuing with the resampling actor as an use case, at some point it is very likely that we'll need to be able to unsubscribe (when we'll be able to switch peak-shaving on and off for example). Maybe is going too much into the future, and we'll still somehow keep all subscriptions forever, but if we do need to unsubscribe, we'll need to close receivers.
Is that accurate or are you thinking of other scenarios as well?
I haven't think very hardly in particular scenarios, but is just more like the vague memory of use cases I saw in the past, but thinking again I think handling subscriptions is a good example. Another scenario for stopping a receiver might be stopping a timer, and maybe you want to re-enable later. But also this more dynamic scenario when we start to compose actors and might need to handle new messages/channels when an actor is created and remove it when it is gone.
Would it help eliminate the two *helper layers?
Yes, exactly. I think with this all that complexity should be gone.
So we just make a basic interface without implementation for the features proposed in this issue, then see how it affects the resampler, just to make sure we limit feature creep.
Or maybe even a simpler version if that sounds like too much work.
Yes, I don't think is something to implement right now, I put it in v0.17.0 just because there isn't much else after select() is done (and thinking about how we could extend it with features that were in the air for a while). So I just had this idea, and thought it was pretty nice that select() wouldn't even have to be touched, so I wanted to write it down.
I agree that it would be good to test the interface in the field before going for it.
We'll evaluate what's the best construction to implement dynamic/optional receivers for select separately. Note that we should still return a full Receiver so merge() can also be used with select().
Another use case for this is actors that need to support processing dispatches conditionally and always need to use some other receiver (for example, for config updates). In this case it would be perfect if the dispatch updates receiver is optional, so we don't need to build 2 different select-loops based on if dispatch is enabled or not.
This actually makes me think that maybe in the future it would be best to implement optional receivers directly in select(), because it is actually an artifact we need only to use with select(), I don't see any other use case for it that is not select(), at least at the moment, at least until we allow for setting to None (or not) at runtime, in that case I can see that it could be used as some sort of generic pausing mechanism for receivers.
We now have experimental.OptionalReceiver, but it can't be set/reset, it is static once created. This actually doesn't make a lot of sense, for cases where we don't need to dynamically update the optional receiver to either set (change the inner receiver) or reset (set the inner receiver to None) it, it can make more sense to provide a NopReceiver, that behaves just the same as OptionalReceiver(None). In these cases we can always do:
Hahaha. Well, you didn't mention this, or I didn't understand it if you did. If it is the later, sorry, I guess I was still too attached to the idea of allowing to set/reset dynamically in my head.
And to be honest, I still don't get why are you against an OptionalReceiver that can be dynamically set/reset, but I will not fight it because I don't have a specific use case for it at the moment, maybe if at some point we need to allow actors to switch between dispatch and continuous mode without destroying the actor and recreating it, but I guess we won't want to go that route.
For the time being, I'm OK to deprecate OptionalReceiver and add a simpler NopReceiver.
Well, you didn't mention this, or I didn't understand it if you did.
I did mention that. I said the actor should only take a Receiver, and the infrastructure code should send a receiver if available, or create an Empty or Blank receiver.
I also mentioned the possibility of just making a channel and creating a receiver without any senders, if you're opposed to making a blank receiver. Broadcast receivers hold a strong reference to the channel, so just say Broadcast().new_receiver() and we get a free empty receiver.
If I had asked the question, "Do you understand my proposal clearly?" I think people would have said yes.
But it worked out in the end, so no complaints from me.
I still don't get why are you against an OptionalReceiver that can be dynamically set/reset
I don't know of a situation where we can't work around it more efficiently. The Receiver.new_receiver() needs to be able to replicate this, which is not impossible; it just needs more metadata. But it is still overhead. We can revisit when there are use cases, of course.
If I had asked the question, "Do you understand my proposal clearly?" I think people would have said yes.
Probably, I think my problem was that I was still thinking about an optional receiver that can be updated dynamically, once you remove that, it falls apart pretty clearly.
You don't. Dangling things do get cleaned up, sometimes many seconds later, but they still do. During that time, it is a problem if we get overflow logs. A channel to which no-one can send anything will just get quietly cleaned up when the interpreter decides.
But of course, that was just a suggestion in the worst case. Having a dedicated empty receiver is of course better.
👍
But just for clarification
You don't. Dangling things do get cleaned up, sometimes many seconds later, but they still do. During that time, it is a problem if we get overflow logs. A channel to which no-one can send anything will just get quietly cleaned up when the interpreter decides.
This might work for this particular case and now and but it is not guaranteed, and I'm not sure if there could still be any hidden memory leakage. The GC will clean up all the memory used by Python, but might not close files and other OS resources (or will not release a condition variable. I think assuming things will just work without proper cleanup is dangerous and we are already paying the price for doing so, so I think we should do a shift and use the approach that anything that could go wrong will eventually go wrong, taking a more defensive approach.
In c++ they are as automatic as in python, they just use RAII instead of using with explicitly. But yeah, the extra indentation in python is annoying. Maybe they should just have added a "storage class" like:
autof=open(...)
# stuff
That does the same as:
withopen(...) asf:
# stuff
Probably the main reason is in python you can't really define scopes, only functions define scopes, but still it is better to cleanup at the end of the function automatically to not cleaning up at all...
Activity
llucax commentedon Jun 9, 2023
Maybe I should have made this a discussion first. If anyone sees any controversy in this, we can convert it to a discussion. If there are no big objections, we can turn the TODO list into issues as we plan to address them.
shsms commentedon Jun 9, 2023
I guess I'm just having trouble imagining what kind of possibilities would come out of these features. It sounds like they're to deal with scenarios where channels that we read from close for some reason. Is that accurate or are you thinking of other scenarios as well?
Is the resampler the only known use-case for these features? I'm not very familiar with it, so maybe that would be just a good first test: it would be nice to see how they would simplify the resampler. Would it help eliminate the two *helper layers?
So we just make a basic interface without implementation for the features proposed in this issue, then see how it affects the resampler, just to make sure we limit feature creep.
Or maybe even a simpler version if that sounds like too much work.
llucax commentedon Jun 9, 2023
Well, continuing with the resampling actor as an use case, at some point it is very likely that we'll need to be able to unsubscribe (when we'll be able to switch peak-shaving on and off for example). Maybe is going too much into the future, and we'll still somehow keep all subscriptions forever, but if we do need to unsubscribe, we'll need to close receivers.
I haven't think very hardly in particular scenarios, but is just more like the vague memory of use cases I saw in the past, but thinking again I think handling subscriptions is a good example. Another scenario for stopping a receiver might be stopping a timer, and maybe you want to re-enable later. But also this more dynamic scenario when we start to compose actors and might need to handle new messages/channels when an actor is created and remove it when it is gone.
Yes, exactly. I think with this all that complexity should be gone.
Yes, I don't think is something to implement right now, I put it in v0.17.0 just because there isn't much else after
select()is done (and thinking about how we could extend it with features that were in the air for a while). So I just had this idea, and thought it was pretty nice thatselect()wouldn't even have to be touched, so I wanted to write it down.I agree that it would be good to test the interface in the field before going for it.
llucax commentedon Nov 9, 2023
Update after a new discussion we had around this:
MergeNamed(RemoveMergeNamed#236) and make a wrappermerge()function forMergeand makeMergeprivate (ExposeMergeas amerge()function #237), so updating the issue to reflect this.Receiversomerge()can also be used withselect().llucax commentedon Jan 8, 2025
Another use case for this is actors that need to support processing dispatches conditionally and always need to use some other receiver (for example, for config updates). In this case it would be perfect if the dispatch updates receiver is optional, so we don't need to build 2 different
select-loops based on if dispatch is enabled or not.4 remaining items
llucax commentedon Apr 15, 2025
From #409 (comment):
llucax commentedon Apr 24, 2025
We now have
experimental.OptionalReceiver, but it can't be set/reset, it is static once created. This actually doesn't make a lot of sense, for cases where we don't need to dynamically update the optional receiver to either set (change the inner receiver) or reset (set the inner receiver toNone) it, it can make more sense to provide aNopReceiver, that behaves just the same asOptionalReceiver(None). In these cases we can always do:shsms commentedon Apr 24, 2025
"What do you have against
OptionalReceiver". Bullies.😠llucax commentedon Apr 24, 2025
Hahaha. Well, you didn't mention this, or I didn't understand it if you did. If it is the later, sorry, I guess I was still too attached to the idea of allowing to set/reset dynamically in my head.
And to be honest, I still don't get why are you against an
OptionalReceiverthat can be dynamically set/reset, but I will not fight it because I don't have a specific use case for it at the moment, maybe if at some point we need to allow actors to switch between dispatch and continuous mode without destroying the actor and recreating it, but I guess we won't want to go that route.For the time being, I'm OK to deprecate
OptionalReceiverand add a simplerNopReceiver.shsms commentedon Apr 24, 2025
I did mention that. I said the actor should only take a
Receiver, and the infrastructure code should send a receiver if available, or create an Empty or Blank receiver.I also mentioned the possibility of just making a channel and creating a receiver without any senders, if you're opposed to making a blank receiver. Broadcast receivers hold a strong reference to the channel, so just say
Broadcast().new_receiver()and we get a free empty receiver.If I had asked the question, "Do you understand my proposal clearly?" I think people would have said yes.
But it worked out in the end, so no complaints from me.
shsms commentedon Apr 24, 2025
I need to get better at explaining things.
I don't know of a situation where we can't work around it more efficiently. The
Receiver.new_receiver()needs to be able to replicate this, which is not impossible; it just needs more metadata. But it is still overhead. We can revisit when there are use cases, of course.llucax commentedon Apr 25, 2025
Probably, I think my problem was that I was still thinking about an optional receiver that can be updated dynamically, once you remove that, it falls apart pretty clearly.
llucax commentedon Apr 25, 2025
This is wrong though. You really need to start thinking about cleanup. You need to do:
Which doesn't look as terse and convenient. 😆
shsms commentedon Apr 25, 2025
You don't. Dangling things do get cleaned up, sometimes many seconds later, but they still do. During that time, it is a problem if we get overflow logs. A channel to which no-one can send anything will just get quietly cleaned up when the interpreter decides.
shsms commentedon Apr 25, 2025
But of course, that was just a suggestion in the worst case. Having a dedicated empty receiver is of course better.
llucax commentedon Apr 28, 2025
👍
But just for clarification
This might work for this particular case and now and but it is not guaranteed, and I'm not sure if there could still be any hidden memory leakage. The GC will clean up all the memory used by Python, but might not close files and other OS resources (or will not release a condition variable. I think assuming things will just work without proper cleanup is dangerous and we are already paying the price for doing so, so I think we should do a shift and use the approach that anything that could go wrong will eventually go wrong, taking a more defensive approach.
shsms commentedon Apr 28, 2025
I agree. I was talking about just that particular case.
It is funny how we end up doing lifecycle management of objects, even though we're writing Python. And these are automatic in C++ and Rust.
llucax commentedon Apr 29, 2025
In c++ they are as automatic as in python, they just use RAII instead of using
withexplicitly. But yeah, the extra indentation in python is annoying. Maybe they should just have added a "storage class" like:That does the same as:
Probably the main reason is in python you can't really define scopes, only functions define scopes, but still it is better to cleanup at the end of the function automatically to not cleaning up at all...