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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3acb2cc3581b8c3c47ad27c6ab1ea34406d7fa39
4
- data.tar.gz: 91e4eb0b80116b6e13d8aca4cf8942de6ef064bc
3
+ metadata.gz: 6cb4e54249171cc430b42e94584d53a8d855dc27
4
+ data.tar.gz: 021d1ef66172c7a992f7f447ffbe0ac2770421d9
5
5
  SHA512:
6
- metadata.gz: e314678c3419674b6a709db0ba224bd46b704ba24455db084dba0f7e9dfccaf4c633243e384411d1fd7046a0c34480f19353a2313190c70ed62599eb50d04a3a
7
- data.tar.gz: 8378f0c684c143cab5c92b4799a1424370f3b0db6395296d7b4bf38b3b8c48e08274035dfcd770f8ecc52308e9b80d74247628e125635f477f2fcec7436ba670
6
+ metadata.gz: d32a9c6b8978674f26a021f5e29618fe3c75012144697431f79033db6f4a503c0d7de39803dedb20c691c0d3feaa0965a9382de598cf9741d7c9bcb33e63292e
7
+ data.tar.gz: 8b699331f7a597f3b99ead0a5fe9da59bdec2648244a802798c952cdac430d729a1297e91807657723ed43fd35a04c3b52d95ec8abf93a1364fd2a2f99140af8
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.2.1
4
+
5
+ * Fix issue with Ruby 2.2 compatibility
6
+
7
+ ## 1.2.0
8
+
9
+ * Add experimental PooledEM adapter (#10)
10
+ * Improve marshalling of resources
11
+
3
12
  ## 1.1.0
4
13
 
5
14
  * Return response body if no processor matches (#7)
data/README.md CHANGED
@@ -26,12 +26,9 @@ Included processors can handle:
26
26
 
27
27
  Restify requires Ruby 2.0+.
28
28
 
29
- ## Restify is still under development
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.
@@ -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
@@ -53,7 +53,7 @@ module Restify
53
53
  method: method,
54
54
  uri: join(uri),
55
55
  data: data,
56
- headers: options.fetch(: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
@@ -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)
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Restify
4
+ class Railtie < Rails::Railtie
5
+ initializer 'restify.logger' do
6
+ config.after_initialize do
7
+ Restify.logger = Rails.logger
8
+ end
9
+ end
10
+ end
11
+ end
@@ -134,7 +134,7 @@ module Restify
134
134
 
135
135
  # @api private
136
136
  def follow_location
137
- headers['LOCATION'] || headers['CONENT_LOCATION']
137
+ headers['LOCATION'] || headers['CONTENT_LOCATION']
138
138
  end
139
139
 
140
140
  private
@@ -3,8 +3,8 @@
3
3
  module Restify
4
4
  module VERSION
5
5
  MAJOR = 1
6
- MINOR = 1
7
- PATCH = 0
6
+ MINOR = 2
7
+ PATCH = 1
8
8
  STAGE = nil
9
9
  STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.').freeze
10
10
 
@@ -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
@@ -14,9 +14,12 @@ require 'restify'
14
14
 
15
15
  if ENV['ADAPTER']
16
16
  case ENV['ADAPTER'].to_s.downcase
17
- when 'em-http-request'
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.0
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-05-12 00:00:00.000000000 Z
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
- - LGPLv3
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.11
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