Class: HTTP::Timeout::PerOperation

Inherits:
Null
  • Object
show all
Defined in:
lib/http/timeout/per_operation.rb,
sig/http.rbs

Overview

Timeout handler with separate timeouts for connect, read, and write

Constant Summary collapse

KEYS =

Mapping of shorthand option keys to their full forms

Returns:

  • (Hash[Symbol, Symbol])
%i[read write connect].to_h { |k| [k, :"#{k}_timeout"] }.freeze
WAIT_RESULTS =

I/O wait result symbols returned by non-blocking operations

Returns:

  • (Array[Symbol])
%i[wait_readable wait_writable].freeze

Constants inherited from Null

Null::NATIVE_CONNECT_TIMEOUT

Instance Attribute Summary collapse

Attributes inherited from Null

#options, #socket

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Null

#close, #closed?, #native_timeout?, #open_socket, #open_with_timeout, #rescue_readable, #rescue_writable, #start_tls

Constructor Details

#initialize(read_timeout: nil, write_timeout: nil, connect_timeout: nil) ⇒ HTTP::Timeout::PerOperation

Initializes per-operation timeout with options

Examples:

HTTP::Timeout::PerOperation.new(read_timeout: 5)

Parameters:

  • read_timeout (Numeric, nil) (defaults to: nil)

    Read timeout in seconds (nil for no timeout)

  • write_timeout (Numeric, nil) (defaults to: nil)

    Write timeout in seconds (nil for no timeout)

  • connect_timeout (Numeric, nil) (defaults to: nil)

    Connect timeout in seconds (nil for no timeout)

  • read_timeout: (Numeric, nil) (defaults to: nil)
  • write_timeout: (Numeric, nil) (defaults to: nil)
  • connect_timeout: (Numeric, nil) (defaults to: nil)


85
86
87
88
89
90
91
# File 'lib/http/timeout/per_operation.rb', line 85

def initialize(read_timeout: nil, write_timeout: nil, connect_timeout: nil)
  super

  @read_timeout = read_timeout
  @write_timeout = write_timeout
  @connect_timeout = connect_timeout
end

Instance Attribute Details

#connect_timeoutNumeric? (readonly)

Returns the value of attribute connect_timeout.

Returns:

  • (Numeric, nil)


1533
1534
1535
# File 'sig/http.rbs', line 1533

def connect_timeout
  @connect_timeout
end

#read_timeoutNumeric? (readonly)

Returns the value of attribute read_timeout.

Returns:

  • (Numeric, nil)


1531
1532
1533
# File 'sig/http.rbs', line 1531

def read_timeout
  @read_timeout
end

#write_timeoutNumeric? (readonly)

Returns the value of attribute write_timeout.

Returns:

  • (Numeric, nil)


1532
1533
1534
# File 'sig/http.rbs', line 1532

def write_timeout
  @write_timeout
end

Class Method Details

.normalize_options(options) ⇒ Hash

Normalize and validate timeout options

Examples:

PerOperation.normalize_options(read: 5, write: 3)

Parameters:

  • options (Hash)

    timeout options with short or long keys

Returns:

  • (Hash)

    normalized options with long keys

Raises:

  • (ArgumentError)

    if options are invalid



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/http/timeout/per_operation.rb', line 23

def self.normalize_options(options)
  remaining  = options.dup
  normalized = {} #: Hash[Symbol, Numeric]

  KEYS.each do |short, long|
    next if !remaining.key?(short) && !remaining.key?(long)

    normalized[long] = resolve_timeout_value!(remaining, short, long)
  end

  raise ArgumentError, "unknown timeout options: #{remaining.keys.join(', ')}" unless remaining.empty?
  raise ArgumentError, "no timeout options given" if normalized.empty?

  normalized
end

Instance Method Details

#connect(socket_class, host, port, nodelay: false) ⇒ void

This method returns an undefined value.

Connects to a socket with connect timeout

Examples:

timeout.connect(TCPSocket, "example.com", 80)

Parameters:

  • socket_class (Class)
  • host (String)
  • port (Integer)
  • nodelay (Boolean) (defaults to: false)
  • nodelay: (Boolean) (defaults to: false)


104
105
106
107
# File 'lib/http/timeout/per_operation.rb', line 104

def connect(socket_class, host, port, nodelay: false)
  @socket = open_socket(socket_class, host, port, connect_timeout: @connect_timeout)
  @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
end

#connect_sslvoid

This method returns an undefined value.

Starts an SSL connection with connect timeout

Examples:

timeout.connect_ssl


116
117
118
119
120
121
122
# File 'lib/http/timeout/per_operation.rb', line 116

def connect_ssl
  rescue_readable(@connect_timeout) do
    rescue_writable(@connect_timeout) do
      @socket.connect_nonblock
    end
  end
end

#readpartial(size, buffer = nil) ⇒ String, :eof

Read data from the socket

Examples:

timeout.readpartial(1024)

Parameters:

  • size (Integer)
  • buffer (String, nil) (defaults to: nil)

Returns:

  • (String, :eof)


136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/http/timeout/per_operation.rb', line 136

def readpartial(size, buffer = nil)
  timeout = false
  loop do
    result = @socket.read_nonblock(size, buffer, exception: false)

    return :eof   if result.nil?
    return result unless WAIT_RESULTS.include?(result)

    raise TimeoutError, "Read timed out after #{@read_timeout} seconds" if timeout

    # marking the socket for timeout. Why is this not being raised immediately?
    # it seems there is some race-condition on the network level between calling
    # #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
    # for reads, and when waiting for x seconds, it returns nil suddenly without completing
    # the x seconds. In a normal case this would be a timeout on wait/read, but it can
    # also mean that the socket has been closed by the server. Therefore we "mark" the
    # socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
    # timeout. Else, the first timeout was a proper timeout.
    # This hack has to be done because io/wait#wait_readable doesn't provide a value for when
    # the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
    timeout = true unless wait_for_io(result, @read_timeout)
  end
end

#wait_for_io(result, timeout) ⇒ Object?

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Waits for I/O readiness based on the result type

Parameters:

  • result (Symbol)

    the I/O wait type (:wait_readable or :wait_writable)

  • timeout (Numeric, nil)

    per-operation timeout limit

Returns:

  • (Object, nil)

    the socket if ready, nil on timeout



188
189
190
191
192
193
194
# File 'lib/http/timeout/per_operation.rb', line 188

def wait_for_io(result, timeout)
  if result == :wait_readable
    @socket.to_io.wait_readable(timeout)
  else
    @socket.to_io.wait_writable(timeout)
  end
end

#write(data) ⇒ Integer

Write data to the socket

Examples:

timeout.write("GET / HTTP/1.1")

Parameters:

  • data (String)

Returns:

  • (Integer)


168
169
170
171
172
173
174
175
176
177
178
# File 'lib/http/timeout/per_operation.rb', line 168

def write(data)
  timeout = false
  loop do
    result = @socket.write_nonblock(data, exception: false)
    return result unless WAIT_RESULTS.include?(result)

    raise TimeoutError, "Write timed out after #{@write_timeout} seconds" if timeout

    timeout = true unless wait_for_io(result, @write_timeout)
  end
end