-
Notifications
You must be signed in to change notification settings - Fork 322
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
Stricter timeout options parsing #754
base: main
Are you sure you want to change the base?
Conversation
lib/http/chainable.rb
Outdated
@@ -90,21 +90,18 @@ def build_request(*args) | |||
# @overload timeout(global_timeout) | |||
# Adds a global timeout to the full request | |||
# @param [Numeric] global_timeout | |||
PER_OPERATION_KEYS = Set.new(%i[read write connect]) |
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.
This looks unused :)
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.
moved in into lib/http/timeout/per_operation.rb but didn't remove it from here
What are we waiting for? |
@stoivo the tests are red, for one thing |
Pushed with no changed and now spec are green |
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.
After taking a closer look, I think we should do parsing a bit differently:
KEYS = %i[read write connect].to_h { |k| [k, :"#{k}_timeout"] }.freeze
class << self
def normalize_options(options)
normalized = {}
original = options.dup
KEYS.each do |short, long|
if original.key?(short) && original.key(long)
raise ArgumentError, "can't pass both #{short} and #{long}"
end
normalized[long] = original.key?(long) ? original.delete(long) : original.delete(short)
raise ArgumentError, "#{long} must be numeric", unless normalized[long].is_a?(Numeric)
end
if original.size.positive?
raise ArgumentError, "unknown timeout options: #{original.keys.join(', ')}"
end
if normalized.empty?
raise ArgumentError, "no timeout options given"
end
normalized
end
end
lib/http/timeout/per_operation.rb
Outdated
next unless options.key? k | ||
|
||
if options.key?("#{k}_timeout".to_sym) | ||
raise ArgumentError, "can't pass both #{k} and #{"#{k}_timeout".to_sym}" |
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.
raise ArgumentError, "can't pass both #{k} and #{"#{k}_timeout".to_sym}" | |
raise ArgumentError, "can't pass both #{k} and #{k}_timeout" |
lib/http/timeout/per_operation.rb
Outdated
|
||
class << self | ||
def parse_options(options) | ||
options = options.dup.then { |opts| expand_names(opts) } |
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.
options = options.dup.then { |opts| expand_names(opts) } | |
options = expand_names(options.dup) |
lib/http/timeout/per_operation.rb
Outdated
options.each do |key, value| | ||
unless SETTINGS.member?(key) && value.is_a?(Numeric) | ||
raise ArgumentError, "invalid option #{key.inspect}, must be numeric " \ | ||
"`.timeout(connect: x, write: y, read: z)`." | ||
end | ||
end | ||
|
||
raise ArgumentError, "at least one option" if options.empty? | ||
|
||
options |
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 believe we also should fail if given options are mistyped or totally wrong ones
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.
this is covered by
options.each do |key, value|
unless SETTINGS.member?(key) && value.is_a?(Numeric)
raise ArgumentError, "invalid option #{key.inspect}, must be numeric " \
"`.timeout(connect: x, write: y, read: z)`."
end
end
Very nice suggestion. So much more strait forward. It looks like it sneaked in a
You should be able to only set one of the actions callbacks like |
I like how it looks but rubocop this it is too high complexity. I think it's nice
|
@ixti ping |
oh. sorry, totally forgot. I'm afk till Monday, will merge as soon as get back |
I've added a commit that disables inline some cops for that method, but I believe I found a regression. Right now it's possible to set global timeout with: HTTP.timeout({ global: 161 }) And now that I'm looking at all of this again, I think next major we should add breaking change and:
@tarcieri WDYT? |
Sounds fine to me although it would be a good time to consider potential other breaking changes too |
I was comparing this HTTP client to others in Ruby and one thing I would like to add for timeouts is that global and per-operation are not mutually exclusive. I may want a request to time out after 1 minute, but spend no more than 150ms on the TCP connection portion or spend no more than 10 seconds idle on a read or write operation. An alternative interpretation is what HTTPX does, which is to treat read as a timeout reading the response, write as a timeout writing the request and global as a overall timeout. In my experience, except for streaming, HTTP clients care about total request timeout and maybe connect timeout, but definitely not per-operation timeouts. Streaming typically cares about operation timeouts and only optionally cares about a total timeout. Often clients measure time to first byte from the server as well as overall latency, so timeouts on both of those could be helpful. I am happy to help implement this type of behavior for 6.X if that is the target version. |
@misalcedo I opened #773 to track a potential redesign of the timeout subsystem for |
Move option transformation into then block since we want to extent it.
Rubocop told me the method started to be too complex. lib/http/chainable.rb:94:5: C: Metrics/PerceivedComplexity: Perceived complexity for timeout is too high. [10/8] def timeout(options) ... ^^^^^^^^^^^^^^^^^^^^ 80 files inspected, 1 offense detected
rubocop offences be rubocop -a 15:37:07 lib/http/timeout/per_operation.rb:17:9: C: Metrics/AbcSize: Assignment Branch Condition size for normalize_options is too high. [<5, 23, 9> 25.2/17] def normalize_options(options) ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lib/http/timeout/per_operation.rb:17:9: C: Metrics/CyclomaticComplexity: Cyclomatic complexity for normalize_options is too high. [10/7] def normalize_options(options) ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lib/http/timeout/per_operation.rb:17:9: C: Metrics/MethodLength: Method has too many lines. [11/10] def normalize_options(options) ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ lib/http/timeout/per_operation.rb:17:9: C: Metrics/PerceivedComplexity: Perceived complexity for normalize_options is too high. [10/8] def normalize_options(options) ... ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 80/80 files |================================================================================================================ 100 =================================================================================================================>| Time: 00:00:00 80 files inspected, 4 offenses detected
@stoivo any chance you could rebase this and get the tests green? We'd like to get it merged before a v6.0.0 release |
I will try to take a look at this this week |
I am sorry. I was motivated to fix this when I opened the MR. I have since moved on to use httpx and faraday. I don't have the extra time to look into this. |
fix #752