Skip to content

Add the oneshot channel implementation#502

Open
shsms wants to merge 3 commits intofrequenz-floss:v1.x.xfrom
shsms:oneshot
Open

Add the oneshot channel implementation#502
shsms wants to merge 3 commits intofrequenz-floss:v1.x.xfrom
shsms:oneshot

Conversation

@shsms
Copy link
Contributor

@shsms shsms commented Mar 5, 2026

No description provided.

Copilot AI review requested due to automatic review settings March 5, 2026 14:44
@shsms shsms requested a review from a team as a code owner March 5, 2026 14:44
@shsms shsms requested review from ela-kotulska-frequenz and removed request for a team March 5, 2026 14:44
@github-actions github-actions bot added part:docs Affects the documentation part:tests Affects the unit, integration and performance (benchmarks) tests part:channels Affects channels implementation part:core Affects the core types (`Sender`, `Receiver`, exceptions, etc.) part:experimental Affects the experimental package labels Mar 5, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 _OneShotReceiver classes in a new _oneshot.py module.
  • Adds an abstract aclose() method to the Sender ABC and a new SenderClosedError exception, with implementations in all existing Sender subclasses (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.

@shsms shsms force-pushed the oneshot branch 4 times, most recently from 3f0b85a to d8d258f Compare March 5, 2026 15:06
Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
def __init__(self) -> None:
"""Create a new one-shot channel."""
self.message: ChannelMessageT | _Empty = _EMPTY
self.sent = False
Copy link

@simonvoelcker simonvoelcker Mar 5, 2026

Choose a reason for hiding this comment

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

Feels like self.sent could be a derived state of self.message instead? Something like:

@property
def sent(self):
  return self.message is not _EMPTY

That would make the illegal state of these not being in agreement impossible by design.

Copy link
Contributor

Choose a reason for hiding this comment

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

Or self._channel.event.is_set().

Copy link
Contributor

@llucax llucax Mar 5, 2026

Choose a reason for hiding this comment

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

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I didn't make any logic changes, just renamed the variable from sent to closed, and now I think it reads much better.

Copy link
Contributor

Choose a reason for hiding this comment

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

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).

def __init__(self) -> None:
"""Create a new one-shot channel."""
self.message: ChannelMessageT | _Empty = _EMPTY
self.sent = False
Copy link
Contributor

Choose a reason for hiding this comment

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

Or self._channel.event.is_set().

from ._sender import Sender, SenderClosedError


def make_oneshot(
Copy link
Contributor

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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++.

Copy link
Contributor

Choose a reason for hiding this comment

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

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
- `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.

😬

Choose a reason for hiding this comment

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

... unless the sender was closed automatically by sending a message through it, I guess? So can would be the correct wording?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done, also rephrased a little.

simonvoelcker
simonvoelcker previously approved these changes Mar 5, 2026
shsms added 2 commits March 6, 2026 11:11
Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
Signed-off-by: Sahas Subramanian <sahas.subramanian@proton.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

part:channels Affects channels implementation part:core Affects the core types (`Sender`, `Receiver`, exceptions, etc.) part:docs Affects the documentation part:experimental Affects the experimental package part:tests Affects the unit, integration and performance (benchmarks) tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants