restify 1.1.0 → 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|