-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Add open_timeout
as an overall timeout option for Socket.tcp
#13368
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
Conversation
Would it be better to raise an |
@deivid-rodriguez |
❌ Tests Failed✖️no tests failed ✔️61985 tests passed(1 flake) |
[Background] Currently, `TCPSocket.new` and `Socket.tcp` accept two kind of timeout options: - `resolv_timeout`, which controls the timeout for DNS resolution - `connect_timeout`, which controls the timeout for the connection attempt With the introduction of Happy Eyeballs Version 2 (as per [RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305)) in[ Feature #20108](https://bugs.ruby-lang.org/issues/20108) and [Feature #20782](https://bugs.ruby-lang.org/issues/20782), both address resolution and connection attempts are now parallelized. As a result, the sum of `resolv_timeout` and `connect_timeout` no longer represents the total timeout duration. This is because, in HEv2, name resolution and connection attempts are performed concurrently, causing the two timeouts to overlap. Example: When `resolv_timeout: 200ms` and `connect_timeout: 100ms` are set: 1. An IPv6 address is resolved after the method starts immediately (IPv4 is still being resolved). 2. A connection attempt is initiated to the IPv6 address 3. After 100ms, `connect_timeout` is exceeded. However, since `resolv_timeout` still has 100ms left, the IPv4 resolution continues. 4. After 200ms from the start, the method raises a `resolv_timeout` error. In this case, the total elapsed time before a timeout is 200ms, not the expected 300ms (100ms + 200ms). Furthermore, in HEv2, connection attempts are also parallelized. It starts a new connection attempts every 250ms for resolved addresses. This makes the definition of `connect_timeout` even more ambiguous—specifically, it becomes unclear from which point the timeout is counted. Additionally, these methods initiate new connection attempts every 250ms (Connection Attempt Delay) for each candidate address, thereby parallelizing connection attempts. However, this behavior makes it unclear from which point in time the connect_timeout is actually measured. Currently, a `connect_timeout` is raised only after the last connection attempt exceeds the timeout. Example: When `connect_timeout: 100ms` is set and 3 address candidates: 1. Start a connection attempt to the address `a` 2. 250ms after step 1, start a new connection attempt to the address `b` 3. 500ms after step 1, start a new connection attempt to the address `c` 4. 1000ms after step 3 (1000ms after starting the connection to `c`, 1250ms after starting the connection to `b,` and 1500ms after starting the connection to `a`) `connect_timeout` is raised This behavior aims to favor successful connections by allowing more time for each attempt, but it results in a timeout model that is difficult to reason about. These methods have supported `resolv_timeout` and `connect_timeout` options even before the introduction of HEv2. However, in many use cases, it would be more convenient if a timeout occurred after a specified duration from the start of the method. Similar functions in other languages (such as PHP, Python, and Go) typically allow specifying only an overall timeout. [Proposal] I propose adding an `open_timeout` option to `Socket.tcp` in this PR, which triggers a timeout after a specified duration has elapsed from the start of the method. The name `open_timeout` aligns with the existing accessor used in `Net::HTTP`. If `open_timeout` is specified together with `resolv_timeout` and `connect_timeout`, I propose that only `open_timeout` be used and the others be ignored. While it is possible to support combinations of `open_timeout`, `resolv_timeout`, and `connect_timeout`, doing so would require defining which timeout takes precedence in which situations. In this case, I believe it is more valuable to keep the behavior simple and easy to understand, rather than supporting more complex use cases. If this proposal is accepted, I also plan to extend `open_timeout` support to `TCPSocket.new`. While the long-term future of `resolv_timeout` and `connect_timeout` may warrant further discussion, I believe the immediate priority is to offer a straightforward way to specify an overall timeout. [Outcome] If `open_timeout` is also supported by `TCPSocket.new`, users would be able to manage total connection timeouts directly in `Net::HTTP#connect` without relying on `Timeout.timeout`. https://github.com/ruby/ruby/blob/aa0f689bf45352c4a592e7f1a044912c40435266/lib/net/http.rb#L1657
…ions > If open_timeout is specified together with resolv_timeout and connect_timeout, I propose that only open_timeout be used and the others be ignored. Since this approach may be unclear to users, I’ve decided to explicitly raise an `ArgumentError` if these options are specified together.
…esses that have not been tried
…y#13368) * Add `open_timeout` as an overall timeout option for `Socket.tcp` [Background] Currently, `TCPSocket.new` and `Socket.tcp` accept two kind of timeout options: - `resolv_timeout`, which controls the timeout for DNS resolution - `connect_timeout`, which controls the timeout for the connection attempt With the introduction of Happy Eyeballs Version 2 (as per [RFC 8305](https://datatracker.ietf.org/doc/html/rfc8305)) in[ Feature #20108](https://bugs.ruby-lang.org/issues/20108) and [Feature #20782](https://bugs.ruby-lang.org/issues/20782), both address resolution and connection attempts are now parallelized. As a result, the sum of `resolv_timeout` and `connect_timeout` no longer represents the total timeout duration. This is because, in HEv2, name resolution and connection attempts are performed concurrently, causing the two timeouts to overlap. Example: When `resolv_timeout: 200ms` and `connect_timeout: 100ms` are set: 1. An IPv6 address is resolved after the method starts immediately (IPv4 is still being resolved). 2. A connection attempt is initiated to the IPv6 address 3. After 100ms, `connect_timeout` is exceeded. However, since `resolv_timeout` still has 100ms left, the IPv4 resolution continues. 4. After 200ms from the start, the method raises a `resolv_timeout` error. In this case, the total elapsed time before a timeout is 200ms, not the expected 300ms (100ms + 200ms). Furthermore, in HEv2, connection attempts are also parallelized. It starts a new connection attempts every 250ms for resolved addresses. This makes the definition of `connect_timeout` even more ambiguous—specifically, it becomes unclear from which point the timeout is counted. Additionally, these methods initiate new connection attempts every 250ms (Connection Attempt Delay) for each candidate address, thereby parallelizing connection attempts. However, this behavior makes it unclear from which point in time the connect_timeout is actually measured. Currently, a `connect_timeout` is raised only after the last connection attempt exceeds the timeout. Example: When `connect_timeout: 100ms` is set and 3 address candidates: 1. Start a connection attempt to the address `a` 2. 250ms after step 1, start a new connection attempt to the address `b` 3. 500ms after step 1, start a new connection attempt to the address `c` 4. 1000ms after step 3 (1000ms after starting the connection to `c`, 1250ms after starting the connection to `b,` and 1500ms after starting the connection to `a`) `connect_timeout` is raised This behavior aims to favor successful connections by allowing more time for each attempt, but it results in a timeout model that is difficult to reason about. These methods have supported `resolv_timeout` and `connect_timeout` options even before the introduction of HEv2. However, in many use cases, it would be more convenient if a timeout occurred after a specified duration from the start of the method. Similar functions in other languages (such as PHP, Python, and Go) typically allow specifying only an overall timeout. [Proposal] I propose adding an `open_timeout` option to `Socket.tcp` in this PR, which triggers a timeout after a specified duration has elapsed from the start of the method. The name `open_timeout` aligns with the existing accessor used in `Net::HTTP`. If `open_timeout` is specified together with `resolv_timeout` and `connect_timeout`, I propose that only `open_timeout` be used and the others be ignored. While it is possible to support combinations of `open_timeout`, `resolv_timeout`, and `connect_timeout`, doing so would require defining which timeout takes precedence in which situations. In this case, I believe it is more valuable to keep the behavior simple and easy to understand, rather than supporting more complex use cases. If this proposal is accepted, I also plan to extend `open_timeout` support to `TCPSocket.new`. While the long-term future of `resolv_timeout` and `connect_timeout` may warrant further discussion, I believe the immediate priority is to offer a straightforward way to specify an overall timeout. [Outcome] If `open_timeout` is also supported by `TCPSocket.new`, users would be able to manage total connection timeouts directly in `Net::HTTP#connect` without relying on `Timeout.timeout`. https://github.com/ruby/ruby/blob/aa0f689bf45352c4a592e7f1a044912c40435266/lib/net/http.rb#L1657 --- * Raise an exception if it is specified together with other timeout options > If open_timeout is specified together with resolv_timeout and connect_timeout, I propose that only open_timeout be used and the others be ignored. Since this approach may be unclear to users, I’ve decided to explicitly raise an `ArgumentError` if these options are specified together. * Add doc * Fix: open_timeout error should be raised even if there are still addresses that have not been tried
With this change, `TCPSocket.new` now accepts the `open_timeout` option. This option raises an exception if the specified number of seconds has elapsed since the start of the method call, even if the operation is still in the middle of name resolution or connection attempts. The addition of this option follows the same intent as the previously merged change to `Socket.tcp` ([Feature #20783]). ruby#13368
…`TCPSocket.new` With this change, `TCPSocket.new` now accepts the `open_timeout` option. This option raises an exception if the specified number of seconds has elapsed since the start of the method call, even if the operation is still in the middle of name resolution or connection attempts. The addition of this option follows the same intent as the previously merged change to `Socket.tcp`. [Feature #21347](https://bugs.ruby-lang.org/issues/21347) ruby#13368
…`TCPSocket.new` (#13909) * [Feature #21347] Add `open_timeout` as an overall timeout option for `TCPSocket.new` With this change, `TCPSocket.new` now accepts the `open_timeout` option. This option raises an exception if the specified number of seconds has elapsed since the start of the method call, even if the operation is still in the middle of name resolution or connection attempts. The addition of this option follows the same intent as the previously merged change to `Socket.tcp`. [Feature #21347](https://bugs.ruby-lang.org/issues/21347) #13368 * Tidy up: Extract rsock_raise_user_specified_timeout() * Added a note to the documentation of `Socket.tcp` * Fix `rsock_init_inetsock` for `FAST_FALLBACK_INIT_INETSOCK_IMPL`
Currently,
TCPSocket.new
andSocket.tcp
accept two kind of timeout options:resolv_timeout
, which controls the timeout for DNS resolutionconnect_timeout
, which controls the timeout for the connection attemptWith the introduction of Happy Eyeballs Version 2 (as per RFC 8305) in Feature #20108 and Feature #20782, both address resolution and connection attempts are now parallelized. As a result, the sum of
resolv_timeout
andconnect_timeout
no longer represents the total timeout duration. This is because, in HEv2, name resolution and connection attempts are performed concurrently, causing the two timeouts to overlap.Example:
When
resolv_timeout: 200ms
andconnect_timeout: 100ms
are set:connect_timeout
is exceeded. However, sinceresolv_timeout
still has 100ms left, the IPv4 resolution continues.resolv_timeout
error.In this case, the total elapsed time before a timeout is 200ms, not the expected 300ms (100ms + 200ms).
Furthermore, in HEv2, connection attempts are also parallelized. It starts a new connection attempts every 250ms for resolved addresses. This makes the definition of
connect_timeout
even more ambiguous—specifically, it becomes unclear from which point the timeout is counted.Additionally, these methods initiate new connection attempts every 250ms (Connection Attempt Delay) for each candidate address, thereby parallelizing connection attempts. However, this behavior makes it unclear from which point in time the connect_timeout is actually measured. Currently, a
connect_timeout
is raised only after the last connection attempt exceeds the timeout.Example:
When
connect_timeout: 100ms
is set and 3 address candidates:a
b
c
c
, 1250ms after starting the connection tob,
and 1500ms after starting the connection toa
)connect_timeout
is raisedThis behavior aims to favor successful connections by allowing more time for each attempt, but it results in a timeout model that is difficult to reason about.
These methods have supported
resolv_timeout
andconnect_timeout
options even before the introduction of HEv2. However, in many use cases, it would be more convenient if a timeout occurred after a specified duration from the start of the method. Similar functions in other languages (such as PHP, Python, and Go) typically allow specifying only an overall timeout.I propose adding an
open_timeout
option toSocket.tcp
in this PR, which triggers a timeout after a specified duration has elapsed from the start of the method.The name
open_timeout
aligns with the existing accessor used inNet::HTTP
. Ifopen_timeout
is specified together withresolv_timeout
andconnect_timeout
, I propose that onlyopen_timeout
be used and the others be ignored. While it is possible to support combinations ofopen_timeout
,resolv_timeout
, andconnect_timeout
, doing so would require defining which timeout takes precedence in which situations. In this case, I believe it is more valuable to keep the behavior simple and easy to understand, rather than supporting more complex use cases.If this proposal is accepted, I also plan to extend
open_timeout
support toTCPSocket.new
.While the long-term future of
resolv_timeout
andconnect_timeout
may warrant further discussion, I believe the immediate priority is to offer a straightforward way to specify an overall timeout.If
open_timeout
is also supported byTCPSocket.new
, users would be able to manage total connection timeouts directly inNet::HTTP#connect
without relying onTimeout.timeout
.ruby/lib/net/http.rb
Line 1657 in aa0f689