httpx/test/session_test.rb
HoneyryderChuck 8bd4dc1fbd fix timers overhead causing spurious wakeups on the select loop
the change to read/write cancellation-driven timeouts as the default
timeout strategy revealed a performance regression; because these were
built on Timers, which never got unsubscribed, this meant that they were
kept beyond the duration of the request they were created for, and
needlessly got picked up for the next timeout tick.

This was fixed by adding a callback on timer intervals, which
unsubscribes them from the timer group when called; these would then be
activated after the timeout is not needed anymore (request send /
response received), thereby removing the overhead on subsequent
requests.

An additional intervals array is also kept in the connection itself;
timeouts from timers are signalled via socket wait calls, however they
were always resulting in timeouts, even when they shouldn't (ex: expect
timeout and send full response payload as a result), and with the wrong
exception class in some cases. By keeping intervals from its requests
around, and monitoring whether there are relevant request triggers, the
connection can therefore handle a timeout or bail out (so that timers
can fire the correct callback).
2023-10-24 22:53:22 +01:00

231 lines
6.7 KiB
Ruby

# frozen_string_literal: true
require_relative "test_helper"
class SessionTest < Minitest::Test
include HTTPHelpers
def test_session_block
yielded = nil
HTTPX::Session.new do |cli|
yielded = cli
end
assert yielded.is_a?(HTTPX::Session), "session should have been yielded"
end
def test_session_plugin
klient_class = Class.new(HTTPX::Session)
klient_class.plugin(TestPlugin)
session = klient_class.new
assert session.respond_to?(:foo), "instance methods weren't added"
assert session.foo == "session-foo", "instance method is unexpected"
assert session.respond_to?(:bar), "load and configure didn't work"
assert session.bar == "config-load-bar", "load and configure didn't work"
assert session.respond_to?(:options), "instance methods weren't added"
assert session.options.respond_to?(:foo), "options methods weren't added"
assert session.options.foo == "options-foo", "option method is unexpected"
request = session.options.request_class.new("GET", "http://example.com/", session.options)
assert request.respond_to?(:foo), "request methods haven't been added"
assert request.foo == "request-foo", "request method is unexpected"
assert request.headers.respond_to?(:foo), "headers methods haven't been added"
assert request.headers.foo == "headers-foo", "headers method is unexpected"
assert session.respond_to?(:response), "response constructor was added"
req_body = request.body
assert req_body.respond_to?(:foo), "request body methods haven't been added"
assert req_body.foo == "request-body-foo", "request body method is unexpected"
response = session.response(request, 200, "2.0", {})
assert response.respond_to?(:foo), "response methods haven't been added"
assert response.foo == "response-foo", "response method is unexpected"
assert request.headers.respond_to?(:foo), "headers methods haven't been added"
assert request.headers.foo == "headers-foo", "headers method is unexpected"
body = response.body
assert body.respond_to?(:foo), "response body methods haven't been added"
assert body.foo == "response-body-foo", "response body method is unexpected"
end
def test_session_timeout_connect_timeout
server = TCPServer.new("127.0.0.1", CONNECT_TIMEOUT_PORT)
begin
uri = build_uri("/", origin("127.0.0.1:#{CONNECT_TIMEOUT_PORT}"))
session = HTTPX.with_timeout(connect_timeout: 0.5)
response = session.get(uri)
verify_error_response(response)
verify_error_response(response, HTTPX::ConnectTimeoutError)
ensure
server.close
end
end
def test_session_timeouts_read_timeout
uri = build_uri("/drip?numbytes=10&duration=4&delay=2&code=200")
session = HTTPX.with(timeout: { read_timeout: 3 })
response = session.get(uri)
verify_error_response(response, HTTPX::ReadTimeoutError)
uri = build_uri("/drip?numbytes=10&duration=1&delay=0&code=200")
response1 = session.get(uri)
verify_status(response1, 200)
end
def test_session_timeouts_write_timeout
start_test_servlet(SlowReader) do |server|
uri = URI("#{server.origin}/")
session = HTTPX.with(timeout: { write_timeout: 4 })
response = session.post(uri, body: StringIO.new("a" * 65_536 * 3 * 5))
verify_error_response(response, HTTPX::WriteTimeoutError)
response1 = session.post(uri, body: StringIO.new("a" * 65_536 * 2 * 5))
verify_status(response1, 200)
end
end
def test_session_timeouts_request_timeout
uri = build_uri("/drip?numbytes=10&duration=4&delay=2&code=200")
session = HTTPX.with(timeout: { request_timeout: 3, operation_timeout: 10 })
response = session.get(uri)
verify_error_response(response, HTTPX::RequestTimeoutError)
uri = build_uri("/drip?numbytes=10&duration=1&delay=0&code=200")
response1 = session.get(uri)
verify_status(response1, 200)
end
# def test_http_timeouts_operation_timeout
# uri = build_uri("/delay/2")
# session = HTTPX.with_timeout(operation_timeout: 1)
# response = session.get(uri)
# verify_error_response(response, /timed out while waiting/)
# end
def test_session_timeout_keep_alive_timeout
uri = build_uri("/get")
HTTPX.plugin(SessionWithPool).wrap do |http|
response1 = http.get(uri)
sleep(3)
response2 = http.get(uri)
verify_status(response1, 200)
verify_status(response2, 200)
connection_count = http.pool.connection_count
assert connection_count == 1, "session opened more connections than expected (#{connection_count})"
end
HTTPX.plugin(SessionWithPool).with(timeout: { keep_alive_timeout: 2 }).wrap do |http|
response1 = http.get(uri)
sleep(3)
response2 = http.get(uri)
verify_status(response1, 200)
verify_status(response2, 200)
ping_count = http.pool.ping_count
assert ping_count == 1, "session should have pinged after timeout (#{ping_count})"
end
end
TestPlugin = Module.new do
self::ClassMethods = Module.new do
def foo
"session-foo"
end
end
self::InstanceMethods = Module.new do
def foo
self.class.foo
end
attr_reader :options
def response(*args)
@options.response_class.new(*args)
end
end
self::RequestClassMethods = Module.new do
def foo
"request-foo"
end
end
self::RequestMethods = Module.new do
def foo
self.class.foo
end
end
self::RequestBodyClassMethods = Module.new do
def foo
"request-body-foo"
end
end
self::RequestBodyMethods = Module.new do
def foo
self.class.foo
end
end
self::ResponseClassMethods = Module.new do
def foo
"response-foo"
end
end
self::ResponseMethods = Module.new do
def foo
self.class.foo
end
end
self::ResponseBodyClassMethods = Module.new do
def foo
"response-body-foo"
end
end
self::ResponseBodyMethods = Module.new do
def foo
self.class.foo
end
end
self::HeadersClassMethods = Module.new do
def foo
"headers-foo"
end
end
self::HeadersMethods = Module.new do
def foo
self.class.foo
end
end
self::OptionsMethods = Module.new do
def option_foo(v)
v
end
end
def self.load_dependencies(mod)
mod.__send__(:include, Module.new do
def bar
"load-bar"
end
end)
end
def self.extra_options(options)
options.merge(foo: "options-foo")
end
def self.configure(mod)
mod.__send__(:include, Module.new do
def bar
"config-#{super}"
end
end)
end
end
private
def origin(orig = httpbin)
"https://#{orig}"
end
end