restify 1.4.4 → 1.5.0

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
  SHA256:
3
- metadata.gz: 05b4120c9755cdd5a88f172e3d5964eff3804add46785bd3cbec8b842f2af459
4
- data.tar.gz: 5cb09e31e263560b373b156cfaa601925b9bde328ff892d4533c2738b10dff67
3
+ metadata.gz: 8910f0a86f7bab64dbcfe43794c8fddbe8d37fb289845105718f61de3228f245
4
+ data.tar.gz: 843c1fd568747ed129589f08a8d7e3e040315f49f49620e46cd40265e1df2411
5
5
  SHA512:
6
- metadata.gz: a5854443d09b5d6ce73f3a68da7c76e2ed7453288b4e9ef940cfb2e437b8666f0c4f03b6602405193728751b5ee2cf46a12937196d412e3d911d7b497e947daf
7
- data.tar.gz: 535598e592a526d20cb8c5d93fe4926deea1d1f92f60bf5fd003115a56841462ad6ce475d96927a09b940524c622e80730ff06c858940c2d08e4ed8396fa8359
6
+ metadata.gz: 1697fecae910ceaf8c93f40416dcef77a1eba6a5bb0aa9fb67fffa520a7191fb273ac5a5bb9202ee00a7eec6ad559ccebd56f07b1a7c572077f4d44c9f7d3500
7
+ data.tar.gz: 4ad078a7595c6fdf7fc867eb367f658c7e1cec5297845467547668177834ad5722447520de2d3bcf1c0c1ff0d7924c903b7fe55cbe443bfa33605b5de2a8da0a
data/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.5.0
4
+
5
+ * Tune typhoeus adapter to be more race-condition resilent
6
+ * Add MessagePack processor enabled by default
7
+
3
8
  ## 1.4.4
4
9
 
5
10
  * Fix race condition in typhoeus adapter
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'typhoeus'
4
4
 
5
+ ::Ethon.logger = ::Logging.logger[Ethon]
6
+
5
7
  module Restify
6
8
  module Adapter
7
9
  class Typhoeus < Base
@@ -26,30 +28,26 @@ module Restify
26
28
  end
27
29
 
28
30
  def call_native(request, writer)
29
- @mutex.synchronize do
30
- logger.debug { "[#{request.object_id}] Queue request #{request}" }
31
- @hydra.queue convert(request, writer)
32
- @hydra.dequeue_many
33
-
34
- if sync?
35
- @hydra.run
36
- else
37
- thread.run
38
- @signal.signal
31
+ req = convert(request, writer)
32
+
33
+ if sync?
34
+ req.run
35
+ else
36
+ @mutex.synchronize do
37
+ logger.debug { "[#{self.object_id}/#{Thread.current.object_id}] [#{request.object_id}] request:add method=#{request.method.upcase} url=#{request.uri}" }
38
+ @hydra.queue(req)
39
+ @hydra.dequeue_many
40
+
41
+ thread.run unless thread.status
39
42
  end
40
- end
41
- end
42
43
 
43
- def queued?
44
- @mutex.synchronize { ns_queued? }
44
+ logger.debug { "[#{self.object_id}/#{Thread.current.object_id}] [#{request.object_id}] request:signal" }
45
+ @signal.signal
46
+ end
45
47
  end
46
48
 
47
49
  private
48
50
 
49
- def ns_queued?
50
- @hydra.queued_requests.any? || @hydra.multi.easy_handles.count > 0
51
- end
52
-
53
51
  def convert(request, writer)
54
52
  ::Typhoeus::Request.new(
55
53
  request.uri,
@@ -61,7 +59,7 @@ module Restify
61
59
  connecttimeout: request.timeout
62
60
  ).tap do |req|
63
61
  req.on_complete do |response|
64
- logger.debug { "[#{request.object_id}] Completed: #{response.code}" }
62
+ logger.debug { "[#{self.object_id}/#{Thread.current.object_id}] [#{request.object_id}] request:complete status=#{response.code}" }
65
63
 
66
64
  if response.timed_out?
67
65
  writer.reject Restify::Timeout.new request
@@ -94,6 +92,7 @@ module Restify
94
92
  def thread
95
93
  if @thread.nil? || !@thread.status
96
94
  # Recreate thread if nil or dead
95
+ logger.debug { "[#{self.object_id}/#{Thread.current.object_id}] hydra:spawn" }
97
96
  @thread = Thread.new { _loop }
98
97
  end
99
98
 
@@ -101,20 +100,25 @@ module Restify
101
100
  end
102
101
 
103
102
  def _loop
103
+ Thread.current.name = 'Restify/Typhoeus Background'
104
104
  loop { _run }
105
105
  end
106
106
 
107
+ def _ongoing?
108
+ @hydra.queued_requests.any? || @hydra.multi.easy_handles.any?
109
+ end
110
+
107
111
  def _run
108
- if queued?
109
- logger.debug { 'Run hydra' }
110
- @hydra.run
111
- logger.debug { 'Hydra finished' }
112
- else
113
- @mutex.synchronize do
114
- return if ns_queued?
115
- logger.debug { 'Pause hydra thread' }
116
- @signal.wait(@mutex)
117
- end
112
+ logger.debug { "[#{self.object_id}/#{Process.pid}] hydra:run" }
113
+ @hydra.run while _ongoing?
114
+ logger.debug { "[#{self.object_id}/#{Thread.current.object_id}] hydra:completed" }
115
+
116
+ @mutex.synchronize do
117
+ return if _ongoing?
118
+
119
+ logger.debug { "[#{self.object_id}/#{Thread.current.object_id}] hydra:pause" }
120
+ @signal.wait(@mutex, 60)
121
+ logger.debug { "[#{self.object_id}/#{Thread.current.object_id}] hydra:resumed" }
118
122
  end
119
123
  rescue StandardError => e
120
124
  logger.error(e)
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module Restify
6
+ module Processors
7
+ class Base
8
+ #
9
+ # Parses generic data structures into resources
10
+ #
11
+ module Parsing
12
+ def self.included(base)
13
+ base.extend ClassMethods
14
+ base.indifferent_access = true
15
+ end
16
+
17
+ def load
18
+ parse deserialized_body, root: true
19
+ end
20
+
21
+ def parse(object, root: false)
22
+ case object
23
+ when Hash
24
+ data = object.each_with_object({}, &method(:parse_data))
25
+ relations = object.each_with_object({}, &method(:parse_rels))
26
+
27
+ if self.class.indifferent_access?
28
+ data = with_indifferent_access(data)
29
+ end
30
+
31
+ Resource.new context,
32
+ data: data,
33
+ response: root ? response : nil,
34
+ relations: relations
35
+
36
+ when Array
37
+ object.map(&method(:parse))
38
+ else
39
+ object
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def parse_data(pair, data)
46
+ data[pair[0].to_s] = parse pair[1]
47
+ end
48
+
49
+ def parse_rels(pair, relations)
50
+ name = case pair[0].to_s.downcase
51
+ when /\A(\w+)_url\z/
52
+ Regexp.last_match[1]
53
+ when 'url'
54
+ 'self'
55
+ else
56
+ return
57
+ end
58
+
59
+ if relations.key?(name) || pair[1].nil? || pair[1].to_s =~ /\A\w*\z/
60
+ return
61
+ end
62
+
63
+ relations[name] = pair[1].to_s
64
+ end
65
+
66
+ def with_indifferent_access(data)
67
+ Hashie::Mash.new data
68
+ end
69
+
70
+ module ClassMethods
71
+ def indifferent_access?
72
+ @indifferent_access
73
+ end
74
+
75
+ attr_writer :indifferent_access
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -3,7 +3,6 @@
3
3
  require 'json'
4
4
 
5
5
  module Restify
6
- #
7
6
  module Processors
8
7
  #
