client-api-builder 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +40 -6
- data/lib/client-api-builder.rb +1 -0
- data/lib/client_api_builder/connection_pools.rb +3 -0
- data/lib/client_api_builder/http2.rb +6 -4
- data/lib/client_api_builder/nested_router.rb +12 -3
- data/lib/client_api_builder/thread_connections/connection_set.rb +132 -0
- data/lib/client_api_builder/thread_connections/settings.rb +19 -0
- data/lib/client_api_builder/thread_connections.rb +56 -0
- data/lib/client_api_builder/version.rb +1 -1
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c5a8b4e5fcad31bffb2d4234fb2d73205ae0e3aed33ba75ec32defa778ed456f
|
|
4
|
+
data.tar.gz: ff349350509add94a7108af7dddf9416b98265752735a510d46d35bc97897849
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cea030b2bb67c167c3dc4f4a798cadd2e4a8c873fe71d19bed944b4263446f0befb3cbdbc6dccbd9741814aeedb52f0b0b7b2aff155f7800c221601d52383fbd
|
|
7
|
+
data.tar.gz: 62d3ccbfc3ad52500a29070c6bad1961ba06539e131fc53e5eb775616c8ca179d91c8bd4f0e3bfa150cb3c40a2f8897f3e8bb957d4ae867400c7ec6a872db42e
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.11.0](https://github.com/dougyouch/client-api-builder/compare/v0.10.0...v0.11.0) (2026-10-04)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **thread_connections:** add per-thread persistent connections ([3ad347d](https://github.com/dougyouch/client-api-builder/commit/3ad347d7a022b60dfb9e0d36de5260dd37f456be))
|
|
9
|
+
* **thread_connections:** add per-thread persistent connections ([4f61cd9](https://github.com/dougyouch/client-api-builder/commit/4f61cd94731ac81c0993bfbee8acf6c9c4328ce2))
|
|
10
|
+
|
|
3
11
|
## [0.10.0](https://github.com/dougyouch/client-api-builder/compare/v0.9.0...v0.10.0) (2026-10-04)
|
|
4
12
|
|
|
5
13
|
|
data/README.md
CHANGED
|
@@ -16,6 +16,7 @@ A Ruby gem for building robust, secure API clients through declarative configura
|
|
|
16
16
|
- **Nested Routing** - Organize complex APIs with hierarchical route structures
|
|
17
17
|
- **Retry Logic** - Configurable automatic retries for transient network failures
|
|
18
18
|
- **Connection Pooling** - Opt-in persistent connections shared across threads, with no extra gems
|
|
19
|
+
- **Per-Thread Connections** - Opt-in persistent connections dedicated to each thread, with no pool to size
|
|
19
20
|
- **HTTP/2** - Opt-in HTTP/2 over TLS, multiplexing concurrent requests on one connection per host (with the `http-2` gem)
|
|
20
21
|
- **Streaming Support** - Handle large payloads efficiently with streaming to files or IO
|
|
21
22
|
- **ActiveSupport Integration** - Optional logging and instrumentation
|
|
@@ -400,9 +401,41 @@ The pools live on the class, like ActiveRecord's, so every instance shares them:
|
|
|
400
401
|
|
|
401
402
|
Pooling uses only `Net::HTTP` from the standard library.
|
|
402
403
|
|
|
404
|
+
### Per-Thread Connections
|
|
405
|
+
|
|
406
|
+
When a program runs a fixed set of worker threads, each making its own requests, include `ClientApiBuilder::ThreadConnections` (after `Router`, instead of `ConnectionPools`) to give every thread its own persistent connections:
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
class MyApiClient
|
|
410
|
+
include ClientApiBuilder::Router
|
|
411
|
+
include ClientApiBuilder::ThreadConnections
|
|
412
|
+
|
|
413
|
+
base_url 'https://api.example.com'
|
|
414
|
+
|
|
415
|
+
# Optional; these are the defaults
|
|
416
|
+
connection_per_thread ttl: 30, # seconds before a connection is closed and replaced
|
|
417
|
+
idle_timeout: 2 # seconds idle before Net::HTTP reconnects (keep_alive_timeout)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
workers = Array.new(20) do
|
|
421
|
+
Thread.new do
|
|
422
|
+
client = MyApiClient.new
|
|
423
|
+
user_ids.each { |id| client.get_user(id: id) } # always over this thread's connection
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
A thread opens a connection per host (scheme, port and connection options) on its first request and reuses it for every later one. There's no pool size to keep in step with the thread count, and no thread ever waits for another's connection.
|
|
429
|
+
|
|
430
|
+
- Sockets the server has closed, or that sat idle past `idle_timeout`, are reopened automatically. A connection whose request raised is closed rather than reused.
|
|
431
|
+
- A request made while the thread's connection is busy (from inside a streaming block, or from another fiber on the thread) opens a connection of its own; the thread keeps one of the two afterwards.
|
|
432
|
+
- Connections of threads that have finished are closed when a thread makes its first request. With short-lived threads, prefer `ConnectionPools`.
|
|
433
|
+
- Sections and subclasses share the class's connections the same way they share pools, and a section can call `connection_per_thread` in its block to get its own.
|
|
434
|
+
- After a fork, the child process opens its own connections. `MyApiClient.close_connections` closes every thread's idle connections now and in-use ones when their request ends.
|
|
435
|
+
|
|
403
436
|
### HTTP/2
|
|
404
437
|
|
|
405
|
-
Include `ClientApiBuilder::HTTP2` (after `Router`, and after `ConnectionPools` if you use
|
|
438
|
+
Include `ClientApiBuilder::HTTP2` (after `Router`, and after `ConnectionPools` or `ThreadConnections` if you use either) to send https requests over HTTP/2. It needs the [`http-2`](https://rubygems.org/gems/http-2) gem, which isn't installed with this one:
|
|
406
439
|
|
|
407
440
|
```ruby
|
|
408
441
|
# Gemfile
|
|
@@ -421,13 +454,13 @@ end
|
|
|
421
454
|
|
|
422
455
|
Every instance of the class shares one connection per host (and connection options), and concurrent requests from different threads travel over it as separate streams. Responses are ordinary `Net::HTTPResponse` objects with `http_version` `'2.0'`, so `handle_response`, `UnexpectedResponse#response` and streaming routes work unchanged.
|
|
423
456
|
|
|
424
|
-
- The protocol is negotiated during the TLS handshake (ALPN). When a server picks HTTP/1.1, that host is remembered and its requests go through `Net::HTTP`, or the
|
|
457
|
+
- The protocol is negotiated during the TLS handshake (ALPN). When a server picks HTTP/1.1, that host is remembered and its requests go through `Net::HTTP`, or the pooled or per-thread connections if the class includes `ConnectionPools` or `ThreadConnections`. `http://` URLs always use HTTP/1.1.
|
|
425
458
|
- The usual connection options apply: `open_timeout`, `read_timeout` (per response header and body chunk), and the SSL options (`verify_mode`, `ca_file`, `cert_store`, `cert`, `key`, ...). Proxies aren't supported.
|
|
426
459
|
- When the server's limit on concurrent streams is reached, a request waits up to `read_timeout` for a stream to free up.
|
|
427
460
|
- Gzip and deflate responses are inflated, as `Net::HTTP` does, unless you set `Accept-Encoding` yourself.
|
|
428
461
|
- A stream the server refused, or never processed before closing the connection (GOAWAY), raises `ClientApiBuilder::HTTP2::StreamRefused`; a dropped connection raises `ClientApiBuilder::HTTP2::ConnectionLost`. Both are retried by `configure_retries`. A stream the server reset raises `ClientApiBuilder::HTTP2::StreamError`.
|
|
429
462
|
- Response data is acknowledged to the server as it arrives, so a streaming consumer slower than the server buffers the difference in memory.
|
|
430
|
-
- Sections use their root client's connections. After a fork, the child opens its own. `MyApiClient.close_connections` closes the HTTP/2 connections (and the
|
|
463
|
+
- Sections use their root client's connections. After a fork, the child opens its own. `MyApiClient.close_connections` closes the HTTP/2 connections (and the pooled or per-thread ones).
|
|
431
464
|
|
|
432
465
|
**When it's faster.** `script/benchmark_http2.rb` compares a new connection per request, `ConnectionPools` and HTTP/2 against a local TLS server. On one machine (Ruby 4.0.7, http-2 1.2.3):
|
|
433
466
|
|
|
@@ -437,7 +470,7 @@ Every instance of the class shares one connection per host (and connection optio
|
|
|
437
470
|
| 50 threads, 20ms delay, one pooled connection per thread | 628 req/s | 1,904 req/s | 1,765 req/s |
|
|
438
471
|
| 50 threads, 20ms delay, default pool of 5 | 640 req/s | 208 req/s | 1,706 req/s |
|
|
439
472
|
|
|
440
|
-
HTTP/2 avoids a TLS handshake per request, like the pools, and isn't limited by a connection count: many threads waiting on a slow API share one connection. Per request, it costs more client CPU than pooled HTTP/1.1 (the protocol layer is pure Ruby), which shows most with large bodies. If you can give
|
|
473
|
+
HTTP/2 avoids a TLS handshake per request, like the pools, and isn't limited by a connection count: many threads waiting on a slow API share one connection. Per request, it costs more client CPU than pooled HTTP/1.1 (the protocol layer is pure Ruby), which shows most with large bodies. If you can give each thread its own connection (a pool as large as the thread count, or `ThreadConnections`), HTTP/1.1 is usually as fast or faster.
|
|
441
474
|
|
|
442
475
|
### Retry Configuration
|
|
443
476
|
|
|
@@ -759,7 +792,8 @@ end
|
|
|
759
792
|
| `configure_retries(max_attempts, sleep = 0.05, backoff: 1, max_sleep: nil, jitter: false)` | Configure retry behavior |
|
|
760
793
|
| `configure_exponential_retries(attempts:, initial: 0.1, max: 10, multiplier: 2, jitter: false)` | Configure retries with exponential backoff |
|
|
761
794
|
| `connection_pool(**settings)` | Configure persistent connection pools (requires `include ClientApiBuilder::ConnectionPools`) |
|
|
762
|
-
| `
|
|
795
|
+
| `connection_per_thread(**settings)` | Configure persistent per-thread connections (requires `include ClientApiBuilder::ThreadConnections`) |
|
|
796
|
+
| `close_connections` | Close the class's pooled or per-thread connections and its sections' (with `ConnectionPools` or `ThreadConnections`) |
|
|
763
797
|
| `section_routers` | Section router classes by name |
|
|
764
798
|
| `route(name, path, options)` | Define an API endpoint |
|
|
765
799
|
| `section(name, options, &block)` | Define nested routes; `inherit:` opts into the root client's `:headers`, `:query_params` and/or `:connection_options` |
|
|
@@ -783,7 +817,7 @@ Define these in your client to change default behavior:
|
|
|
783
817
|
| Method | Default |
|
|
784
818
|
|--------|---------|
|
|
785
819
|
| `retry_request?(exception, options)` | `true` for the network errors listed under Retry Configuration |
|
|
786
|
-
| `with_http_connection(uri, connection_options, &block)` | Yields a started `Net::HTTP`: a new connection per request,
|
|
820
|
+
| `with_http_connection(uri, connection_options, &block)` | Yields a started `Net::HTTP`: a new connection per request, a pooled one with `ConnectionPools`, or the thread's own with `ThreadConnections` |
|
|
787
821
|
| `escape_path(value)` | Percent-encodes path values (`ERB::Util.url_encode`) |
|
|
788
822
|
| `parse_response(response, options)` | Parses the body as JSON, `nil` when empty |
|
|
789
823
|
| `handle_response(response, options, &block)` | Applies `return:`, parsing and the response block |
|
data/lib/client-api-builder.rb
CHANGED
|
@@ -31,6 +31,7 @@ module ClientApiBuilder
|
|
|
31
31
|
autoload :RouteValueValidator, 'client_api_builder/route_value_validator'
|
|
32
32
|
autoload :Router, 'client_api_builder/router'
|
|
33
33
|
autoload :Section, 'client_api_builder/section'
|
|
34
|
+
autoload :ThreadConnections, 'client_api_builder/thread_connections'
|
|
34
35
|
|
|
35
36
|
module NetHTTP
|
|
36
37
|
autoload :Request, 'client_api_builder/net_http_request'
|
|
@@ -26,6 +26,9 @@ module ClientApiBuilder
|
|
|
26
26
|
unless base.include?(::ClientApiBuilder::Router)
|
|
27
27
|
raise ArgumentError, 'include ClientApiBuilder::Router before ClientApiBuilder::ConnectionPools'
|
|
28
28
|
end
|
|
29
|
+
if base.include?(::ClientApiBuilder::ThreadConnections)
|
|
30
|
+
raise ArgumentError, 'include either ClientApiBuilder::ConnectionPools or ClientApiBuilder::ThreadConnections'
|
|
31
|
+
end
|
|
29
32
|
# HTTP2 falls back to the pools through super, so it must come after them
|
|
30
33
|
if base.ancestors.any? { |mod| mod.name == 'ClientApiBuilder::HTTP2' }
|
|
31
34
|
raise ArgumentError, 'include ClientApiBuilder::ConnectionPools before ClientApiBuilder::HTTP2'
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Purpose: opt-in HTTP/2 for https requests, using the http-2 gem (add gem 'http-2' to your
|
|
4
4
|
# Gemfile; it isn't a runtime dependency). Include after ClientApiBuilder::Router, and after
|
|
5
|
-
# ClientApiBuilder::ConnectionPools when using
|
|
5
|
+
# ClientApiBuilder::ConnectionPools or ClientApiBuilder::ThreadConnections when using either:
|
|
6
6
|
#
|
|
7
7
|
# class MyClient
|
|
8
8
|
# include ClientApiBuilder::Router
|
|
@@ -13,8 +13,9 @@
|
|
|
13
13
|
#
|
|
14
14
|
# Every instance of the class shares one connection per origin, carrying concurrent requests
|
|
15
15
|
# as streams. TLS negotiates the protocol (ALPN): when the server picks HTTP/1.1, the origin is
|
|
16
|
-
# remembered and its requests go through Net::HTTP (or the
|
|
17
|
-
# URLs always use HTTP/1.1. Responses are Net::HTTPResponse objects with
|
|
16
|
+
# remembered and its requests go through Net::HTTP (or the pooled or per-thread connections)
|
|
17
|
+
# instead. http URLs always use HTTP/1.1. Responses are Net::HTTPResponse objects with
|
|
18
|
+
# http_version '2.0'.
|
|
18
19
|
module ClientApiBuilder
|
|
19
20
|
module HTTP2
|
|
20
21
|
autoload :Connection, 'client_api_builder/http2/connection'
|
|
@@ -54,7 +55,8 @@ module ClientApiBuilder
|
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
module ClassMethods
|
|
57
|
-
# Closes this class's HTTP/2 connections, and its
|
|
58
|
+
# Closes this class's HTTP/2 connections, and its pooled or per-thread connections when it
|
|
59
|
+
# has them
|
|
58
60
|
def close_connections
|
|
59
61
|
http2_connections.close
|
|
60
62
|
super if defined?(super)
|
|
@@ -53,8 +53,16 @@ module ClientApiBuilder
|
|
|
53
53
|
connection_pool(**settings)
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
#
|
|
57
|
-
#
|
|
56
|
+
# Gives this section its own per-thread connections (see
|
|
57
|
+
# ThreadConnections.connection_per_thread), the same way connection_pool does
|
|
58
|
+
def self.connection_per_thread(**settings)
|
|
59
|
+
include ::ClientApiBuilder::ThreadConnections
|
|
60
|
+
|
|
61
|
+
connection_per_thread(**settings)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Closes the connections of any nested sections that have their own; a section with its
|
|
65
|
+
# own uses ConnectionPools or ThreadConnections.close_connections, which close those as well
|
|
58
66
|
def self.close_connections
|
|
59
67
|
section_routers.each_value(&:close_connections)
|
|
60
68
|
end
|
|
@@ -79,7 +87,8 @@ module ClientApiBuilder
|
|
|
79
87
|
root_router.handle_response(response, options, &)
|
|
80
88
|
end
|
|
81
89
|
|
|
82
|
-
# Uses the root client's connections unless this section calls connection_pool
|
|
90
|
+
# Uses the root client's connections unless this section calls connection_pool or
|
|
91
|
+
# connection_per_thread
|
|
83
92
|
def with_http_connection(uri, connection_options, &)
|
|
84
93
|
root_router.with_http_connection(uri, connection_options, &)
|
|
85
94
|
end
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClientApiBuilder
|
|
4
|
+
module ThreadConnections
|
|
5
|
+
# A client class's connections, kept per thread: one per scheme, host, port and connection
|
|
6
|
+
# options, opened on the thread's first request and reused by its later ones.
|
|
7
|
+
#
|
|
8
|
+
# A connection is taken out of its thread's slot while a request uses it, so a request made
|
|
9
|
+
# before it's back (from a streaming block, or another fiber on the thread) opens one of its
|
|
10
|
+
# own; whichever finishes second is closed rather than kept. A connection that outlives the
|
|
11
|
+
# ttl, was opened before close, or whose request raised is closed instead of reused. The
|
|
12
|
+
# connections of threads that have died are closed when a thread makes its first request.
|
|
13
|
+
# After a fork the child starts with none; the parent's are dropped without being closed,
|
|
14
|
+
# since closing a shared TLS socket would disturb the parent.
|
|
15
|
+
class ConnectionSet
|
|
16
|
+
MONOTONIC_CLOCK = -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) }
|
|
17
|
+
|
|
18
|
+
attr_reader :settings
|
|
19
|
+
|
|
20
|
+
def initialize(settings, clock: MONOTONIC_CLOCK)
|
|
21
|
+
@settings = settings
|
|
22
|
+
@clock = clock
|
|
23
|
+
reset
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Yields the current thread's Net::HTTP session and returns the block's result
|
|
27
|
+
def with_connection(uri, connection_options)
|
|
28
|
+
key = [uri.scheme, uri.hostname, uri.port, connection_options.dup.freeze].freeze
|
|
29
|
+
connection = checkout(key) || open_connection(uri, connection_options)
|
|
30
|
+
returned = false
|
|
31
|
+
begin
|
|
32
|
+
result = yield connection.http
|
|
33
|
+
checkin(key, connection)
|
|
34
|
+
returned = true
|
|
35
|
+
result
|
|
36
|
+
ensure
|
|
37
|
+
connection.close unless returned
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Closes idle connections now; connections in use are closed when their request ends
|
|
42
|
+
def close
|
|
43
|
+
idle = @mutex.synchronize do
|
|
44
|
+
@closed_at = now
|
|
45
|
+
remove_threads(@threads.keys)
|
|
46
|
+
end
|
|
47
|
+
idle.each(&:close)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Idle connections, across all threads
|
|
51
|
+
def size
|
|
52
|
+
@mutex.synchronize { @threads.each_value.sum(&:size) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def reset
|
|
58
|
+
@pid = Process.pid
|
|
59
|
+
@mutex = Mutex.new
|
|
60
|
+
@threads = {}
|
|
61
|
+
@closed_at = nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def forked?
|
|
65
|
+
@pid != Process.pid
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def now
|
|
69
|
+
@clock.call
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Returns the thread's connection for key, or nil when it has none to reuse. Retired
|
|
73
|
+
# connections are closed after the lock is released.
|
|
74
|
+
def checkout(key)
|
|
75
|
+
retired = []
|
|
76
|
+
reset if forked?
|
|
77
|
+
@mutex.synchronize do
|
|
78
|
+
retired.concat(remove_threads(dead_threads)) unless @threads.key?(Thread.current)
|
|
79
|
+
take(key, retired)
|
|
80
|
+
end
|
|
81
|
+
ensure
|
|
82
|
+
retired.each(&:close)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def take(key, retired)
|
|
86
|
+
connection = @threads[Thread.current]&.delete(key)
|
|
87
|
+
return connection unless connection && retire?(connection)
|
|
88
|
+
|
|
89
|
+
retired << connection
|
|
90
|
+
nil
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def open_connection(uri, connection_options)
|
|
94
|
+
ConnectionPools::Connection.open(uri.hostname, uri.port,
|
|
95
|
+
{ keep_alive_timeout: settings.idle_timeout }.merge(connection_options), now)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def checkin(key, connection)
|
|
99
|
+
connection.used(now)
|
|
100
|
+
kept = @mutex.synchronize { !retire?(connection) && keep?(key, connection) }
|
|
101
|
+
connection.close unless kept
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Keeps the connection for the thread's next request, unless a request that ran while
|
|
105
|
+
# this one did has already left one there
|
|
106
|
+
def keep?(key, connection)
|
|
107
|
+
connections = (@threads[Thread.current] ||= {})
|
|
108
|
+
return false if connections.key?(key)
|
|
109
|
+
|
|
110
|
+
connections[key] = connection
|
|
111
|
+
true
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def dead_threads
|
|
115
|
+
@threads.keys.reject(&:alive?)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Forgets the threads and returns their idle connections
|
|
119
|
+
def remove_threads(threads)
|
|
120
|
+
threads.flat_map { |thread| @threads.delete(thread).values }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def retire?(connection)
|
|
124
|
+
connection.expired?(settings.ttl, now) || closed_since_opened?(connection)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def closed_since_opened?(connection)
|
|
128
|
+
!@closed_at.nil? && connection.opened_at <= @closed_at
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ClientApiBuilder
|
|
4
|
+
module ThreadConnections
|
|
5
|
+
# ttl: seconds a connection may live before it's closed and replaced
|
|
6
|
+
# idle_timeout: seconds a connection may sit unused before Net::HTTP reconnects it
|
|
7
|
+
# (Net::HTTP's keep_alive_timeout; a connection option overrides it)
|
|
8
|
+
Settings = Data.define(:ttl, :idle_timeout) do
|
|
9
|
+
def initialize(ttl: 30, idle_timeout: 2)
|
|
10
|
+
{ ttl: ttl, idle_timeout: idle_timeout }.each do |name, value|
|
|
11
|
+
next if value.is_a?(Numeric) && value.positive?
|
|
12
|
+
|
|
13
|
+
raise ArgumentError, "#{name} must be a positive number of seconds, got #{value.inspect}"
|
|
14
|
+
end
|
|
15
|
+
super
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Purpose: opt-in persistent HTTP connections dedicated to each thread. Include after
|
|
4
|
+
# ClientApiBuilder::Router, instead of ClientApiBuilder::ConnectionPools:
|
|
5
|
+
#
|
|
6
|
+
# class MyClient
|
|
7
|
+
# include ClientApiBuilder::Router
|
|
8
|
+
# include ClientApiBuilder::ThreadConnections
|
|
9
|
+
#
|
|
10
|
+
# connection_per_thread ttl: 60
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Each thread opens its own connection per host on its first request and reuses it for its
|
|
14
|
+
# later ones, so there's no pool size to match to the thread count and no waiting for a
|
|
15
|
+
# connection to free up.
|
|
16
|
+
module ClientApiBuilder
|
|
17
|
+
module ThreadConnections
|
|
18
|
+
autoload :ConnectionSet, 'client_api_builder/thread_connections/connection_set'
|
|
19
|
+
autoload :Settings, 'client_api_builder/thread_connections/settings'
|
|
20
|
+
|
|
21
|
+
def self.included(base)
|
|
22
|
+
unless base.include?(::ClientApiBuilder::Router)
|
|
23
|
+
raise ArgumentError, 'include ClientApiBuilder::Router before ClientApiBuilder::ThreadConnections'
|
|
24
|
+
end
|
|
25
|
+
if base.include?(::ClientApiBuilder::ConnectionPools)
|
|
26
|
+
raise ArgumentError, 'include either ClientApiBuilder::ConnectionPools or ClientApiBuilder::ThreadConnections'
|
|
27
|
+
end
|
|
28
|
+
# HTTP2 falls back to these connections through super, so it must come after them
|
|
29
|
+
if base.ancestors.any? { |mod| mod.name == 'ClientApiBuilder::HTTP2' }
|
|
30
|
+
raise ArgumentError, 'include ClientApiBuilder::ThreadConnections before ClientApiBuilder::HTTP2'
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
base.extend ClassMethods
|
|
34
|
+
base.connection_per_thread
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module ClassMethods
|
|
38
|
+
# Configures this class's per-thread connections, replacing any it already had.
|
|
39
|
+
# Subclasses share their parent's unless they call connection_per_thread themselves.
|
|
40
|
+
def connection_per_thread(**settings)
|
|
41
|
+
redefine_class_method(:thread_connections, ConnectionSet.new(Settings.new(**settings)))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Closes every thread's idle connections now and in-use ones when their request ends,
|
|
45
|
+
# including those of sections that have their own
|
|
46
|
+
def close_connections
|
|
47
|
+
thread_connections.close
|
|
48
|
+
section_routers.each_value(&:close_connections)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def with_http_connection(uri, connection_options, &)
|
|
53
|
+
self.class.thread_connections.with_connection(uri, connection_options, &)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: client-api-builder
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.11.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Doug Youch
|
|
@@ -59,6 +59,9 @@ files:
|
|
|
59
59
|
- lib/client_api_builder/route_value_validator.rb
|
|
60
60
|
- lib/client_api_builder/router.rb
|
|
61
61
|
- lib/client_api_builder/section.rb
|
|
62
|
+
- lib/client_api_builder/thread_connections.rb
|
|
63
|
+
- lib/client_api_builder/thread_connections/connection_set.rb
|
|
64
|
+
- lib/client_api_builder/thread_connections/settings.rb
|
|
62
65
|
- lib/client_api_builder/version.rb
|
|
63
66
|
homepage: https://github.com/dougyouch/client-api-builder
|
|
64
67
|
licenses:
|