Description
Channels (#586, #497) are a pretty complicated design problem, and fairly central to user experience, so while I think our first cut is pretty decent, we'll likely want to fine tune it as we get experience using them.
Here are my initial notes (basically questions that I decided to defer while implementing version 1):
In open_memory_channel
, should we make max_buffer_size=0
default? Is this really a good choice across a broad range of settings? The docs recommend it right now, but that's not really based on lots of practical experience or anything. And it's way easier to add defaults later than it is to remove or change them.
For channels that allow buffering, it's theoretically possible for values to get "lost" without there ever being an exception (send
puts it in the buffer, then receiver never takes it out). This even happens if both sides are being careful, and closing their endpoint objects when they're done with them. We could potentially make it so that ReceiveChannel.aclose
raises an exception (I guess BrokenResourceError
) if the channel wasn't already cleanly closed by the sender. This would be inconsistent with the precedent set by ReceiveStream
(which inherited it from BSD sockets), but OTOH, ReceiveStream
is a much lower-level API – to use a ReceiveStream
you need some non-trivial engineering work to implement a protocol on top, while ReceiveChannel
is supposed to be a simple high-level protocol that's usable out-of-the-box. Practically speaking, though, it would be pretty annoying if some code inside a consumer crashes, and then ReceiveChannel.__aexit__
throws away the actual exception and replaces it with BrokenChannelError
.
Should memory channels have a sync close()
method? They could easily, they only reason not to is that it makes life simpler for hypothetical IPC channels. I have no idea yet how common those will be, or whether it's actually important for in-process channels and cross-process channels to have compatible APIs.
I have mixed feelings about open_memory_stream
returning a bare tuple (send_channel, receive_channel)
. It's simple, and allows for unpacking. But, it requires everyone to memorize which order they go in, which is annoying, and it forces you to juggle two objects, as compared to the traditional Queue
design that only has one object. I'm convinced that having two objects is important for closure-tracking and IPC, but there is an option that's a kind of hybrid: it could return a ChannelPair
object, that's a simple container for a .send_channel
and .receive_channel
object, and, if we want, the ChannelPair
could have send
and receive
methods that simply delegate to its constituent objects. Then people who hate having two objects could treat ChannelPair
like a Queue
, while still breaking it into pieces if desired, or doing closure tracking like async with channel_pair.send_channel: ...
. But... I played with this a bit, and found it annoying to type out channel_pair.send_channel
all the time. In particular, if you do want to split the objects up, then you lose the option of deconstructing-assignment, so you have to do cumbersome attribute access instead. (Or we could also support deconstructing-assignemtn by implementing __iter__
, but that's yet another confusing way then...) For now my intuition is that closure tracking is so broadly useful that everyone will want to use it, and that's easiest with destructuring assignment. But experience might easily prove that intuition wrong.
Is clone
confusing? Is there anything we can do to make it less confusing? Maybe a better name? So far people have flagged this multiple times, but we didn't have docs yet, so it could just be that...
And btw, the Channel
vs Stream
distinction is much less obvious than I'd like. No-one would look at those and immediately guess that ah, a Channel
carries objects while a Stream
carries bytes. It is learnable, and has some weak justification, and I haven't though of anything better yet, but if someone thinks of something brilliant please speak up.
Should the *_nowait
methods be in the ABC, or just in the memory implementation? They don't make a lot of sense for things like inter-process channels, or like, say... websockets. (A websocket can implement the SendChannel
and ReceiveChannel
interfaces, just with the proviso that objects being sent have to be type str
or bytes
.) The risk of harm is not large, you can always implement these as unconditional raise WouldBlock
, but maybe it will turn out to be more ergonomic to move them out of the ABC.
We should probably have a trio.testing.check_channel
, like we do for streams.