Class: HTTP::Timeout::PerOperation

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

Overview

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

Constant Summary collapse

KEYS =

Mapping of shorthand option keys to their full forms

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

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

%i[wait_readable wait_writable].freeze

Constants inherited from Null

Null::NATIVE_CONNECT_TIMEOUT

Instance Attribute Summary

Attributes inherited from Null

#options, #socket

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Null

#close, #closed?, #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)



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

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)


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

#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