镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

Improve composability of receivers #116

Open

Description

@llucax

What's needed?

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 wrapper on the above that only accepts 0 or 1 receivers, to implement an optional receiver (Add OptionalReceiver for handling unset receivers #409)
  • 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.

Alternatives and workarounds

Break the select loop, use tasks manually.

Additional context

This would fix a few open issues / discussions:

Activity

  1. added
    type:enhancementNew feature or enhancement visitble to users
    part:coreAffects the core types (`Sender`, `Receiver`, exceptions, etc.)
    on Jun 9, 2023
  2. added this to the v0.17.0 milestone on Jun 9, 2023
  3. llucax commented on Jun 9, 2023

    @llucax
    ContributorAuthor

    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.

  4. shsms commented on Jun 9, 2023

    @shsms
    Contributor

    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.

  5. llucax commented on Jun 9, 2023

    @llucax
    ContributorAuthor

    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.

  6. modified the milestones: v0.17.0, v1.1.0 on Aug 15, 2023
  7. llucax commented on Nov 9, 2023

    @llucax
    ContributorAuthor

    Update after a new discussion we had around this:

    • We'll remove MergeNamed (Remove MergeNamed #236) and make a wrapper merge() function for Merge and make Merge private (Expose Merge as a merge() function #237), so updating the issue to reflect this.
    • 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().
  8. llucax commented on Jan 8, 2025

    @llucax
    ContributorAuthor

    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.

  9. 4 remaining items

  10. llucax commented on Apr 15, 2025

    @llucax
    ContributorAuthor

    From #409 (comment):

    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.

  11. llucax commented on Apr 24, 2025

    @llucax
    ContributorAuthor

    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:

    recv: Receiver[T]
    maybe_recv: Receiver[T] = NopReceiver[T]() if recv is None else recv
  12. shsms commented on Apr 24, 2025

    @shsms
    Contributor

    "What do you have against OptionalReceiver". Bullies.😠

  13. llucax commented on Apr 24, 2025

    @llucax
    ContributorAuthor

    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.

  14. shsms commented on Apr 24, 2025

    @shsms
    Contributor

    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.

  15. shsms commented on Apr 24, 2025

    @shsms
    Contributor

    I need to get better at explaining things.

    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.

  16. llucax commented on Apr 25, 2025

    @llucax
    ContributorAuthor

    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.

  17. llucax commented on Apr 25, 2025

    @llucax
    ContributorAuthor

    Broadcast receivers hold a strong reference to the channel, so just say Broadcast().new_receiver() and we get a free empty receiver.

    This is wrong though. You really need to start thinking about cleanup. You need to do:

    async with aclosing(Broadcast(name="NOP channel")) as chan:
        with closing(chan.new_receiver()) as recv:
            use_receiver(recv)

    Which doesn't look as terse and convenient. 😆

  18. shsms commented on Apr 25, 2025

    @shsms
    Contributor

    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.

  19. shsms commented on Apr 25, 2025

    @shsms
    Contributor

    But of course, that was just a suggestion in the worst case. Having a dedicated empty receiver is of course better.

  20. llucax commented on Apr 28, 2025

    @llucax
    ContributorAuthor

    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.

  21. shsms commented on Apr 28, 2025

    @shsms
    Contributor

    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.

  22. llucax commented on Apr 29, 2025

    @llucax
    ContributorAuthor

    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:

    auto f = open(...)
    # stuff

    That does the same as:

    with open(...) as f:
        # 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...

  23. modified the milestones: v1.8.0, Untriaged on Jun 25, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    part:coreAffects the core types (`Sender`, `Receiver`, exceptions, etc.)priority:highAddress this as soon as possibletype:enhancementNew feature or enhancement visitble to users

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions