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 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