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
data/lib/restify/promise.rb
CHANGED
|
@@ -1,95 +1,229 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Restify
|
|
4
|
-
class Promise
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
#
|
|
4
|
+
class Promise
|
|
5
|
+
class AlreadyCompleteError < StandardError; end
|
|
6
|
+
|
|
8
7
|
# @api private
|
|
9
8
|
attr_writer :driver
|
|
10
9
|
|
|
10
|
+
# @api private
|
|
11
|
+
attr_reader :reason
|
|
12
|
+
|
|
11
13
|
def initialize(*dependencies, &task)
|
|
12
|
-
@
|
|
13
|
-
@
|
|
14
|
+
@mutex = Mutex.new
|
|
15
|
+
@condition = nil
|
|
16
|
+
@observers = nil
|
|
14
17
|
@driver = nil
|
|
18
|
+
@state = :pending
|
|
19
|
+
@value = nil
|
|
20
|
+
@reason = nil
|
|
21
|
+
@task = task
|
|
22
|
+
@dependencies = dependencies.empty? ? nil : dependencies.flatten
|
|
23
|
+
|
|
24
|
+
# When dependencies were passed in, but none are left after
|
|
25
|
+
# flattening, then we don't have to wait for explicit dependencies
|
|
26
|
+
# or resolution through a writer.
|
|
27
|
+
fulfill([]) if !@task && @dependencies&.empty?
|
|
28
|
+
end
|
|
15
29
|
|
|
16
|
-
|
|
30
|
+
def pending?
|
|
31
|
+
@state == :pending
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def fulfilled?
|
|
35
|
+
@state == :fulfilled
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def rejected?
|
|
39
|
+
@state == :rejected
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def complete?
|
|
43
|
+
@state == :fulfilled || @state == :rejected
|
|
44
|
+
end
|
|
17
45
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# through a writer.
|
|
21
|
-
complete true, [], nil if !@task && @dependencies.empty? && dependencies.any?
|
|
46
|
+
def incomplete?
|
|
47
|
+
!complete?
|
|
22
48
|
end
|
|
23
49
|
|
|
50
|
+
# Wait until the promise is complete.
|
|
51
|
+
#
|
|
52
|
+
# @param timeout [Numeric, Restify::Timeout, nil] Maximum seconds
|
|
53
|
+
# to wait, or `Restify::Timeout.default_timeout` if nil.
|
|
54
|
+
#
|
|
55
|
+
# @raise [Restify::Timeout::Error] When the timeout expired first.
|
|
56
|
+
#
|
|
57
|
+
# @return [self]
|
|
58
|
+
#
|
|
24
59
|
def wait(timeout = nil)
|
|
25
|
-
|
|
60
|
+
return self if complete?
|
|
26
61
|
|
|
27
|
-
|
|
62
|
+
timeout = Timeout.new(timeout, self)
|
|
28
63
|
|
|
29
64
|
# Let the driver run on the current thread instead of sleeping and
|
|
30
|
-
# switching to another thread if possible.
|
|
31
|
-
#
|
|
32
|
-
|
|
65
|
+
# switching to another thread if possible. The driver returns when
|
|
66
|
+
# the promise is complete, the timeout expired, or it cannot run
|
|
67
|
+
# here.
|
|
68
|
+
@driver&.drive(self, timeout)
|
|
69
|
+
|
|
70
|
+
until complete?
|
|
71
|
+
remaining = timeout.remaining
|
|
72
|
+
raise timeout unless remaining.positive?
|
|
33
73
|
|
|
34
|
-
|
|
74
|
+
run(timeout) if claim(remaining)
|
|
75
|
+
end
|
|
35
76
|
|
|
36
77
|
self
|
|
37
78
|
end
|
|
38
79
|
|
|
80
|
+
# Wait until the promise is complete, and return its value, or nil
|
|
81
|
+
# if it was rejected.
|
|
82
|
+
#
|
|
83
|
+
# @see #wait
|
|
84
|
+
#
|
|
85
|
+
def value(timeout = nil)
|
|
86
|
+
wait(timeout)
|
|
87
|
+
@value
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Wait until the promise is complete, and return its value, or raise
|
|
91
|
+
# the reason it was rejected with.
|
|
92
|
+
#
|
|
93
|
+
# @see #wait
|
|
94
|
+
#
|
|
95
|
+
def value!(timeout = nil)
|
|
96
|
+
wait(timeout)
|
|
97
|
+
raise @reason if rejected?
|
|
98
|
+
|
|
99
|
+
@value
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Return a new promise that will be fulfilled with the result of the
|
|
103
|
+
# given block once this promise is complete.
|
|
104
|
+
#
|
|
39
105
|
def then(&)
|
|
40
|
-
Promise.new(
|
|
106
|
+
Promise.new(self, &)
|
|
41
107
|
end
|
|
42
108
|
|
|
43
|
-
|
|
44
|
-
|
|
109
|
+
# @api private
|
|
110
|
+
#
|
|
111
|
+
def add_observer(&block)
|
|
112
|
+
complete = @mutex.synchronize do
|
|
113
|
+
(@observers ||= []) << block unless complete?
|
|
114
|
+
complete?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
yield(@value, @reason) if complete
|
|
118
|
+
|
|
119
|
+
self
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def fulfill(value)
|
|
125
|
+
complete(:fulfilled, value, nil)
|
|
45
126
|
end
|
|
46
127
|
|
|
47
|
-
|
|
128
|
+
def reject(reason)
|
|
129
|
+
complete(:rejected, nil, reason)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def complete(state, value, reason)
|
|
133
|
+
observers = @mutex.synchronize do
|
|
134
|
+
raise AlreadyCompleteError.new("Promise already #{@state}") if complete?
|
|
135
|
+
|
|
136
|
+
# Set the state last, as it is read without holding the lock:
|
|
137
|
+
@value = value
|
|
138
|
+
@reason = reason
|
|
139
|
+
@state = state
|
|
140
|
+
|
|
141
|
+
# Release everything not needed anymore:
|
|
142
|
+
@task = @dependencies = nil
|
|
48
143
|
|
|
49
|
-
|
|
50
|
-
def ns_execute(timeout = nil)
|
|
51
|
-
return unless compare_and_set_state(:processing, :pending)
|
|
52
|
-
return unless @task || @dependencies.any?
|
|
144
|
+
@condition&.broadcast
|
|
53
145
|
|
|
54
|
-
|
|
55
|
-
|
|
146
|
+
@observers.tap { @observers = nil }
|
|
147
|
+
end
|
|
56
148
|
|
|
57
|
-
|
|
149
|
+
observers&.each do |observer|
|
|
150
|
+
observer.call(value, reason)
|
|
151
|
+
rescue StandardError => e
|
|
152
|
+
Restify.logger&.error(self.class.name) { e }
|
|
153
|
+
end
|
|
58
154
|
|
|
59
|
-
|
|
155
|
+
self
|
|
60
156
|
end
|
|
61
157
|
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
|
|
158
|
+
# Claim running the task for the current thread, or sleep until the
|
|
159
|
+
# promise is complete, or can be claimed, e.g. because another
|
|
160
|
+
# thread gave up running it.
|
|
161
|
+
#
|
|
162
|
+
# The mutex is only held to claim, not while running the task. Other
|
|
163
|
+
# threads sleep on the condition variable with their own remaining
|
|
164
|
+
# time, whereas blocking on the mutex could not time out.
|
|
165
|
+
#
|
|
166
|
+
def claim(remaining)
|
|
167
|
+
@mutex.synchronize do
|
|
168
|
+
next false if complete?
|
|
169
|
+
|
|
170
|
+
if @state == :pending && (@task || @dependencies)
|
|
171
|
+
@state = :processing
|
|
172
|
+
next true
|
|
173
|
+
end
|
|
65
174
|
|
|
66
|
-
|
|
67
|
-
|
|
175
|
+
(@condition ||= ConditionVariable.new).wait(@mutex, remaining)
|
|
176
|
+
false
|
|
68
177
|
end
|
|
178
|
+
end
|
|
69
179
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
180
|
+
def run(timeout)
|
|
181
|
+
until complete?
|
|
182
|
+
if (rejected = @dependencies&.find {|d| d.wait(timeout).rejected? })
|
|
183
|
+
return reject(rejected.reason)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
args = @dependencies&.map(&:value)
|
|
187
|
+
|
|
188
|
+
begin
|
|
189
|
+
value = @task ? @task.call(*args) : args
|
|
190
|
+
rescue Exception => e # rubocop:disable Lint/RescueException
|
|
191
|
+
return reject(e)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
if value.is_a?(Promise)
|
|
195
|
+
@dependencies = [value]
|
|
196
|
+
@task = ->(v) { v }
|
|
197
|
+
else
|
|
198
|
+
fulfill(value)
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
ensure
|
|
202
|
+
# Give up the claim when the timeout expired, or the thread was
|
|
203
|
+
# killed. Another waiting thread can take over, and continue with
|
|
204
|
+
# the dependencies completed so far.
|
|
205
|
+
if @state == :processing
|
|
206
|
+
@mutex.synchronize do
|
|
207
|
+
@state = :pending
|
|
208
|
+
@condition&.broadcast
|
|
209
|
+
end
|
|
210
|
+
end
|
|
73
211
|
end
|
|
74
212
|
|
|
75
213
|
class << self
|
|
76
214
|
def create(driver: nil)
|
|
77
|
-
promise =
|
|
215
|
+
promise = new
|
|
78
216
|
promise.driver = driver
|
|
79
217
|
yield Writer.new(promise)
|
|
80
218
|
promise
|
|
81
219
|
end
|
|
82
220
|
|
|
83
221
|
def fulfilled(value)
|
|
84
|
-
|
|
85
|
-
writer.fulfill value
|
|
86
|
-
end
|
|
222
|
+
new.send(:fulfill, value)
|
|
87
223
|
end
|
|
88
224
|
|
|
89
|
-
def rejected(
|
|
90
|
-
|
|
91
|
-
writer.reject value
|
|
92
|
-
end
|
|
225
|
+
def rejected(reason)
|
|
226
|
+
new.send(:reject, reason)
|
|
93
227
|
end
|
|
94
228
|
end
|
|
95
229
|
|
|
@@ -99,11 +233,11 @@ module Restify
|
|
|
99
233
|
end
|
|
100
234
|
|
|
101
235
|
def fulfill(value)
|
|
102
|
-
@promise.send
|
|
236
|
+
@promise.send(:fulfill, value)
|
|
103
237
|
end
|
|
104
238
|
|
|
105
239
|
def reject(reason)
|
|
106
|
-
@promise.send
|
|
240
|
+
@promise.send(:reject, reason)
|
|
107
241
|
end
|
|
108
242
|
|
|
109
243
|
def set
|
data/lib/restify/relation.rb
CHANGED
|
@@ -55,13 +55,15 @@ module Restify
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def expand(params)
|
|
58
|
-
params = convert
|
|
59
|
-
variables = extract!
|
|
58
|
+
params = convert(params)
|
|
59
|
+
variables = extract!(params)
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
uri.query_values = (uri.query_values || {}).merge params if params.any?
|
|
61
|
+
return expand_static(params) if template.variables.empty?
|
|
63
62
|
|
|
64
|
-
|
|
63
|
+
uri = template.expand(variables)
|
|
64
|
+
uri.query_values = (uri.query_values || {}).merge(params) if params.any?
|
|
65
|
+
|
|
66
|
+
context.join(uri)
|
|
65
67
|
end
|
|
66
68
|
|
|
67
69
|
def pattern
|
|
@@ -74,6 +76,18 @@ module Restify
|
|
|
74
76
|
|
|
75
77
|
private
|
|
76
78
|
|
|
79
|
+
# A template without variables always expands to the same URI. It
|
|
80
|
+
# can be parsed and joined only once. The URI is frozen, as all
|
|
81
|
+
# requests share it.
|
|
82
|
+
def expand_static(params)
|
|
83
|
+
@static_uri ||= context.join(template.pattern).freeze
|
|
84
|
+
return @static_uri if params.empty?
|
|
85
|
+
|
|
86
|
+
@static_uri.dup.tap do |uri|
|
|
87
|
+
uri.query_values = (uri.query_values || {}).merge(params)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
77
91
|
def convert(params)
|
|
78
92
|
params.each_pair.with_object({}) do |param, hash|
|
|
79
93
|
hash[param[0]] = convert_param param[1]
|
data/lib/restify/timeout.rb
CHANGED
|
@@ -8,62 +8,44 @@ module Restify
|
|
|
8
8
|
attr_accessor :default_timeout
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
# Default wait timeout of 300 seconds.
|
|
12
11
|
self.default_timeout = 300
|
|
13
12
|
|
|
13
|
+
# @return [Float] Seconds to wait in total.
|
|
14
|
+
attr_reader :duration
|
|
15
|
+
|
|
14
16
|
def initialize(timeout, target = nil)
|
|
15
|
-
@timeout = parse_timeout(timeout)
|
|
16
17
|
@target = target
|
|
17
|
-
@
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
def wait_on!(ivar)
|
|
21
|
-
ivar.value!(wait_interval).tap do
|
|
22
|
-
raise self unless ivar.complete?
|
|
23
|
-
end
|
|
24
|
-
rescue ::Timeout::Error
|
|
25
|
-
raise self
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def timeout!
|
|
29
|
-
raise self if wait_interval <= 0
|
|
18
|
+
@duration = parse_timeout(timeout)
|
|
19
|
+
@deadline = now + @duration
|
|
30
20
|
end
|
|
31
21
|
|
|
32
22
|
def remaining
|
|
33
|
-
|
|
23
|
+
@deadline - now
|
|
34
24
|
end
|
|
35
25
|
|
|
36
26
|
def exception
|
|
37
|
-
Error.new(@target)
|
|
27
|
+
Error.new(@target, @duration)
|
|
38
28
|
end
|
|
39
29
|
|
|
40
30
|
private
|
|
41
31
|
|
|
42
|
-
def wait_interval
|
|
43
|
-
@deadline - now
|
|
44
|
-
end
|
|
45
|
-
|
|
46
32
|
def now
|
|
47
33
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
48
34
|
end
|
|
49
35
|
|
|
50
36
|
def parse_timeout(value)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
begin
|
|
54
|
-
value = Float(value)
|
|
55
|
-
rescue ArgumentError
|
|
56
|
-
raise ArgumentError.new \
|
|
57
|
-
"Timeout must be an number but is #{value}"
|
|
58
|
-
end
|
|
37
|
+
value = self.class.default_timeout if value.nil?
|
|
38
|
+
timeout = Float(value, exception: false)
|
|
59
39
|
|
|
60
|
-
raise ArgumentError.new
|
|
40
|
+
raise ArgumentError.new("Timeout must be a number but is #{value.inspect}") unless timeout
|
|
41
|
+
raise ArgumentError.new("Timeout must be > 0 but is #{value.inspect}.") unless timeout.positive?
|
|
42
|
+
raise ArgumentError.new("Timeout must be finite but is #{value.inspect}.") unless timeout.finite?
|
|
61
43
|
|
|
62
|
-
|
|
44
|
+
timeout
|
|
63
45
|
end
|
|
64
46
|
|
|
65
47
|
class << self
|
|
66
|
-
def new(timeout,
|
|
48
|
+
def new(timeout, target = nil)
|
|
67
49
|
return timeout if timeout.is_a?(self)
|
|
68
50
|
|
|
69
51
|
super
|
|
@@ -71,15 +53,16 @@ module Restify
|
|
|
71
53
|
end
|
|
72
54
|
|
|
73
55
|
class Error < ::Timeout::Error
|
|
74
|
-
attr_reader :target
|
|
56
|
+
attr_reader :target, :duration
|
|
75
57
|
|
|
76
|
-
def initialize(target)
|
|
58
|
+
def initialize(target, duration)
|
|
77
59
|
@target = target
|
|
60
|
+
@duration = duration
|
|
78
61
|
|
|
79
62
|
if @target
|
|
80
|
-
super("Operation on #{@target} timed out")
|
|
63
|
+
super("Operation on #{@target} timed out after #{duration}s")
|
|
81
64
|
else
|
|
82
|
-
super(
|
|
65
|
+
super("Operation timed out after #{duration}s")
|
|
83
66
|
end
|
|
84
67
|
end
|
|
85
68
|
end
|
data/lib/restify/version.rb
CHANGED
data/lib/restify/webmock.rb
CHANGED
|
@@ -73,7 +73,7 @@ module WebMock
|
|
|
73
73
|
yield.tap do |promise|
|
|
74
74
|
# Observers run when the request is complete, even when
|
|
75
75
|
# nobody waits on the promise.
|
|
76
|
-
promise.add_observer do |
|
|
76
|
+
promise.add_observer do |response, _|
|
|
77
77
|
next unless response
|
|
78
78
|
|
|
79
79
|
CallbackRegistry.invoke_callbacks(
|
data/lib/restify.rb
CHANGED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify::Adapter::Ethon::Easy do
|
|
6
|
+
let(:easy) { described_class.new }
|
|
7
|
+
|
|
8
|
+
describe '#response' do
|
|
9
|
+
subject(:response) { easy.response(request) }
|
|
10
|
+
|
|
11
|
+
let(:request) { Restify::Request.new(uri: 'http://example.org/base') }
|
|
12
|
+
|
|
13
|
+
# A transfer is handed back from libcurl with its return code and, for
|
|
14
|
+
# HTTP, the status code of the response.
|
|
15
|
+
let(:info) do
|
|
16
|
+
{
|
|
17
|
+
return_code: :ok,
|
|
18
|
+
response_code: 200,
|
|
19
|
+
effective_url: 'http://example.org/base',
|
|
20
|
+
response_headers: "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n",
|
|
21
|
+
response_body: '{}',
|
|
22
|
+
}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
before { allow(easy).to receive_messages(info) }
|
|
26
|
+
|
|
27
|
+
it 'returns the response' do
|
|
28
|
+
expect(response).to be_a Restify::Response
|
|
29
|
+
expect(response).to have_attributes(code: 200, body: '{}', request:)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it 'reuses the request URI without redirects' do
|
|
33
|
+
expect(response.uri).to be request.uri
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
context 'when redirected' do
|
|
37
|
+
let(:info) { super().merge(effective_url: 'http://example.org/other/base') }
|
|
38
|
+
|
|
39
|
+
it 'uses the effective URL as the response URI' do
|
|
40
|
+
expect(response.uri.to_s).to eq 'http://example.org/other/base'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
context 'when the transfer failed' do
|
|
45
|
+
let(:info) { super().merge(return_code: :couldnt_connect, response_code: 0) }
|
|
46
|
+
|
|
47
|
+
it { expect { response }.to raise_error Restify::NetworkError, /connect/i }
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Non-HTTP protocols are refused before a transfer is started, but
|
|
51
|
+
# libcurl still reports success with a zero status code whenever no
|
|
52
|
+
# HTTP status was received, and no status code at all when it cannot
|
|
53
|
+
# be read back. Neither has a response to process.
|
|
54
|
+
context 'without an HTTP status' do
|
|
55
|
+
let(:info) { super().merge(response_code: 0) }
|
|
56
|
+
|
|
57
|
+
it { expect { response }.to raise_error Restify::NetworkError, /without HTTP status/ }
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
context 'with an unavailable HTTP status' do
|
|
61
|
+
let(:info) { super().merge(response_code: nil) }
|
|
62
|
+
|
|
63
|
+
it { expect { response }.to raise_error Restify::NetworkError, /without HTTP status/ }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
describe 'headers' do
|
|
67
|
+
subject(:headers) { response.headers }
|
|
68
|
+
|
|
69
|
+
context 'with multiple values' do
|
|
70
|
+
let(:info) { super().merge(response_headers: "HTTP/1.1 200 OK\r\nLink: <a>\r\nLink: <b>\r\n\r\n") }
|
|
71
|
+
|
|
72
|
+
it { is_expected.to eq('LINK' => ['<a>', '<b>']) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
context 'with a folded value' do
|
|
76
|
+
let(:info) { super().merge(response_headers: "HTTP/1.1 200 OK\r\nX-Long: first\r\n\tsecond\r\n\r\n") }
|
|
77
|
+
|
|
78
|
+
it { is_expected.to eq('X_LONG' => 'first second') }
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
context 'with multiple responses, e.g. after a redirect' do
|
|
82
|
+
let(:info) do
|
|
83
|
+
super().merge(
|
|
84
|
+
response_headers: "HTTP/1.1 302 Found\r\nLocation: /b\r\n\r\n" \
|
|
85
|
+
"HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\n\r\n",
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
it { is_expected.to eq('CONTENT_TYPE' => 'text/plain') }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context 'without headers' do
|
|
93
|
+
let(:info) { super().merge(response_headers: nil) }
|
|
94
|
+
|
|
95
|
+
it { is_expected.to eq({}) }
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe '#reset' do
|
|
101
|
+
it 'forgets the previous request' do
|
|
102
|
+
easy._otel_span = Object.new
|
|
103
|
+
easy._restify_request = Object.new
|
|
104
|
+
easy._restify_writer = Object.new
|
|
105
|
+
|
|
106
|
+
easy.reset
|
|
107
|
+
|
|
108
|
+
expect([easy._otel_span, easy._restify_request, easy._restify_writer]).to all be_nil
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify::Adapter::Ethon::EventLoop do
|
|
6
|
+
subject(:events) { described_class.new }
|
|
7
|
+
|
|
8
|
+
let(:threads) { [] }
|
|
9
|
+
|
|
10
|
+
let(:options) do
|
|
11
|
+
Restify::Adapter::Ethon::Options.new(Restify::Adapter::Ethon::ENFORCED_OPTIONS)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
before do
|
|
15
|
+
stub_request(:get, 'http://stubserver/base').to_return(status: 200, body: '{}')
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
after { threads.each(&:kill).each(&:join) }
|
|
19
|
+
|
|
20
|
+
# A prepared easy handle with its promise, and a queue recording the
|
|
21
|
+
# thread completing it.
|
|
22
|
+
def transfer
|
|
23
|
+
request = Restify::Request.new(uri: 'http://localhost:9292/base')
|
|
24
|
+
writer = nil
|
|
25
|
+
promise = Restify::Promise.create {|w| writer = w }
|
|
26
|
+
completed_by = Queue.new
|
|
27
|
+
|
|
28
|
+
easy = Restify::Adapter::Ethon::Easy.new
|
|
29
|
+
easy._restify_request = request
|
|
30
|
+
easy._restify_writer = writer
|
|
31
|
+
easy.prepare(request, options)
|
|
32
|
+
easy.on_complete do |completed|
|
|
33
|
+
completed_by << Thread.current
|
|
34
|
+
writer.set { completed.response(request) }
|
|
35
|
+
events.notify
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
[easy, promise, completed_by]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
describe '#drive' do
|
|
42
|
+
it 'runs transfers in the calling thread' do
|
|
43
|
+
easy, promise, completed_by = transfer
|
|
44
|
+
events.enqueue(easy)
|
|
45
|
+
|
|
46
|
+
expect(events.drive(promise, Restify::Timeout.new(1))).to be true
|
|
47
|
+
expect(promise.value.code).to eq 200
|
|
48
|
+
expect(completed_by.pop(timeout: 1)).to eq Thread.current
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
it 'gives up when the timeout expires' do
|
|
52
|
+
expect(events.drive(Restify::Promise.new, Restify::Timeout.new(0.1))).to be true
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
it 'does not run the loop again from within the loop' do
|
|
56
|
+
events.instance_variable_get(:@lock).acquire(0.1)
|
|
57
|
+
|
|
58
|
+
expect(events.drive(Restify::Promise.new, Restify::Timeout.new(0.1))).to be false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe '#run' do
|
|
63
|
+
it 'completes transfers enqueued from other threads' do
|
|
64
|
+
background = Thread.new { events.run }
|
|
65
|
+
threads << background
|
|
66
|
+
|
|
67
|
+
easy, promise, completed_by = transfer
|
|
68
|
+
events.enqueue(easy)
|
|
69
|
+
|
|
70
|
+
expect(completed_by.pop(timeout: 1)).to eq background
|
|
71
|
+
expect(promise).to be_fulfilled
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '#forked!' do
|
|
76
|
+
it 'rejects pending transfers' do
|
|
77
|
+
easy, promise, = transfer
|
|
78
|
+
events.enqueue(easy)
|
|
79
|
+
|
|
80
|
+
events.forked!
|
|
81
|
+
|
|
82
|
+
expect(promise).to be_rejected
|
|
83
|
+
expect(promise.reason).to be_a Restify::NetworkError
|
|
84
|
+
expect(promise.reason.message).to include 'Request started before fork'
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it 'does not release pending handles' do
|
|
88
|
+
easy, = transfer
|
|
89
|
+
events.enqueue(easy)
|
|
90
|
+
|
|
91
|
+
events.forked!
|
|
92
|
+
|
|
93
|
+
expect(easy.handle.autorelease?).to be false
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|