Class: Hypertube::Core::Http2Client::Http2Client

Inherits:
Object
  • Object
show all
Defined in:
lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb

Defined Under Namespace

Classes: Http2TransportError

Constant Summary collapse

MAX_ATTEMPTS =
2
DEFAULT_CONNECT_TIMEOUT_SECONDS =
120
DEFAULT_RESPONSE_TIMEOUT_SECONDS =
180
RESTRICTED_HTTP2_HEADERS =
%w[
  connection
  keep-alive
  proxy-connection
  transfer-encoding
  upgrade
  host
].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_data) ⇒ Http2Client

Returns a new instance of Http2Client.



132
133
134
135
136
137
138
139
140
141
142
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 132

def initialize(connection_data)
  @connection_data = connection_data
  @send_receive_lock = Mutex.new
  @url = connection_data.respond_to?(:hostname) ? connection_data.hostname : connection_data.to_s
  @uri = URI.parse(@url)
  unless %w[http https].include?(@uri.scheme.to_s.downcase)
    raise "Http2Client requires 'http' (h2c) or 'https' (h2 over TLS) scheme. Provided URL: #{@url}"
  end
  @response_timeout = connection_data.respond_to?(:response_timeout) ? connection_data.response_timeout : DEFAULT_RESPONSE_TIMEOUT_SECONDS
  @http_client = build_http_client
end

Class Method Details

.close(connection_data) ⇒ Object



71
72
73
74
75
76
77
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 71

def close(connection_data)
  client = nil
  @clients_lock.synchronize do
    client = @clients.delete(connection_key(connection_data))
  end
  client&.close
end

.get_state(connection_data) ⇒ Object



79
80
81
82
83
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 79

def get_state(connection_data)
  @clients_lock.synchronize do
    @clients.key?(connection_key(connection_data)) ? :OPEN : nil
  end
end

.send_message(connection_data, message) ⇒ Object

Raises:

  • (ArgumentError)


29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 29

def send_message(connection_data, message)
  raise ArgumentError, 'connection_data cannot be nil' if connection_data.nil?
  raise ArgumentError, 'message cannot be nil' if message.nil?

  begin
    send_message_body(connection_data, message)
  rescue StandardError
    # Evict desynced/broken connection after final failure (bug 32346).
    close(connection_data)
    raise
  end
end

.send_message_body(connection_data, message) ⇒ Object

Raises:

  • (RuntimeError)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 42

def send_message_body(connection_data, message)
  client = create_or_get_client(connection_data, expected_current: nil)
  last_ex = nil

  MAX_ATTEMPTS.times do |attempt|
    begin
      # Lock the current client only for this send so a recreate on retry
      # switches to the new client's gate.
      client.with_send_receive_lock do
        return client.send_bytes(message)
      end
    rescue StandardError => ex
      last_ex = ex
      break if attempt == MAX_ATTEMPTS - 1
      break unless transient_http2_error?(ex)

      client = create_or_get_client(connection_data, expected_current: client)
    end
  end

  if last_ex
    e = RuntimeError.new("HTTP/2 send to #{connection_key(connection_data)} failed after #{MAX_ATTEMPTS} attempts. Last error: #{last_ex.message}")
    e.set_backtrace(last_ex.backtrace)
    raise e
  end

  raise RuntimeError, "HTTP/2 send to #{connection_key(connection_data)} failed after #{MAX_ATTEMPTS} attempts."
end

Instance Method Details

#closeObject



198
199
200
201
202
203
204
205
206
207
208
209
210
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 198

def close
  return unless @http_client

  if @http_client.respond_to?(:close)
    @http_client.close
  elsif @http_client.respond_to?(:shutdown)
    @http_client.shutdown
  end
rescue StandardError
  # Best-effort transport cleanup only; callers handle send errors.
ensure
  @http_client = nil
end

#send_bytes(message) ⇒ Object



148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 148

def send_bytes(message)
  payload = normalize_payload(message)
  response = nil
  ensure_server_reachable!
  Timeout.timeout(operation_timeout_seconds) do
    response = @http_client.post(@url, headers: request_headers, body: payload)

    if defined?(HTTPX::ErrorResponse) && response.is_a?(HTTPX::ErrorResponse)
      response_error = response.respond_to?(:error) ? response.error : nil
      details = response_error ? response_error.message : response.to_s
      raise Http2TransportError, "HTTP/2 request to #{@url} failed: #{details}"
    end

    status_code = response.status.to_i
    http_version = response.version.to_s
    if !http_version.empty? && !http_version.start_with?('2')
      raise RuntimeError, "HTTP/2 request to #{@url} failed: the endpoint did not negotiate HTTP/2."
    end

    reason = extract_reason_header(response)
    if status_code < 200 || status_code >= 300
      raise RuntimeError, "HTTP/2 request to #{@url} failed with status #{status_code} #{reason}. "
    end

    return response.body.to_s.bytes
  end
rescue Timeout::Error
  timeout_text =
    if @response_timeout && @response_timeout >= 0
      "timed out after #{@response_timeout} seconds"
    else
      "timed out"
    end
  raise Http2TransportError, "HTTP/2 request to #{@url} #{timeout_text}. The server may not be responding."
rescue StandardError => e
  if timeout_error?(e)
    timeout_text =
      if @response_timeout && @response_timeout >= 0
        "timed out after #{@response_timeout} seconds"
      else
        "timed out"
      end
    raise Http2TransportError, "HTTP/2 request to #{@url} #{timeout_text}. The server may not be responding."
  end
  if defined?(HTTPX::Error) && e.is_a?(HTTPX::Error)
    raise Http2TransportError, "HTTP/2 request to #{@url} failed: #{e.message}. The server may be unavailable or not responding."
  end
  raise
end

#with_send_receive_lockObject



144
145
146
# File 'lib/hypertube-ruby-sdk/core/http2_client/http2_client.rb', line 144

def with_send_receive_lock
  @send_receive_lock.synchronize { yield }
end