restify 1.1.0 → 1.2.1
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 +9 -0
- data/README.md +2 -5
- data/lib/restify.rb +4 -0
- data/lib/restify/adapter/pooled_em.rb +273 -0
- data/lib/restify/context.rb +25 -1
- data/lib/restify/global.rb +10 -0
- data/lib/restify/railtie.rb +11 -0
- data/lib/restify/response.rb +1 -1
- data/lib/restify/version.rb +2 -2
- data/spec/restify/context_spec.rb +66 -0
- data/spec/spec_helper.rb +8 -1
- metadata +8 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6cb4e54249171cc430b42e94584d53a8d855dc27
|
4
|
+
data.tar.gz: 021d1ef66172c7a992f7f447ffbe0ac2770421d9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d32a9c6b8978674f26a021f5e29618fe3c75012144697431f79033db6f4a503c0d7de39803dedb20c691c0d3feaa0965a9382de598cf9741d7c9bcb33e63292e
|
7
|
+
data.tar.gz: 8b699331f7a597f3b99ead0a5fe9da59bdec2648244a802798c952cdac430d729a1297e91807657723ed43fd35a04c3b52d95ec8abf93a1364fd2a2f99140af8
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -26,12 +26,9 @@ Included processors can handle:
|
|
26
26
|
|
27
27
|
Restify requires Ruby 2.0+.
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
* It is build on experimental obligation library.
|
32
|
-
|
33
|
-
Planned features:
|
29
|
+
### Planned features
|
34
30
|
|
31
|
+
* HTTP cache
|
35
32
|
* API versions via header
|
36
33
|
* Content-Type and Language negotiation
|
37
34
|
* Processors for MessagePack, JSON-HAL, etc.
|
data/lib/restify.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'forwardable'
|
4
|
+
|
3
5
|
require 'restify/version'
|
4
6
|
|
5
7
|
require 'hashie'
|
@@ -32,6 +34,8 @@ module Restify
|
|
32
34
|
require 'restify/processors/json'
|
33
35
|
end
|
34
36
|
|
37
|
+
require 'restify/railtie' if defined?(Rails::Railtie)
|
38
|
+
|
35
39
|
PROCESSORS = [Processors::Json].freeze
|
36
40
|
|
37
41
|
extend Global
|
@@ -0,0 +1,273 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'eventmachine'
|
4
|
+
require 'em-http-request'
|
5
|
+
|
6
|
+
module Restify
|
7
|
+
module Adapter
|
8
|
+
class PooledEM < Base
|
9
|
+
# rubocop:disable RedundantFreeze
|
10
|
+
LOG_PROGNAME = 'restify.adapter.pooled-em'.freeze
|
11
|
+
|
12
|
+
# This class maintains a pool of connection objects, grouped by origin,
|
13
|
+
# and ensures limits for total parallel requests and per-origin requests.
|
14
|
+
#
|
15
|
+
# It does so by maintaining a list of already open, reusable connections.
|
16
|
+
# When any of them are checked out for usage, it counts the usages to
|
17
|
+
# prevent constraints being broken.
|
18
|
+
class Pool
|
19
|
+
def initialize(size: 32, per_host: 6, connect_timeout: 2, inactivity_timeout: 10)
|
20
|
+
@size = size
|
21
|
+
@per_host = per_host
|
22
|
+
@connect_timeout = connect_timeout
|
23
|
+
@inactivity_timeout = inactivity_timeout
|
24
|
+
|
25
|
+
@host = Hash.new {|h, k| h[k] = 0 }
|
26
|
+
@available = []
|
27
|
+
@queue = []
|
28
|
+
@used = 0
|
29
|
+
end
|
30
|
+
|
31
|
+
# Request a connection from the pool.
|
32
|
+
#
|
33
|
+
# Attempts to checkout a reusable connection from the pool (or create a
|
34
|
+
# new one). If any of the limits have been reached, the request will be
|
35
|
+
# put onto a queue until other connections are released.
|
36
|
+
#
|
37
|
+
# Returns a Deferrable that succeeds with a connection instance once a
|
38
|
+
# connection has been checked out (usually immediately).
|
39
|
+
#
|
40
|
+
# @return [Deferrable<Request>]
|
41
|
+
#
|
42
|
+
def get(request, timeout: 2)
|
43
|
+
defer = Deferrable.new(request)
|
44
|
+
defer.timeout(timeout, :timeout)
|
45
|
+
defer.errback { @queue.delete(defer) }
|
46
|
+
|
47
|
+
checkout(defer)
|
48
|
+
|
49
|
+
defer
|
50
|
+
end
|
51
|
+
|
52
|
+
# Return a connection to the pool.
|
53
|
+
#
|
54
|
+
# If there are requests in the queue (due to one of the limits having
|
55
|
+
# been reached), they will be given an attempt to use the released
|
56
|
+
# connection.
|
57
|
+
#
|
58
|
+
# If no requests are queued, the connection will be held for reuse by a
|
59
|
+
# subsequent request.
|
60
|
+
#
|
61
|
+
# @return [void]
|
62
|
+
#
|
63
|
+
def release(conn)
|
64
|
+
@available.unshift(conn) if @available.size < @size
|
65
|
+
@used -= 1 if @used > 0
|
66
|
+
|
67
|
+
Restify.logger.debug(LOG_PROGNAME) do
|
68
|
+
"[#{conn.uri}] Released to pool (#{@available.size}/#{@used}/#{size})"
|
69
|
+
end
|
70
|
+
|
71
|
+
checkout(@queue.shift) if @queue.any? # checkout next waiting defer
|
72
|
+
end
|
73
|
+
|
74
|
+
alias << release
|
75
|
+
|
76
|
+
def remove(conn)
|
77
|
+
close(conn)
|
78
|
+
|
79
|
+
Restify.logger.debug(LOG_PROGNAME) do
|
80
|
+
"[#{conn.uri}] Removed from pool (#{@available.size}/#{@used}/#{size})"
|
81
|
+
end
|
82
|
+
|
83
|
+
checkout(@queue.shift) if @queue.any? # checkout next waiting defer
|
84
|
+
end
|
85
|
+
|
86
|
+
# Determine the number of connections in the pool.
|
87
|
+
#
|
88
|
+
# This takes into account both reusable (idle) and used connections.
|
89
|
+
#
|
90
|
+
# @return [Integer]
|
91
|
+
#
|
92
|
+
def size
|
93
|
+
@available.size + @used
|
94
|
+
end
|
95
|
+
|
96
|
+
private
|
97
|
+
|
98
|
+
def close(conn)
|
99
|
+
@used -= 1 if @used > 0
|
100
|
+
@host[conn.uri.to_s] -= 1
|
101
|
+
|
102
|
+
conn.close
|
103
|
+
end
|
104
|
+
|
105
|
+
def checkout(defer)
|
106
|
+
origin = defer.request.uri.origin
|
107
|
+
|
108
|
+
if (index = find_reusable_connection(origin))
|
109
|
+
defer.succeed reuse_connection(index, origin)
|
110
|
+
elsif can_build_new_connection?(origin)
|
111
|
+
defer.succeed new_connection(origin)
|
112
|
+
else
|
113
|
+
queue defer
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def find_reusable_connection(origin)
|
118
|
+
@available.find_index {|conn| conn.uri == origin }
|
119
|
+
end
|
120
|
+
|
121
|
+
def reuse_connection(index, origin)
|
122
|
+
@used += 1
|
123
|
+
@available.delete_at(index).tap do
|
124
|
+
Restify.logger.debug(LOG_PROGNAME) do
|
125
|
+
"[#{origin}] Take connection from pool " \
|
126
|
+
"(#{@available.size}/#{@used}/#{size})"
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def new_connection(origin)
|
132
|
+
# If we have reached the limit, we have to throw away the oldest
|
133
|
+
# reusable connection in order to open a new one
|
134
|
+
close_oldest if size >= @size
|
135
|
+
|
136
|
+
@used += 1
|
137
|
+
new(origin).tap do
|
138
|
+
Restify.logger.debug(LOG_PROGNAME) do
|
139
|
+
"[#{origin}] Add new connection to pool " \
|
140
|
+
"(#{@available.size}/#{@used}/#{size})"
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def close_oldest
|
146
|
+
close(@available.pop)
|
147
|
+
|
148
|
+
Restify.logger.debug(LOG_PROGNAME) do
|
149
|
+
"[#{origin}] Closed oldest connection in pool " \
|
150
|
+
"(#{@available.size}/#{@used}/#{size})"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def queue(defer)
|
155
|
+
Restify.logger.debug(LOG_PROGNAME) do
|
156
|
+
"[#{origin}] Wait for free slot " \
|
157
|
+
"(#{@available.size}/#{@used}/#{size})"
|
158
|
+
end
|
159
|
+
|
160
|
+
@queue << defer
|
161
|
+
end
|
162
|
+
|
163
|
+
def new(origin)
|
164
|
+
Restify.logger.debug(LOG_PROGNAME) do
|
165
|
+
"Connect to '#{origin}' " \
|
166
|
+
"(#{@connect_timeout}/#{@inactivity_timeout})..."
|
167
|
+
end
|
168
|
+
|
169
|
+
@host[origin] += 1
|
170
|
+
|
171
|
+
EventMachine::HttpRequest.new origin,
|
172
|
+
connect_timeout: @connect_timeout,
|
173
|
+
inactivity_timeout: @inactivity_timeout
|
174
|
+
end
|
175
|
+
|
176
|
+
def can_build_new_connection?(origin)
|
177
|
+
return false if @host[origin] >= @per_host
|
178
|
+
|
179
|
+
size < @size || @available.any?
|
180
|
+
end
|
181
|
+
|
182
|
+
class Deferrable
|
183
|
+
include ::EventMachine::Deferrable
|
184
|
+
|
185
|
+
attr_reader :request
|
186
|
+
|
187
|
+
def initialize(request)
|
188
|
+
@request = request
|
189
|
+
end
|
190
|
+
|
191
|
+
def succeed(connection)
|
192
|
+
@connection = connection
|
193
|
+
super
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def initialize(**kwargs)
|
199
|
+
@pool = Pool.new(**kwargs)
|
200
|
+
end
|
201
|
+
|
202
|
+
# rubocop:disable MethodLength
|
203
|
+
# rubocop:disable AbcSize
|
204
|
+
# rubocop:disable BlockLength
|
205
|
+
def call_native(request, writer)
|
206
|
+
next_tick do
|
207
|
+
defer = @pool.get(request)
|
208
|
+
|
209
|
+
defer.errback do |error|
|
210
|
+
writer.reject(error)
|
211
|
+
end
|
212
|
+
|
213
|
+
defer.callback do |conn|
|
214
|
+
begin
|
215
|
+
req = conn.send request.method.downcase,
|
216
|
+
keepalive: true,
|
217
|
+
redirects: 3,
|
218
|
+
path: request.uri.normalized_path,
|
219
|
+
query: request.uri.normalized_query,
|
220
|
+
body: request.body,
|
221
|
+
head: request.headers
|
222
|
+
|
223
|
+
req.callback do
|
224
|
+
writer.fulfill Response.new(
|
225
|
+
request,
|
226
|
+
req.last_effective_url,
|
227
|
+
req.response_header.status,
|
228
|
+
req.response_header,
|
229
|
+
req.response
|
230
|
+
)
|
231
|
+
|
232
|
+
if req.response_header['CONNECTION'] == 'close'
|
233
|
+
@pool.remove(conn)
|
234
|
+
else
|
235
|
+
@pool << conn
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
req.errback do
|
240
|
+
@pool.remove(conn)
|
241
|
+
writer.reject(req.error)
|
242
|
+
end
|
243
|
+
rescue Exception => ex # rubocop:disable RescueException
|
244
|
+
@pool.remove(conn)
|
245
|
+
writer.reject(ex)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
# rubocop:enable all
|
251
|
+
|
252
|
+
private
|
253
|
+
|
254
|
+
def next_tick(&block)
|
255
|
+
ensure_running
|
256
|
+
EventMachine.next_tick(&block)
|
257
|
+
end
|
258
|
+
|
259
|
+
def ensure_running
|
260
|
+
return if EventMachine.reactor_running?
|
261
|
+
|
262
|
+
Thread.new do
|
263
|
+
begin
|
264
|
+
EventMachine.run {}
|
265
|
+
rescue => e
|
266
|
+
puts "#{self.class} -> #{e}\n#{e.backtrace.join("\n")}"
|
267
|
+
raise e
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
data/lib/restify/context.rb
CHANGED
@@ -53,7 +53,7 @@ module Restify
|
|
53
53
|
method: method,
|
54
54
|
uri: join(uri),
|
55
55
|
data: data,
|
56
|
-
headers:
|
56
|
+
headers: headers
|
57
57
|
|
58
58
|
ret = cache.call(request) {|req| adapter.call(req) }
|
59
59
|
ret.then do |response|
|
@@ -65,6 +65,26 @@ module Restify
|
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
+
def encode_with(coder)
|
69
|
+
coder.map = marshal_dump
|
70
|
+
end
|
71
|
+
|
72
|
+
def init_with(coder)
|
73
|
+
marshal_load(coder.map)
|
74
|
+
end
|
75
|
+
|
76
|
+
def marshal_dump
|
77
|
+
{
|
78
|
+
uri: uri.to_s,
|
79
|
+
headers: headers
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
def marshal_load(dump)
|
84
|
+
initialize dump.delete(:uri), \
|
85
|
+
headers: dump.fetch(:headers)
|
86
|
+
end
|
87
|
+
|
68
88
|
private
|
69
89
|
|
70
90
|
def adapter
|
@@ -75,6 +95,10 @@ module Restify
|
|
75
95
|
options[:cache] || Restify.cache
|
76
96
|
end
|
77
97
|
|
98
|
+
def headers
|
99
|
+
options.fetch(:headers, {})
|
100
|
+
end
|
101
|
+
|
78
102
|
class << self
|
79
103
|
def raise_response_error(response)
|
80
104
|
case response.code
|
data/lib/restify/global.rb
CHANGED
@@ -30,6 +30,16 @@ module Restify
|
|
30
30
|
@cache = cache
|
31
31
|
end
|
32
32
|
|
33
|
+
def logger
|
34
|
+
@logger ||= ::Logger.new(STDOUT).tap do |logger|
|
35
|
+
logger.level = :info
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def logger=(logger)
|
40
|
+
@logger = logger
|
41
|
+
end
|
42
|
+
|
33
43
|
private
|
34
44
|
|
35
45
|
def resolve_context(uri, **opts)
|
data/lib/restify/response.rb
CHANGED
data/lib/restify/version.rb
CHANGED
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
describe Restify::Context do
|
7
|
+
let(:uri) { 'http://localhost' }
|
8
|
+
let(:kwargs) { {} }
|
9
|
+
let(:context) { Restify::Context.new(uri, **kwargs) }
|
10
|
+
|
11
|
+
describe '<serialization>' do
|
12
|
+
shared_examples 'serialization' do
|
13
|
+
describe '#uri' do
|
14
|
+
subject { super().uri }
|
15
|
+
|
16
|
+
it { expect(subject).to be_a Addressable::URI }
|
17
|
+
it { expect(subject).to eq context.uri }
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#adapter' do
|
21
|
+
let(:kwargs) { {adapter: double('adapter')} }
|
22
|
+
subject { super().options[:adapter] }
|
23
|
+
|
24
|
+
it 'adapter is not serialized' do
|
25
|
+
expect(subject).to equal nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#cache' do
|
30
|
+
let(:kwargs) { {adapter: double('cache')} }
|
31
|
+
subject { super().options[:cache] }
|
32
|
+
|
33
|
+
it 'cache is not serialized' do
|
34
|
+
expect(subject).to equal nil
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#headers' do
|
39
|
+
let(:kwargs) { {headers: {'Accept': 'application/json'}} }
|
40
|
+
subject { super().options[:headers] }
|
41
|
+
|
42
|
+
it do
|
43
|
+
expect(subject).to eq context.send :headers
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'YAML' do
|
49
|
+
let(:dump) { YAML.dump(context) }
|
50
|
+
let(:load) { YAML.load(dump) } # rubocop:disable YAMLLoad
|
51
|
+
|
52
|
+
subject { load }
|
53
|
+
|
54
|
+
include_examples 'serialization'
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'Marshall' do
|
58
|
+
let(:dump) { Marshal.dump(context) }
|
59
|
+
let(:load) { Marshal.load(dump) } # rubocop:disable MarshalLoad
|
60
|
+
|
61
|
+
subject { load }
|
62
|
+
|
63
|
+
include_examples 'serialization'
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -14,9 +14,12 @@ require 'restify'
|
|
14
14
|
|
15
15
|
if ENV['ADAPTER']
|
16
16
|
case ENV['ADAPTER'].to_s.downcase
|
17
|
-
when 'em
|
17
|
+
when 'em'
|
18
18
|
require 'restify/adapter/em'
|
19
19
|
Restify.adapter = Restify::Adapter::EM.new
|
20
|
+
when 'em-pooled'
|
21
|
+
require 'restify/adapter/pooled_em'
|
22
|
+
Restify.adapter = Restify::Adapter::PooledEM.new
|
20
23
|
when 'typhoeus'
|
21
24
|
require 'restify/adapter/typhoeus'
|
22
25
|
Restify.adapter = Restify::Adapter::Typhoeus.new
|
@@ -37,6 +40,10 @@ Dir[File.expand_path('spec/support/**/*.rb')].each {|f| require f }
|
|
37
40
|
RSpec.configure do |config|
|
38
41
|
config.order = 'random'
|
39
42
|
|
43
|
+
config.before(:each) do
|
44
|
+
Restify.logger.level = Logger::DEBUG
|
45
|
+
end
|
46
|
+
|
40
47
|
config.after(:each) do
|
41
48
|
EventMachine.stop if defined?(EventMachine) && EventMachine.reactor_running?
|
42
49
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: restify
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1
|
4
|
+
version: 1.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jan Graichen
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: concurrent-ruby
|
@@ -136,6 +136,7 @@ files:
|
|
136
136
|
- lib/restify.rb
|
137
137
|
- lib/restify/adapter/base.rb
|
138
138
|
- lib/restify/adapter/em.rb
|
139
|
+
- lib/restify/adapter/pooled_em.rb
|
139
140
|
- lib/restify/adapter/typhoeus.rb
|
140
141
|
- lib/restify/cache.rb
|
141
142
|
- lib/restify/context.rb
|
@@ -145,6 +146,7 @@ files:
|
|
145
146
|
- lib/restify/processors/base.rb
|
146
147
|
- lib/restify/processors/json.rb
|
147
148
|
- lib/restify/promise.rb
|
149
|
+
- lib/restify/railtie.rb
|
148
150
|
- lib/restify/registry.rb
|
149
151
|
- lib/restify/relation.rb
|
150
152
|
- lib/restify/request.rb
|
@@ -152,6 +154,7 @@ files:
|
|
152
154
|
- lib/restify/response.rb
|
153
155
|
- lib/restify/version.rb
|
154
156
|
- spec/restify/cache_spec.rb
|
157
|
+
- spec/restify/context_spec.rb
|
155
158
|
- spec/restify/global_spec.rb
|
156
159
|
- spec/restify/link_spec.rb
|
157
160
|
- spec/restify/processors/base_spec.rb
|
@@ -164,7 +167,7 @@ files:
|
|
164
167
|
- spec/spec_helper.rb
|
165
168
|
homepage: https://github.com/jgraichen/restify
|
166
169
|
licenses:
|
167
|
-
-
|
170
|
+
- LGPL-3.0+
|
168
171
|
metadata: {}
|
169
172
|
post_install_message:
|
170
173
|
rdoc_options: []
|
@@ -182,12 +185,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
182
185
|
version: '0'
|
183
186
|
requirements: []
|
184
187
|
rubyforge_project:
|
185
|
-
rubygems_version: 2.6.
|
188
|
+
rubygems_version: 2.6.14
|
186
189
|
signing_key:
|
187
190
|
specification_version: 4
|
188
191
|
summary: An experimental hypermedia REST client.
|
189
192
|
test_files:
|
190
193
|
- spec/restify/cache_spec.rb
|
194
|
+
- spec/restify/context_spec.rb
|
191
195
|
- spec/restify/global_spec.rb
|
192
196
|
- spec/restify/link_spec.rb
|
193
197
|
- spec/restify/processors/base_spec.rb
|