Hello @ioquatix, thank you once again for your dedicated work!
I have trouble understanding the behaviour of Timeout.timeout block when run inside async reactor. The test scenario uses an HTTP call with delayed response but in the real world it could be a call to some LLM which is not unusual to take several seconds.
This works as expected (no timeout set)
# test.rb
require 'async'
require 'net/http'
start = Time.now
Sync do
Net::HTTP.get(URI 'https://httpbin.org/delay/5')
end
puts "Duration: #{Time.now - start}s"
# => Duration: 5.797459s
This is odd
No timeout error is raised and the total execution time seems to be HTTP call (~5.5s) + timeout (2s)
# test.rb
require 'async'
require 'net/http'
start = Time.now
Sync do
Timeout.timeout 2 do
Net::HTTP.get(URI 'https://httpbin.org/delay/5')
puts "...request finished, no timeout though; in #{Time.now - start}s"
end
end
puts "Duration: #{Time.now - start}s"
# => ...request finished, no timeout though; in 7.7007s
# Duration: 7.700962s
This works but why?
# test.rb
require 'async'
require 'net/http'
start = Time.now
Sync do
Timeout.timeout 2, Async::TimeoutError do
Net::HTTP.get(URI 'https://httpbin.org/delay/5')
end
end
puts "Duration: #{Time.now - start}s"
with the expected result:
/Users/leuser/.rvm/gems/ruby-3.4.2/gems/async-2.36.0/lib/async/scheduler.rb:324:in 'IO::Event::Selector::KQueue#io_wait': execution expired (Async::TimeoutError)
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/async-2.36.0/lib/async/scheduler.rb:324:in 'Async::Scheduler#io_wait'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-protocol-0.2.2/lib/net/protocol.rb:229:in 'IO#wait_readable'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-protocol-0.2.2/lib/net/protocol.rb:229:in 'Net::BufferedIO#rbuf_fill'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-protocol-0.2.2/lib/net/protocol.rb:199:in 'Net::BufferedIO#readuntil'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-protocol-0.2.2/lib/net/protocol.rb:209:in 'Net::BufferedIO#readline'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http/response.rb:159:in 'Net::HTTPResponse.read_status_line'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http/response.rb:147:in 'Net::HTTPResponse.read_new'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:2448:in 'block in Net::HTTP#transport_request'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:2439:in 'Kernel#catch'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:2439:in 'Net::HTTP#transport_request'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:2410:in 'Net::HTTP#request'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:2281:in 'Net::HTTP#request_get'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:826:in 'block in Net::HTTP.get_response'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:1630:in 'Net::HTTP#start'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:1064:in 'Net::HTTP.start'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:824:in 'Net::HTTP.get_response'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/net-http-0.9.1/lib/net/http.rb:805:in 'Net::HTTP.get'
from test.rb:11:in 'block (2 levels) in <main>'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/async-2.36.0/lib/async/scheduler.rb:645:in 'block in Async::Scheduler#timeout_after'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/async-2.36.0/lib/async/scheduler.rb:626:in 'Async::Scheduler#with_timeout'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/async-2.36.0/lib/async/scheduler.rb:644:in 'Async::Scheduler#timeout_after'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/timeout-0.6.0/lib/timeout.rb:285:in 'Timeout.timeout'
from test.rb:8:in 'block in <main>'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/async-2.36.0/lib/async/task.rb:234:in 'block in Async::Task#run'
from /Users/leuser/.rvm/gems/ruby-3.4.2/gems/async-2.36.0/lib/async/task.rb:512:in 'block in Async::Task#schedule'
I know that Timeout.timeout takes into account the Fiber scheduler and it seems the timeout implementation inside the scheduler depends on the error class being passed to it. Since to my knowledge async's aim is to provide async execution in a transparent manner (i.e. the wrapped code should not know about the async context), passing the very specific Async::TimeoutError (or StandardError or some custom error) is not the ideal solution. Could this be a bug?
Test setup
Ruby 3.4.2
macOS Tahoe
Apple M3
Hello @ioquatix, thank you once again for your dedicated work!
I have trouble understanding the behaviour of
Timeout.timeoutblock when run inside async reactor. The test scenario uses an HTTP call with delayed response but in the real world it could be a call to some LLM which is not unusual to take several seconds.This works as expected (no timeout set)
This is odd
No timeout error is raised and the total execution time seems to be HTTP call (~5.5s) + timeout (2s)
This works but why?
with the expected result:
I know that
Timeout.timeouttakes into account the Fiber scheduler and it seems the timeout implementation inside the scheduler depends on the error class being passed to it. Since to my knowledgeasync's aim is to provide async execution in a transparent manner (i.e. the wrapped code should not know about the async context), passing the very specificAsync::TimeoutError(orStandardErroror some custom error) is not the ideal solution. Could this be a bug?Test setup
Ruby 3.4.2
macOS Tahoe
Apple M3