-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Support timeout in Channel.receive_first and Channel.send_first
#16094
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
197c805
3aeba59
32b691e
2d3f862
44ba3bd
dafda82
846bb58
d3d0025
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -30,12 +30,18 @@ class Channel(T) | |||||
| # :nodoc: | ||||||
| record UseDefault | ||||||
|
|
||||||
| class ClosedError < Exception | ||||||
| class Error < Exception | ||||||
| end | ||||||
|
|
||||||
| class ClosedError < Error | ||||||
| def initialize(msg = "Channel is closed") | ||||||
| super(msg) | ||||||
| end | ||||||
| end | ||||||
|
|
||||||
| class TimeoutError < Error | ||||||
| end | ||||||
|
|
||||||
| private module SenderReceiverCloseAction | ||||||
| def close | ||||||
| self.state = DeliveryState::Closed | ||||||
|
|
@@ -294,21 +300,67 @@ class Channel(T) | |||||
| pp.text inspect | ||||||
| end | ||||||
|
|
||||||
| def self.receive_first(*channels) | ||||||
| receive_first channels | ||||||
| # Returns the first available value received from the given *channels*, or | ||||||
| # raises `Channel::TimeoutError` if given a *timeout* that expires before a | ||||||
| # value is received. | ||||||
| # | ||||||
| # ``` | ||||||
| # c1 = Channel(String).new(1) | ||||||
| # c2 = Channel(String).new(1) | ||||||
| # | ||||||
| # c2.send "hello" | ||||||
| # value = Channel.receive_first c1, c2 # => receives "hello" from c2 | ||||||
| # | ||||||
| # begin | ||||||
| # # will timeout after 1 second and raise Channel::TimeoutError because | ||||||
| # # no channels are ready to receive | ||||||
| # value = Channel.receive_first c1, c2, timeout: 1.second | ||||||
| # rescue ex : Channel::TimeoutError | ||||||
| # Log.error(exception: ex) | ||||||
| # end | ||||||
| # ``` | ||||||
| def self.receive_first(*channels, timeout : Time::Span? = nil) | ||||||
| receive_first channels, timeout: timeout | ||||||
| end | ||||||
|
|
||||||
| def self.receive_first(channels : Enumerable(Channel)) | ||||||
| _, value = self.select(channels.map(&.receive_select_action)) | ||||||
| # :ditto: | ||||||
| def self.receive_first(channels : Enumerable(Channel), *, timeout : Time::Span? = nil) | ||||||
| actions = channels.map(&.receive_select_action) | ||||||
| actions = actions.to_a + [TimeoutAction.new(timeout)] unless timeout.nil? | ||||||
|
||||||
| actions = actions.to_a + [TimeoutAction.new(timeout)] unless timeout.nil? | |
| actions = actions.to_a << TimeoutAction.new(timeout) unless timeout.nil? |
ditto for .send_first
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I try actions = actions.to_a << TimeoutAction.new(timeout) unless timeout.nil? the compiler gives this error:
In src\channel.cr:330:45
330 | actions = actions.to_a << TimeoutAction.new(timeout) unless timeout.nil?
^--
Error: expected argument #1 to 'Array(Channel::StrictReceiveAction(Int32))#<<' to be Channel::StrictReceiveAction(Int32), not Channel::TimeoutAction
Overloads are:
- Array(T)#<<(value : T)
So I ended up with the actions = actions.to_a + [TimeoutAction.new(timeout)] approach as it was the only way I could figure out how to make the compiler happy with the types.
I would love if there was a better approach than concatenating these two temporary arrays to make the compiler happy?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of array concatenation, I tried splatting method arguments into the call to self.select and that works and seems like a better approach? And I assume splatting method arguments avoids heap allocation, but @straight-shoota please confirm?
index, value = self.select(*actions, TimeoutAction.new(timeout))There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Splatting only works for Tuple. So this code wouldn't compile if actions was, for example, Array or StaticArray.
Seems like we're missing a test case for that. 🙈
We can use splatting as an optimization for Tuple, but we would need other branches for other enumerables as well, just like in .select_impl.
For the array append to work, we must type the array as Array(SelectAction) (which is fine because .select expects that item type). An easy way to do that is to explicitly cast the output type of the map block: channels.map(&.receive_select_action.as(SelectAction)). This should result in an appropriate collection type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've added some additional tests to cover Array and StaticArray, and yep as you said compilation failed:
In src\channel.cr:360:34
360 | index, value = self.select(*actions, TimeoutAction.new(timeout))
^
Error: argument to splat must be a tuple, not Array(Channel::StrictReceiveAction(Int32))
I'm sorry (for being such a noob) but I cannot for the life of me figure out how to make the compiler happy trying to use << to append a TimeoutAction. I tried the following:
In src\channel.cr:364:41
364 | actions = actions.to_a.as(Array(SelectAction)) << timeout_action
^-----------
Error: can't use Channel::SelectAction(S) as generic type argument yet, use a more specific type
...
In src\channel.cr:364:55
364 | actions = actions.to_a(&.as(SelectAction)) << timeout_action
^-------------
Error: expected argument #1 to 'Array(Channel::StrictReceiveAction(Int32))#<<' to be Channel::StrictReceiveAction(Int32), not Channel::TimeoutAction
...
In src\channel.cr:365:59
365 | actions = actions.to_a.map(&.as(SelectAction)) << timeout_action
^-------------
Error: expected argument #1 to 'Array(Channel::StrictReceiveAction(Int32))#<<' to be Channel::StrictReceiveAction(Int32), not Channel::TimeoutAction
But have ended up back at the actions.to_a + [TimeoutAction.new(timeout)] as the only way I could get it to compile.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
thought: I'm wondering if a dedicated
Channel::TimeoutErrormakes much sense. Perhaps a generic timeout error would be better? It could be reused for other kinds of timeouts. I suppose it usually shouldn't matter much whether a timeout occurred in a channel or somewhere else...There's already
IO::Timeout, but it probably makes sense to be a separate type because it should be in theIO::Errorhierarchy. I don't thinkChannel::Erroris equivalently relevant.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I started out just using
IO::TimeoutErrorbut I convinced myself it wasn't appropriate to use an IO related error for a Channel timeout.If adding a
Channel::Errorsuperclass to the existingChannel::ClosedErrorand newChannel::TimeoutErrorhas little utility, I'll go ahead and remove it?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've removed
Channel::Error, but leftChannel::TimeoutErrorfor now pending whether or not there should be a generic and reusableTimeoutErrorin the stdlib.