9
8
  # Decode plain JSON responses.
@@ -11,82 +10,17 @@ module Restify
11
10
  # JSON fields matching *_url will be parsed as relations.
12
11
  #
13
12
  class Json < Base
14
- def load
15
- parse ::JSON.parse(body), root: true
16
- end
17
-
18
- private
19
-
20
- # rubocop:disable Metrics/MethodLength
21
- def parse(object, root: false)
22
- case object
23
- when Hash
24
- data = object.each_with_object({}, &method(:parse_data))
25
- relations = object.each_with_object({}, &method(:parse_rels))
26
-
27
- if self.class.indifferent_access?
28
- data = with_indifferent_access(data)
29
- end
30
-
31
- if root
32
- Resource.new context,
33
- data: data,
34
- response: response,
35
- relations: relations
36
- else
37
- Resource.new context,
38
- data: data,
39
- relations: relations
40
- end
41
- when Array
42
- object.map(&method(:parse))
43
- else
44
- object
45
- end
46
- end
47
-
48
- def parse_data(pair, data)
49
- data[pair[0].to_s] = parse pair[1]
50
- end
51
-
52
- def parse_rels(pair, relations)
53
- name = case pair[0].to_s.downcase
54
- when /\A(\w+)_url\z/
55
- Regexp.last_match[1]
56
- when 'url'
57
- 'self'
58
- else
59
- return
60
- end
13
+ include Parsing
61
14
 
62
- if relations.key?(name) || pair[1].nil? || pair[1].to_s =~ /\A\w*\z/
63
- return
64
- end
65
-
66
- relations[name] = pair[1].to_s
67
- end
68
-
69
- def json
70
- @json ||= JSON.parse(response.body)
71
- end
72
-
73
- def with_indifferent_access(data)
74
- Hashie::Mash.new data
15
+ def deserialized_body
16
+ ::JSON.parse(body)
75
17
  end
76
18
 
77
19
  class << self
78
20
  def accept?(response)
79
21
  response.content_type =~ %r{\Aapplication/json($|;)}
80
22
  end
81
-
82
- def indifferent_access?
83
- @indifferent_access
84
- end
85
-
86
- attr_writer :indifferent_access
87
23
  end
88
-
89
- self.indifferent_access = true
90
24
  end
91
25
  end
92
26
  end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'msgpack'
4
+
5
+ module Restify
6
+ module Processors
7
+ #
8
+ # Decode messagepack encoded responses.
9
+ #
10
+ # Fields matching *_url will be parsed as relations.
11
+ #
12
+ class Msgpack < Base
13
+ include Parsing
14
+
15
+ def deserialized_body
16
+ ::MessagePack.unpack(body)
17
+ end
18
+
19
+ class << self
20
+ def accept?(response)
21
+ response.content_type =~ %r{\Aapplication/(x-)?msgpack($|;)}
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -3,8 +3,8 @@
3
3
  module Restify
4
4
  module VERSION
5
5
  MAJOR = 1
6
- MINOR = 4
7
- PATCH = 4
6
+ MINOR = 5
7
+ PATCH = 0
8
8
  STAGE = nil
9
9
  STRING = [MAJOR, MINOR, PATCH, STAGE].reject(&:nil?).join('.').freeze
10
10
 
data/lib/restify.rb CHANGED
@@ -9,7 +9,6 @@ require 'concurrent'
9
9
  require 'addressable/uri'
10
10
  require 'addressable/template'
11
11
 
12
- #
13
12
  module Restify
14
13
  require 'restify/error'
15
14
  require 'restify/logging'
@@ -34,10 +33,13 @@ module Restify
34
33
 
35
34
  module Processors
36
35
  require 'restify/processors/base'
36
+ require 'restify/processors/base/parsing'
37
+
37
38
  require 'restify/processors/json'
39
+ require 'restify/processors/msgpack'
38
40
  end
39
41
 
40
- PROCESSORS = [Processors::Json].freeze
42
+ PROCESSORS = [Processors::Json, Processors::Msgpack].freeze
41
43
 
