diff --git a/examples/timeouts/client.rb b/examples/timeouts/client.rb new file mode 100644 index 00000000..f3814a64 --- /dev/null +++ b/examples/timeouts/client.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Thomas Morgan. + +$LOAD_PATH.unshift File.expand_path("../../lib", __dir__) +# $:.unshift File.expand_path '../../../protocol-http1/lib', __dir__ + +require 'async' +require 'async/http/client' +require 'async/http/endpoint' + +CONNECT_TIMEOUT = 3 +IDLE_TIMEOUT = 90 +READ_WRITE_TIMEOUT = 5 +RESPONSE_WAIT_TIMEOUT = 55 + +METRICS = { + requests_sent: 0, + responses_received: 0, +} + +module HTTP1Timeouts + + def sending_request + stream.io.timeout = READ_WRITE_TIMEOUT + + # To count after the request has been fully written, move this into waiting_for_response. + METRICS[:requests_sent] += 1 + end + + def waiting_for_response + # Upstreams sometimes take a while to process a request and begin the response. + # This allows extra time at that stage. + stream.io.timeout = RESPONSE_WAIT_TIMEOUT + end + + def received_response + # Return to a shorter timeout for reading the body, if any. + stream.io.timeout = READ_WRITE_TIMEOUT + + METRICS[:responses_received] += 1 + end + +end + +module HTTP2Timeouts + + def connection_ready + # To facilitate keepalive, use IDLE_TIMEOUT instead of READ_WRITE_TIMEOUT + stream.io.timeout = IDLE_TIMEOUT + end + +end + +Protocol::HTTP1::Connection.include HTTP1Timeouts +Async::HTTP::Protocol::HTTP2::Client.include HTTP2Timeouts + +endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:8080', reuse_port: true, timeout: CONNECT_TIMEOUT) + + +protocol = Async::HTTP::Protocol::HTTP1 +# protocol = Async::HTTP::Protocol::HTTP2 + +puts "Making request with #{protocol}..." +Async do |task| + client = Async::HTTP::Client.new(endpoint, protocol: protocol) + response = client.get(endpoint.path) + puts response.read +ensure + client.close +end + +puts METRICS diff --git a/examples/timeouts/server.rb b/examples/timeouts/server.rb new file mode 100644 index 00000000..e8f22284 --- /dev/null +++ b/examples/timeouts/server.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +# Released under the MIT License. +# Copyright, 2023, by Thomas Morgan. + +$LOAD_PATH.unshift File.expand_path("../../lib", __dir__) +# $:.unshift File.expand_path '../../../protocol-http1/lib', __dir__ + +require 'async' +require 'async/http/server' +require 'async/http/endpoint' + +IDLE_TIMEOUT = 90 +READ_WRITE_TIMEOUT = 5 + +METRICS = { + requests_received: 0, + responses_sent: 0, +} + +module HTTP1Timeouts + + def waiting_for_request + if count == 0 + # First request for this connection + stream.io.timeout = READ_WRITE_TIMEOUT + else + # Additional requests; aka keep-alive + stream.io.timeout = IDLE_TIMEOUT + end + end + + def receiving_request + # Client must send the request headers and body, if any, in a timely manner. + stream.io.timeout = READ_WRITE_TIMEOUT + + METRICS[:requests_received] += 1 + end + + def sent_response + # alternate location for: + # stream.io.timeout = IDLE_TIMEOUT + + METRICS[:responses_sent] += 1 + end + +end + +module HTTP2Timeouts + + def connection_ready + stream.io.timeout = IDLE_TIMEOUT + end + +end + +Protocol::HTTP1::Connection.include HTTP1Timeouts +Async::HTTP::Protocol::HTTP2::Server.include HTTP2Timeouts + +endpoint = Async::HTTP::Endpoint.parse('http://127.0.0.1:8080', reuse_port: true, timeout: READ_WRITE_TIMEOUT) + + +protocol = Async::HTTP::Protocol::HTTP1 +# protocol = Async::HTTP::Protocol::HTTP2 + +puts "Accepting #{protocol}..." +begin + Async do + server = Async::HTTP::Server.for(endpoint, protocol: protocol) do |request| + Protocol::HTTP::Response[200, {}, 'response from server'] + end + server.run + end +rescue Interrupt +end + +puts METRICS diff --git a/lib/async/http/protocol/http1/server.rb b/lib/async/http/protocol/http1/server.rb index 71fc9de0..e2fcf879 100644 --- a/lib/async/http/protocol/http1/server.rb +++ b/lib/async/http/protocol/http1/server.rb @@ -96,6 +96,8 @@ def each(task: Task.current) # Gracefully finish reading the request body if it was not already done so. request&.finish + sent_response if respond_to?(:sent_response) + # This ensures we yield at least once every iteration of the loop and allow other fibers to execute. task.yield rescue => error diff --git a/lib/async/http/protocol/http2/connection.rb b/lib/async/http/protocol/http2/connection.rb index 9b26c9f2..162a9d9c 100644 --- a/lib/async/http/protocol/http2/connection.rb +++ b/lib/async/http/protocol/http2/connection.rb @@ -50,6 +50,8 @@ def http2? end def start_connection + connection_ready if respond_to?(:connection_ready) + @reader || read_in_background end