Add the oneshot channel implementation#502
Add the oneshot channel implementation#502shsms wants to merge 3 commits intofrequenz-floss:v1.x.xfrom
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds a new oneshot channel implementation to the frequenz.channels library, along with a Sender.aclose() abstract method and SenderClosedError exception class. A oneshot channel allows sending exactly one message, after which the sender and receiver are automatically closed.
Changes:
- Introduces
make_oneshot()factory function and supporting internal_OneShot,_OneShotSender, and_OneShotReceiverclasses in a new_oneshot.pymodule. - Adds an abstract
aclose()method to theSenderABC and a newSenderClosedErrorexception, with implementations in all existingSendersubclasses (Anycast._Sender,Broadcast._Sender,RelaySender). - Adds tests for the oneshot channel and updates exports and release notes.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
src/frequenz/channels/_oneshot.py |
New file implementing the oneshot channel with sender, receiver, and factory function |
src/frequenz/channels/_sender.py |
Adds abstract aclose() method to Sender ABC and new SenderClosedError exception class |
src/frequenz/channels/__init__.py |
Exports make_oneshot and SenderClosedError |
src/frequenz/channels/_anycast.py |
Adds aclose() and SenderClosedError support to Anycast sender |
src/frequenz/channels/_broadcast.py |
Adds aclose() and SenderClosedError support to Broadcast sender |
src/frequenz/channels/experimental/_relay_sender.py |
Adds aclose() to RelaySender, removes redundant typing.Generic base class |
tests/test_oneshot.py |
New test file covering oneshot send/receive, close, and error scenarios |
RELEASE_NOTES.md |
Documents the new oneshot channel feature |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
3f0b85a to
d8d258f
Compare
Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
src/frequenz/channels/_oneshot.py
Outdated
| def __init__(self) -> None: | ||
| """Create a new one-shot channel.""" | ||
| self.message: ChannelMessageT | _Empty = _EMPTY | ||
| self.sent = False |
There was a problem hiding this comment.
Feels like self.sent could be a derived state of self.message instead? Something like:
@property
def sent(self):
return self.message is not _EMPTYThat would make the illegal state of these not being in agreement impossible by design.
There was a problem hiding this comment.
Or self._channel.event.is_set().
There was a problem hiding this comment.
Maybe we can get rid of the redundancy (and possibility of ending with inconsistent state) merging sent, event and message into a asyncio.Queue(maxsize=1).
There was a problem hiding this comment.
we also use sent to track if a channel was closed before sending anything. I guess I should change to use a different field for tracking closed, then.
There was a problem hiding this comment.
I didn't make any logic changes, just renamed the variable from sent to closed, and now I think it reads much better.
There was a problem hiding this comment.
It definitely reads much better with closed. I guess with this all 3 "states" are mostly independent (it can be closed at any time, and the event can be set and drained or not drained, I guess the only dependency is drained can't be true if the event is not set, but that's just a very small dependency).
src/frequenz/channels/_oneshot.py
Outdated
| def __init__(self) -> None: | ||
| """Create a new one-shot channel.""" | ||
| self.message: ChannelMessageT | _Empty = _EMPTY | ||
| self.sent = False |
There was a problem hiding this comment.
Or self._channel.event.is_set().
| from ._sender import Sender, SenderClosedError | ||
|
|
||
|
|
||
| def make_oneshot( |
There was a problem hiding this comment.
open_oneshot_channel()? 😬
I feel pretty strongly about this one. This is not a Trio-only thing, open is also used in asyncio, like open_connection().
I would also be up for open_channel() and take the type as an argument somehow, but I guess it will make the interface more complicated (it will need more type parameters) with very little gain.
There was a problem hiding this comment.
I'm actually inclined to switching back to channels.oneshot and channels.broadcast.
IMHO, open doesn't work for us at all. For files and networking, open is an idiomatic verb. In both those cases, there is a counterparty or a resource owner, and the idea is to open something to access the other side.
What we are doing is more like creating, because we are creating both sides and the link between them. So to me, Trio's naming is imprecise.
I'd take inspiration from os.pipe, which is so much closer to what we're doing.
There was a problem hiding this comment.
That would also match Rust, but I'm ok with make_oneshot, etc. as well, and it also somewhat similar to the Go syntax, where you have to call make for many things, including channels, a bit like the new keyword of C++.
There was a problem hiding this comment.
oneshot for me is not an option because it is not a verb. That leaves us with make or open. Let's say we forget about trio and asyncio, I don't understand why you want to introduce an asymmetry between make/close instead of going with open.
And you are opening a channel, the fact that you hide the channel object l, and you close it indirectly by closing all it's sender's or all it's receivers doesn't change the fact that you are opening a channel that needs to be closed.
And I don't get why you want to follow other programming languages naming conventions in python. I really think is not very relevant what rust or go does here.
I think os.pipe is not a good example because it follows very old POSIX naming conventions, not python ones.
I think open_oneshot_channel is the most precise, descriptive and pythonic option.
RELEASE_NOTES.md
Outdated
| <!-- Here goes the main new features and examples or instructions on how to use them --> | ||
| - There's a new `oneshot` channel, which can be created with the `make_oneshot` function, which returns a sender and a receiver. A single message can be sent using the sender, after which it will be closed. And the receiver will close as soon as the message is received. | ||
|
|
||
| - `Sender`s now have an `aclose`, which can be used to close them. |
There was a problem hiding this comment.
| - `Sender`s now have an `aclose`, which can be used to close them. | |
| - `Sender`s now have an `aclose`, which must be used to close them. |
😬
There was a problem hiding this comment.
... unless the sender was closed automatically by sending a message through it, I guess? So can would be the correct wording?
There was a problem hiding this comment.
I think we want users to close things that are not needed anymore, even though we will take some precaution to cleanup. Because python lacks deterministic or async destructors, cleanup must be done by the users.
I will update the note.
There was a problem hiding this comment.
Done, also rephrased a little.
Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
No description provided.