42
44
  extend Global
43
45
  end
@@ -0,0 +1,185 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Processors::Msgpack do
6
+ let(:context) { Restify::Context.new('http://test.host/') }
7
+ let(:response) { double 'response' }
8
+
9
+ before do
10
+ allow(response).to receive(:links).and_return []
11
+ allow(response).to receive(:follow_location).and_return nil
12
+ end
13
+
14
+ describe 'class' do
15
+ describe '#accept?' do
16
+ subject { described_class.accept? response }
17
+
18
+ it 'accepts msgpack mime type (I)' do
19
+ expect(response).to receive(:content_type).and_return('application/msgpack')
20
+ expect(subject).to be_truthy
21
+ end
22
+
23
+ it 'accepts msgpack mime type (II)' do
24
+ expect(response).to receive(:content_type).and_return('application/msgpack; abc')
25
+ expect(subject).to be_truthy
26
+ end
27
+
28
+ it 'accepts x-msgpack mime type (I)' do
29
+ expect(response).to receive(:content_type).and_return('application/x-msgpack')
30
+ expect(subject).to be_truthy
31
+ end
32
+
33
+ it 'accepts x-msgpack mime type (II)' do
34
+ expect(response).to receive(:content_type).and_return('application/x-msgpack; abc')
35
+ expect(subject).to be_truthy
36
+ end
37
+ end
38
+ end
39
+
40
+ describe '#resource' do
41
+ subject { described_class.new(context, response).resource }
42
+ before { allow(response).to receive(:body).and_return(body) }
43
+
44
+ describe 'parsing' do
45
+ context 'single object' do
46
+ let(:body) do
47
+ ::MessagePack.dump('msg' => 'value')
48
+ end
49
+
50
+ it { is_expected.to be_a Restify::Resource }
51
+ it { expect(subject.response).to be response }
52
+ it { is_expected.to eq 'msg' => 'value' }
53
+ end
54
+
55
+ context 'object with relation fields' do
56
+ let(:body) do
57
+ ::MessagePack.dump(
58
+ 'msg' => 'value',
59
+ 'search_url' => 'https://google.com{?q}'
60
+ )
61
+ end
62
+
63
+ it do
64
+ is_expected.to eq \
65
+ 'msg' => 'value', 'search_url' => 'https://google.com{?q}'
66
+ end
67
+
68
+ it { is_expected.to have_relation :search }
69
+ it { expect(subject.relation(:search)).to eq 'https://google.com{?q}' }
70
+ end
71
+
72
+ context 'object with implicit self relation' do
73
+ let(:body) do
74
+ ::MessagePack.dump(
75
+ 'url' => '/self'
76
+ )
77
+ end
78
+
79
+ it { expect(subject.relation(:self)).to eq '/self' }
80
+ end
81
+
82
+ context 'single array' do
83
+ let(:body) do
84
+ ::MessagePack.dump([1, 2, nil, 'STR'])
85
+ end
86
+
87
+ it { is_expected.to be_a Restify::Resource }
88
+ it { expect(subject.response).to be response }
89
+ it { is_expected.to eq [1, 2, nil, 'STR'] }
90
+ end
91
+
92
+ context 'array with objects' do
93
+ let(:body) do
94
+ ::MessagePack.dump([{'a' => 0}, {'b' => 1}])
95
+ end
96
+
97
+ it { is_expected.to eq [{'a' => 0}, {'b' => 1}] }
98
+ end
99
+
100
+ context 'array with resources' do
101
+ let(:body) do
102
+ ::MessagePack.dump([
103
+ {'name' => 'John', 'self_url' => '/users/john'},
104
+ {'name' => 'Jane', 'self_url' => '/users/jane'}
105
+ ])
106
+ end
107
+
108
+ it 'parses objects as resources' do
109
+ expect(subject).to all(be_a(Restify::Resource))
110
+ end
111
+
112
+ it 'parses relations of resources' do
113
+ expect(subject.map {|r| r.relation :self }).to eq \
114
+ ['/users/john', '/users/jane']
115
+ end
116
+ end
117
+
118
+ context 'nested objects' do
119
+ let(:body) do
120
+ ::MessagePack.dump(
121
+ 'john' => {'name' => 'John'},
122
+ 'jane' => {'name' => 'Jane'}
123
+ )
124
+ end
125
+
126
+ it { is_expected.to be_a Restify::Resource }
127
+ it { expect(subject.response).to be response }
128
+
129
+ it 'parses objects as resources' do
130
+ expect(subject['john']).to be_a Restify::Resource
131
+ expect(subject['jane']).to be_a Restify::Resource
132
+
133
+ expect(subject['john']['name']).to eq 'John'
134
+ expect(subject['jane']['name']).to eq 'Jane'
135
+ end
136
+ end
137
+
138
+ context 'single value' do
139
+ let(:body) do
140
+ ::MessagePack.dump('BLUB')
141
+ end
142
+
143
+ it { is_expected.to be_a Restify::Resource }
144
+ it { expect(subject.response).to be response }
145
+ it { is_expected.to eq 'BLUB' }
146
+ end
147
+
148
+ context 'with indifferent access' do
149
+ let(:body) do
150
+ ::MessagePack.dump('name' => 'John', 'age' => 24)
151
+ end
152
+
153
+ it '#key?' do
154
+ expect(subject).to have_key 'name'
155
+ expect(subject).to have_key 'age'
156
+
157
+ expect(subject).to have_key :name
158
+ expect(subject).to have_key :age
159
+ end
160
+
161
+ it '#[]' do
162
+ expect(subject['name']).to eq 'John'
163
+ expect(subject['age']).to eq 24
164
+
165
+ expect(subject[:name]).to eq 'John'
166
+ expect(subject[:age]).to eq 24
167
+ end
168
+ end
169
+
170
+ context 'with method getter access' do
171
+ let(:body) do
172
+ ::MessagePack.dump('name' => 'John', 'age' => 24)
173
+ end
174
+
175
+ it '#<method getter>' do
176
+ expect(subject).to respond_to :name
177
+ expect(subject).to respond_to :age
178
+
179
+ expect(subject.name).to eq 'John'
180
+ expect(subject.age).to eq 24
181
+ end
182
+ end
183
+ end
184
+ end
185
+ 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.4.4
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jan Graichen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-13 00:00:00.000000000 Z
11
+ date: 2018-07-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '2.0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: msgpack
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '1.2'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '1.2'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: bundler
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -145,7 +159,9 @@ files:
145
159
  - lib/restify/link.rb
146
160
  - lib/restify/logging.rb
147
161
  - lib/restify/processors/base.rb
162
+ - lib/restify/processors/base/parsing.rb
148
163
  - lib/restify/processors/json.rb
164
+ - lib/restify/processors/msgpack.rb
149
165
  - lib/restify/promise.rb
150
166
  - lib/restify/registry.rb
151
167
  - lib/restify/relation.rb
@@ -159,6 +175,7 @@ files:
159
175
  - spec/restify/link_spec.rb
160
176
  - spec/restify/processors/base_spec.rb
161
177
  - spec/restify/processors/json_spec.rb
178
+ - spec/restify/processors/msgpack_spec.rb
162
179
  - spec/restify/promise_spec.rb
163
180
  - spec/restify/registry_spec.rb
164
181
  - spec/restify/relation_spec.rb
@@ -196,6 +213,7 @@ test_files:
196
213
  - spec/restify/link_spec.rb
197
214
  - spec/restify/processors/base_spec.rb
198
215
  - spec/restify/processors/json_spec.rb
216
+ - spec/restify/processors/msgpack_spec.rb
199
217
  - spec/restify/promise_spec.rb
200
218
  - spec/restify/registry_spec.rb
201
219
  - spec/restify/relation_spec.rb