restify 3.0.0.rc2 → 3.0.0.rc3
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 +27 -0
- data/README.md +0 -1
- data/lib/restify/adapter/ethon/easy.rb +147 -0
- data/lib/restify/adapter/ethon/event_loop.rb +255 -0
- data/lib/restify/adapter/ethon/options.rb +81 -0
- data/lib/restify/adapter/ethon/pool.rb +43 -0
- data/lib/restify/adapter/ethon.rb +56 -307
- data/lib/restify/adapter/telemetry.rb +1 -1
- data/lib/restify/adapter/typhoeus.rb +5 -1
- data/lib/restify/context.rb +11 -1
- data/lib/restify/processors/base/parsing.rb +30 -25
- data/lib/restify/promise.rb +181 -47
- data/lib/restify/relation.rb +19 -5
- data/lib/restify/timeout.rb +19 -36
- data/lib/restify/version.rb +1 -1
- data/lib/restify/webmock.rb +1 -1
- data/lib/restify.rb +0 -1
- data/spec/restify/adapter/ethon/easy_spec.rb +111 -0
- data/spec/restify/adapter/ethon/event_loop_spec.rb +96 -0
- data/spec/restify/adapter/ethon/options_spec.rb +104 -0
- data/spec/restify/adapter/ethon/pool_spec.rb +81 -0
- data/spec/restify/adapter/ethon_spec.rb +111 -67
- data/spec/restify/adapter/loop_lock_spec.rb +6 -6
- data/spec/restify/context_spec.rb +14 -0
- data/spec/restify/features/opentelemetry_spec.rb +2 -0
- data/spec/restify/processors/json_spec.rb +67 -0
- data/spec/restify/promise_spec.rb +218 -21
- data/spec/restify/relation_spec.rb +23 -0
- data/spec/restify/timeout_spec.rb +38 -39
- metadata +9 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d8fa974df7fe0cd06afd87ffe8bef89d87559ece784a801070aa0b8342b883eb
|
|
4
|
+
data.tar.gz: 1f3cc52e1b4bf7995f50dbe5ef9360986d71c7d157b47b1527f79cca2ff66179
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cdc59e5ca15e38eb8224b638e675807f669cf0ee00802427334238107ce8a5cddc16b71cbec12e0013055fe5431b38033509381f4c1a1727e07c38cfed1be4fc
|
|
7
|
+
data.tar.gz: 7da86c8fb8dbf2829da84e9f3e0c5d47d226e360a35cc7b6b33e1da02d7507ef4570d703d31c3ef7448fb3586592218d5924b926e70d1694abfdb916de6e1c4f
|
data/CHANGELOG.md
CHANGED
|
@@ -17,6 +17,33 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
|
|
|
17
17
|
|
|
18
18
|
### Breaks
|
|
19
19
|
|
|
20
|
+
## 3.0.0-rc3 - (2026-09-27)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
### Changes
|
|
25
|
+
|
|
26
|
+
- Perf: Avoid parsing/joining URIs when not necessary, e.g., for relations without variables or absolute URIs.
|
|
27
|
+
- Perf: Reuse libcurl handles in the Ethon adapter
|
|
28
|
+
- Perf: Optimize libcurl option preparation in the Ethon adapter
|
|
29
|
+
- Perf: Optimize parsing of data and relations
|
|
30
|
+
- Perf: Replace `Concurrent::IVar` with a lightweight promise implementation
|
|
31
|
+
- Remove `concurrent-ruby` dependency
|
|
32
|
+
|
|
33
|
+
### Fixes
|
|
34
|
+
|
|
35
|
+
- Apply `Restify::Timeout.default_timeout` when waiting on a promise without
|
|
36
|
+
an explicit timeout, e.g. with the Typhoeus adapter, instead of waiting forever
|
|
37
|
+
- A timeout while waiting on a promise does not reject the promise anymore.
|
|
38
|
+
Waiting again, e.g. with a longer timeout, continues where the last wait gave up.
|
|
39
|
+
- Honor the timeout of all threads waiting on the same promise, not only of the
|
|
40
|
+
thread running its `#then` blocks
|
|
41
|
+
- Only create relations from `*_url` fields with string values, e.g. not from numbers, objects, or arrays
|
|
42
|
+
|
|
43
|
+
### Breaks
|
|
44
|
+
|
|
45
|
+
- `Restify::Promise` does not inherit from `Concurrent::IVar` anymore
|
|
46
|
+
|
|
20
47
|
## 3.0.0.rc2 - (2026-09-25)
|
|
21
48
|
|
|
22
49
|
---
|
data/README.md
CHANGED
|
@@ -11,7 +11,6 @@ Restify can be used to consume hypermedia REST APIs (like GitHubs), to build a s
|
|
|
11
11
|
|
|
12
12
|
Restify is build upon the following libraries:
|
|
13
13
|
|
|
14
|
-
* [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby)
|
|
15
14
|
* [addressable](https://github.com/sporkmonger/addressable)
|
|
16
15
|
* [ethon](https://github.com/typhoeus/ethon), using libcurl
|
|
17
16
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Restify
|
|
4
|
+
module Adapter
|
|
5
|
+
class Ethon < Base
|
|
6
|
+
#
|
|
7
|
+
# A libcurl easy handle for Restify requests, translating a
|
|
8
|
+
# request into libcurl options, and a completed transfer back into
|
|
9
|
+
# a response.
|
|
10
|
+
#
|
|
11
|
+
# Handles are reused for many requests, see `Pool`.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
#
|
|
15
|
+
class Easy < ::Ethon::Easy
|
|
16
|
+
attr_accessor :_otel_span, :_restify_request, :_restify_writer
|
|
17
|
+
|
|
18
|
+
TIMEOUT_MS = Options.number(:timeout_ms)
|
|
19
|
+
CONNECTTIMEOUT_MS = Options.number(:connecttimeout_ms)
|
|
20
|
+
|
|
21
|
+
def reset
|
|
22
|
+
@_otel_span = @_restify_request = @_restify_writer = nil
|
|
23
|
+
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def prepare(request, options)
|
|
28
|
+
options.apply(self)
|
|
29
|
+
|
|
30
|
+
self.url = request.uri.to_s
|
|
31
|
+
self.headers = DEFAULT_HEADERS.merge(request.headers)
|
|
32
|
+
|
|
33
|
+
if request.timeout
|
|
34
|
+
# libcurl requires millisecond-based timeouts so fractional
|
|
35
|
+
# seconds must be converted and rounded.
|
|
36
|
+
timeout = (request.timeout * 1000).ceil
|
|
37
|
+
::Ethon::Curl.easy_setopt(handle, TIMEOUT_MS, :long, timeout)
|
|
38
|
+
::Ethon::Curl.easy_setopt(handle, CONNECTTIMEOUT_MS, :long, timeout)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
prepare_method(request.method, request.body)
|
|
42
|
+
|
|
43
|
+
# The OpenTelemetry instrumentation for Ethon takes the method
|
|
44
|
+
# from `#http_request`, which is not used here.
|
|
45
|
+
@otel_method = request.method
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def response(request)
|
|
49
|
+
code = return_code
|
|
50
|
+
if code != :ok
|
|
51
|
+
raise Restify::NetworkError.new(request, ::Ethon::Curl.easy_strerror(code))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Non-HTTP protocols are refused before a transfer is started,
|
|
55
|
+
# but libcurl still reports success with a zero status code
|
|
56
|
+
# whenever no HTTP status was received, and no status code at
|
|
57
|
+
# all when it cannot be read back.
|
|
58
|
+
status = response_code
|
|
59
|
+
if status.nil? || status.zero?
|
|
60
|
+
raise Restify::NetworkError.new(request, 'Response without HTTP status')
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
::Restify::Response.new(
|
|
64
|
+
request,
|
|
65
|
+
effective_uri(request),
|
|
66
|
+
status,
|
|
67
|
+
parse_headers(response_headers),
|
|
68
|
+
response_body,
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
# Like Ethon's actions for each method, see `Ethon::Easy::Http`.
|
|
75
|
+
# An empty body counts as no body.
|
|
76
|
+
def prepare_method(method, body)
|
|
77
|
+
body = nil if body.nil? || body.empty?
|
|
78
|
+
|
|
79
|
+
case method
|
|
80
|
+
when 'get'
|
|
81
|
+
if body
|
|
82
|
+
postfields(body)
|
|
83
|
+
self.customrequest = 'GET'
|
|
84
|
+
end
|
|
85
|
+
when 'post'
|
|
86
|
+
postfields(body || '')
|
|
87
|
+
when 'put'
|
|
88
|
+
self.upload = true
|
|
89
|
+
self.infilesize = body ? body.bytesize : 0
|
|
90
|
+
set_read_callback(body) if body
|
|
91
|
+
when 'head'
|
|
92
|
+
postfields(body) if body
|
|
93
|
+
self.nobody = true
|
|
94
|
+
else
|
|
95
|
+
postfields(body) if body
|
|
96
|
+
self.customrequest = method.upcase
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def postfields(body)
|
|
101
|
+
self.postfieldsize = body.bytesize
|
|
102
|
+
self.copypostfields = body
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def effective_uri(request)
|
|
106
|
+
url = effective_url
|
|
107
|
+
|
|
108
|
+
# Only parse the URL after redirects, as parsing is expensive.
|
|
109
|
+
return request.uri if url.nil? || url == request.uri.to_s
|
|
110
|
+
|
|
111
|
+
Addressable::URI.parse(url)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_headers(raw)
|
|
115
|
+
headers = {}
|
|
116
|
+
return headers if raw.nil?
|
|
117
|
+
|
|
118
|
+
# The raw headers can contain multiple blocks, e.g. from
|
|
119
|
+
# informational responses or when following redirects; only
|
|
120
|
+
# use the latest ones:
|
|
121
|
+
block = raw.split(/\r?\n\r?\n/).reject {|b| b.strip.empty? }.last
|
|
122
|
+
|
|
123
|
+
# Split on newlines that are not followed by whitespace to
|
|
124
|
+
# keep folded header values together.
|
|
125
|
+
block.to_s.split(/\r?\n(?!\s)/).each do |line|
|
|
126
|
+
line = line.strip
|
|
127
|
+
next if line.empty? || line.start_with?('HTTP/')
|
|
128
|
+
|
|
129
|
+
key, value = line.split(':', 2)
|
|
130
|
+
next if value.nil?
|
|
131
|
+
|
|
132
|
+
key = key.strip.upcase.tr('-', '_')
|
|
133
|
+
value = value.strip.gsub(/\r?\n\s*/, ' ')
|
|
134
|
+
|
|
135
|
+
case (current = headers[key])
|
|
136
|
+
when nil then headers[key] = value
|
|
137
|
+
when Array then current << value
|
|
138
|
+
else headers[key] = [current, value]
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
headers
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Restify
|
|
4
|
+
module Adapter
|
|
5
|
+
class Ethon < Base
|
|
6
|
+
#
|
|
7
|
+
# An event loop running libcurl's multi interface in
|
|
8
|
+
# `socket_action` mode, waiting for sockets with nio4r.
|
|
9
|
+
#
|
|
10
|
+
# Can woken up to add new easy handles immediately but can run
|
|
11
|
+
# from any thread. `LoopLock` ensures all interaction with libcurl
|
|
12
|
+
# is still serialized and threads are correctly prioritized.
|
|
13
|
+
#
|
|
14
|
+
# @api private
|
|
15
|
+
#
|
|
16
|
+
class EventLoop
|
|
17
|
+
include Logging
|
|
18
|
+
|
|
19
|
+
INTERESTS = {
|
|
20
|
+
none: nil,
|
|
21
|
+
in: :r,
|
|
22
|
+
out: :w,
|
|
23
|
+
inout: :rw,
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
26
|
+
def initialize(**multi)
|
|
27
|
+
@selector = NIO::Selector.new
|
|
28
|
+
@monitors = {}
|
|
29
|
+
@timer = nil
|
|
30
|
+
@queue = Queue.new
|
|
31
|
+
|
|
32
|
+
# libcurl only stores the function pointers, therefore the procs
|
|
33
|
+
# must be referenced here too, or they would be garbage
|
|
34
|
+
# collected.
|
|
35
|
+
@socketfunction = method(:on_socket).to_proc
|
|
36
|
+
@timerfunction = method(:on_timer).to_proc
|
|
37
|
+
|
|
38
|
+
@multi = ::Ethon::Multi.new(execution_mode: :socket_action, **multi)
|
|
39
|
+
@multi.socketfunction = @socketfunction
|
|
40
|
+
@multi.timerfunction = @timerfunction
|
|
41
|
+
|
|
42
|
+
# Wake up the background thread from `select` when a waiting
|
|
43
|
+
# thread wants to run the loop.
|
|
44
|
+
@lock = LoopLock.new { @selector.wakeup }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def enqueue(easy)
|
|
48
|
+
@queue << easy
|
|
49
|
+
@selector.wakeup
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Wake up threads waiting for the loop, e.g. after completing a
|
|
53
|
+
# transfer, to check if their result is available now.
|
|
54
|
+
def notify
|
|
55
|
+
@lock.notify
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Run the loop in the calling thread until the promise is
|
|
59
|
+
# complete or the timeout expires.
|
|
60
|
+
#
|
|
61
|
+
# Several threads can wait at the same time, but only one runs
|
|
62
|
+
# the loop. The others sleep until either their promise
|
|
63
|
+
# completes or the loop is released.
|
|
64
|
+
#
|
|
65
|
+
# @return [Boolean] false when the loop cannot be run in the
|
|
66
|
+
# calling thread, i.e. when waiting from within the loop
|
|
67
|
+
# itself.
|
|
68
|
+
#
|
|
69
|
+
def drive(promise, timeout) # rubocop:disable Naming/PredicateMethod
|
|
70
|
+
return false if @lock.owned?
|
|
71
|
+
|
|
72
|
+
while @lock.acquire(timeout) { promise.complete? }
|
|
73
|
+
begin
|
|
74
|
+
step(timeout.remaining) until promise.complete? || !timeout.remaining.positive?
|
|
75
|
+
rescue StandardError => e
|
|
76
|
+
error(e)
|
|
77
|
+
ensure
|
|
78
|
+
@lock.release
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Run the loop whenever no thread waits for it. Blocks forever,
|
|
86
|
+
# as meant for a background thread.
|
|
87
|
+
def run
|
|
88
|
+
loop do
|
|
89
|
+
@lock.acquire_background
|
|
90
|
+
|
|
91
|
+
begin
|
|
92
|
+
step
|
|
93
|
+
ensure
|
|
94
|
+
@lock.release
|
|
95
|
+
end
|
|
96
|
+
rescue StandardError => e
|
|
97
|
+
error(e)
|
|
98
|
+
end
|
|
99
|
+
ensure
|
|
100
|
+
debug 'loop:exit'
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Forget the loop in a forked child process, without releasing
|
|
104
|
+
# its libcurl handles, as they belong to the parent process.
|
|
105
|
+
# Transfers not completed yet are rejected, as they were started
|
|
106
|
+
# by the parent process too.
|
|
107
|
+
#
|
|
108
|
+
def forked!
|
|
109
|
+
pending = @multi.easy_handles.dup
|
|
110
|
+
pending << @queue.pop(true) until @queue.empty?
|
|
111
|
+
|
|
112
|
+
@multi.handle.autorelease = false
|
|
113
|
+
|
|
114
|
+
pending.each do |easy|
|
|
115
|
+
easy.handle.autorelease = false
|
|
116
|
+
easy._restify_writer.reject(
|
|
117
|
+
Restify::NetworkError.new(
|
|
118
|
+
easy._restify_request,
|
|
119
|
+
'Request started before fork',
|
|
120
|
+
),
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def step(limit = nil)
|
|
130
|
+
dequeue_all
|
|
131
|
+
|
|
132
|
+
# libcurl needs to be notified about its own timeouts, e.g. to
|
|
133
|
+
# start newly added transfers or to time out stalled ones.
|
|
134
|
+
timeout!
|
|
135
|
+
|
|
136
|
+
timeout = select_timeout
|
|
137
|
+
timeout = limit if limit && (timeout.nil? || timeout > limit)
|
|
138
|
+
debug 'loop:select', timeout: timeout
|
|
139
|
+
|
|
140
|
+
# nil on timeout; empty array when woken up
|
|
141
|
+
@selector.select(timeout)&.each do |monitor|
|
|
142
|
+
# libcurl can remove sockets while earlier events of the
|
|
143
|
+
# same batch are processed, e.g. when a completed transfer
|
|
144
|
+
# tears down other connections. Skip monitors that are gone
|
|
145
|
+
# by now.
|
|
146
|
+
next unless @monitors[monitor.value].equal?(monitor)
|
|
147
|
+
|
|
148
|
+
socket_action(monitor.value, readiness(monitor))
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def dequeue_all
|
|
153
|
+
loop do
|
|
154
|
+
easy = begin
|
|
155
|
+
@queue.pop(true)
|
|
156
|
+
rescue ThreadError
|
|
157
|
+
break
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
add(easy)
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def add(easy)
|
|
165
|
+
# Restore the OpenTelemetry span the request originated from,
|
|
166
|
+
# so that the Ethon instrumentation picks up the correct
|
|
167
|
+
# parent when the easy handle is added to libcurl.
|
|
168
|
+
#
|
|
169
|
+
# Handle exceptions from Ethon or WebMock too, and reject the
|
|
170
|
+
# promise, so that the errors can be handled in user code.
|
|
171
|
+
OpenTelemetry::Trace.with_span(easy._otel_span) do
|
|
172
|
+
@multi.add(easy)
|
|
173
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
174
|
+
easy._restify_writer.reject(e)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Seconds until libcurl wants to be called again, or nil to
|
|
179
|
+
# block until a socket becomes ready or the loop is woken up.
|
|
180
|
+
def select_timeout
|
|
181
|
+
return nil unless @timer
|
|
182
|
+
|
|
183
|
+
[@timer - now, 0].max
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def timeout!
|
|
187
|
+
return unless @timer
|
|
188
|
+
return if now < @timer
|
|
189
|
+
|
|
190
|
+
# Reset before invoking libcurl, as it will set a new timeout
|
|
191
|
+
# from within the socket action.
|
|
192
|
+
@timer = nil
|
|
193
|
+
|
|
194
|
+
socket_action
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def readiness(monitor)
|
|
198
|
+
readiness = []
|
|
199
|
+
readiness << :in if monitor.readable?
|
|
200
|
+
readiness << :out if monitor.writable?
|
|
201
|
+
readiness
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def socket_action(socket = nil, readiness = 0)
|
|
205
|
+
@multi.socket_action(socket, readiness)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# libcurl: called when asking for readiness monitoring
|
|
209
|
+
def on_socket(_easy, socket, what, _userp, _socketp)
|
|
210
|
+
debug 'socket:action', tag: socket, what: what
|
|
211
|
+
|
|
212
|
+
if what == :remove
|
|
213
|
+
@monitors.delete(socket)&.close
|
|
214
|
+
else
|
|
215
|
+
monitor = (@monitors[socket] ||= register(socket))
|
|
216
|
+
monitor.interests = INTERESTS.fetch(what)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
:ok
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
error(e)
|
|
222
|
+
:ok
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# libcurl: called on timeout changes
|
|
226
|
+
def on_timer(_handle, timeout_ms, _userp)
|
|
227
|
+
debug 'timer:set', timeout: timeout_ms
|
|
228
|
+
@timer = timeout_ms.negative? ? nil : now + (timeout_ms / 1000.0)
|
|
229
|
+
|
|
230
|
+
:ok
|
|
231
|
+
rescue StandardError => e
|
|
232
|
+
error(e)
|
|
233
|
+
:ok
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def register(socket)
|
|
237
|
+
# FD owned by libcurl: IO/ruby must never close it
|
|
238
|
+
io = IO.for_fd(socket, autoclose: false)
|
|
239
|
+
|
|
240
|
+
@selector.register(io, :r).tap do |monitor|
|
|
241
|
+
monitor.value = socket
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def now
|
|
246
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def _log_prefix
|
|
250
|
+
"[#{object_id}/#{Thread.current.object_id}]"
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Restify
|
|
4
|
+
module Adapter
|
|
5
|
+
class Ethon < Base
|
|
6
|
+
#
|
|
7
|
+
# libcurl options, converted to the arguments for
|
|
8
|
+
# `curl_easy_setopt` once for performance reasons.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
#
|
|
12
|
+
class Options
|
|
13
|
+
DEFINITIONS = ::Ethon::Curl.easy_options(nil)
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def number(name)
|
|
17
|
+
definition(name).fetch(:opt)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def definition(name)
|
|
21
|
+
DEFINITIONS.fetch(name) do
|
|
22
|
+
raise ArgumentError.new("Unknown libcurl option: #{name}")
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Options by name, e.g. `followlocation: true`, nil values are
|
|
28
|
+
# skipped.
|
|
29
|
+
#
|
|
30
|
+
def initialize(options)
|
|
31
|
+
@options = options.filter_map do |name, value|
|
|
32
|
+
definition = self.class.definition(name)
|
|
33
|
+
next if value.nil?
|
|
34
|
+
|
|
35
|
+
compile(name, definition, value)
|
|
36
|
+
end.freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def apply(easy)
|
|
40
|
+
handle = easy.handle
|
|
41
|
+
|
|
42
|
+
@options.each do |opt, type, value|
|
|
43
|
+
if type
|
|
44
|
+
::Ethon::Curl.easy_setopt(handle, opt, type, value)
|
|
45
|
+
else
|
|
46
|
+
::Ethon::Curl.set_option(opt, value, handle)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def to_a
|
|
52
|
+
@options
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
# Like `Ethon::Curls::Options#set_option` for the supported types.
|
|
58
|
+
def compile(name, definition, value)
|
|
59
|
+
opt = definition[:opt]
|
|
60
|
+
|
|
61
|
+
case definition[:type]
|
|
62
|
+
when :bool then [opt, :long, value && value != 0 ? 1 : 0]
|
|
63
|
+
when :int, :time then [opt, :long, Integer(value)]
|
|
64
|
+
when :enum then [opt, :long, lookup(name, definition, value)]
|
|
65
|
+
when :bitmask then [opt, :long, Array(value).reduce(0) {|mask, v| mask | lookup(name, definition, v) }]
|
|
66
|
+
when :string then [opt, :string, value.to_s]
|
|
67
|
+
else [name, nil, value]
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def lookup(name, definition, value)
|
|
72
|
+
return Integer(value) unless value.is_a?(Symbol) || value.is_a?(String)
|
|
73
|
+
|
|
74
|
+
definition[:opts].fetch(value.to_sym) do
|
|
75
|
+
raise ArgumentError.new("Unknown value for libcurl option #{name}: #{value.inspect}")
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Restify
|
|
4
|
+
module Adapter
|
|
5
|
+
class Ethon < Base
|
|
6
|
+
#
|
|
7
|
+
# Idle libcurl easy handles, kept for reuse.
|
|
8
|
+
#
|
|
9
|
+
# Handles can be taken and released from any thread. A handle must
|
|
10
|
+
# only be released once libcurl is done with it.
|
|
11
|
+
#
|
|
12
|
+
# @api private
|
|
13
|
+
#
|
|
14
|
+
class Pool
|
|
15
|
+
def initialize(size:, &factory)
|
|
16
|
+
@size = size
|
|
17
|
+
@factory = factory
|
|
18
|
+
@mutex = Mutex.new
|
|
19
|
+
@idle = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def checkout
|
|
23
|
+
@mutex.synchronize { @idle.pop } || @factory.call
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def release(easy)
|
|
27
|
+
easy.reset
|
|
28
|
+
|
|
29
|
+
@mutex.synchronize { @idle << easy if @idle.size < @size }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Forget all handles without releasing them as they belong to
|
|
33
|
+
# the parent process.
|
|
34
|
+
def forked!
|
|
35
|
+
@mutex.synchronize do
|
|
36
|
+
@idle.each {|easy| easy.handle.autorelease = false }
|
|
37
|
+
@idle.clear
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|