Class: PatientHttp::ClientPool

Inherits:
Object
  • Object
show all
Defined in:
lib/patient_http/client_pool.rb

Overview

Pool of HTTP clients with LRU eviction.

Maintains a pool of clients lazily instantiated for each host. The pool is capped with an LRU algorithm - when a new client is needed and the pool is at capacity, the least recently used client is closed and removed.

Constant Summary collapse

PROTOCOLS =

Supported protocol names mapped to their async-http implementations. Forcing :http1 also limits the TLS ALPN advertisement to http/1.1, which avoids HTTP/2 negotiation with servers and middleboxes that mishandle it.

{
  http1: Async::HTTP::Protocol::HTTP11,
  http2: Async::HTTP::Protocol::HTTP2
}.freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(max_size:, connection_timeout: nil, proxy_url: nil, retries: 3, protocol: nil) ⇒ ClientPool

Returns a new instance of ClientPool.



18
19
20
21
22
23
24
25
26
27
28
29
30
31
# File 'lib/patient_http/client_pool.rb', line 18

def initialize(max_size:, connection_timeout: nil, proxy_url: nil, retries: 3, protocol: nil)
  if protocol && !PROTOCOLS.include?(protocol)
    raise ArgumentError.new("protocol must be one of #{PROTOCOLS.keys.inspect}, got: #{protocol.inspect}")
  end

  @clients = {}
  @max_size = max_size
  @connection_timeout = connection_timeout
  @proxy_url = proxy_url
  @retries = retries
  @protocol = protocol
  @mutex = Mutex.new
  @proxy_client = nil
end

Instance Attribute Details

#connection_timeoutObject (readonly)

Returns the value of attribute connection_timeout.



33
34
35
# File 'lib/patient_http/client_pool.rb', line 33

def connection_timeout
  @connection_timeout
end

#max_sizeObject (readonly)

Returns the value of attribute max_size.



33
34
35
# File 'lib/patient_http/client_pool.rb', line 33

def max_size
  @max_size
end

#protocolObject (readonly)

Returns the value of attribute protocol.



33
34
35
# File 'lib/patient_http/client_pool.rb', line 33

def protocol
  @protocol
end

#proxy_urlObject (readonly)

Returns the value of attribute proxy_url.



33
34
35
# File 'lib/patient_http/client_pool.rb', line 33

def proxy_url
  @proxy_url
end

#retriesObject (readonly)

Returns the value of attribute retries.



33
34
35
# File 'lib/patient_http/client_pool.rb', line 33

def retries
  @retries
end

Instance Method Details

#client_for(endpoint) ⇒ Protocol::HTTP::AcceptEncoding

Get or create a client for the given endpoint.

Parameters:

  • endpoint (Async::HTTP::Endpoint)

    the target endpoint

Returns:

  • (Protocol::HTTP::AcceptEncoding)

    wrapped client



39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/patient_http/client_pool.rb', line 39

def client_for(endpoint)
  key = host_key(endpoint)

  @mutex.synchronize do
    if @clients.key?(key)
      # Move to end (most recently used) by re-inserting
      client = @clients.delete(key)
      @clients[key] = client
      return client
    end

    evict_lru if @clients.size >= @max_size
    @clients[key] = make_client(endpoint)
  end
end

#closevoid

This method returns an undefined value.

Close all clients and release resources.



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/patient_http/client_pool.rb', line 91

def close
  @mutex.synchronize do
    @clients.each_value do |client|
      client.close
    rescue
      nil
    end
    @clients.clear

    begin
      @proxy_client&.close
    rescue
      nil
    end
    @proxy_client = nil
  end
end

#evict(url) ⇒ void

This method returns an undefined value.

Evict and close the client for the given URL.

This forces a new connection to be established on the next request to this host.

Parameters:

  • url (String)

    the request URL whose host client should be evicted



115
116
117
118
119
120
121
122
123
124
125
126
127
# File 'lib/patient_http/client_pool.rb', line 115

def evict(url)
  endpoint = Async::HTTP::Endpoint.parse(url)
  key = host_key(endpoint)

  @mutex.synchronize do
    client = @clients.delete(key)
    begin
      client&.close
    rescue
      nil
    end
  end
end

#request(http_method, url, headers, body, &block) ⇒ Protocol::HTTP::Response

Make a request.

Parameters:

  • http_method (String, Symbol)

    HTTP method

  • url (String)

    request URL

  • headers (Hash)

    request headers

  • body (String, nil)

    request body

  • block (Proc)

    optional block to process the response

Returns:

  • (Protocol::HTTP::Response)

    the response



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
# File 'lib/patient_http/client_pool.rb', line 63

def request(http_method, url, headers, body, &block)
  endpoint = Async::HTTP::Endpoint.parse(url)
  client = client_for(endpoint)

  verb = http_method.to_s.upcase

  options = {
    headers: headers,
    body: body,
    scheme: endpoint.scheme,
    authority: endpoint.authority
  }

  request = ::Protocol::HTTP::Request[verb, endpoint.path, **options]
  response = client.call(request)

  return response unless block_given?

  begin
    yield response
  ensure
    response.close
  end
end

#sizeInteger

Returns number of clients in the pool.

Returns:

  • (Integer)

    number of clients in the pool



130
131
132
# File 'lib/patient_http/client_pool.rb', line 130

def size
  @mutex.synchronize { @clients.size }
end