pebblebed 0.3.26 → 0.4.0

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: 14d1b62f07bf96068be143f86a02af3d0e9d3f86
4
- data.tar.gz: 52004701d254aa4afd4855e4c0028ac9bf17bb29
3
+ metadata.gz: 5a72b667ab7760684fba215b9c8b1efcae9c1294
4
+ data.tar.gz: 2a0663c6a8a01c4996c5e4726d39efbeca6cb97e
5
5
  SHA512:
6
- metadata.gz: 24d3974b44099d97ef4dad54bfc50a7c13a4dabdd5b693a22da982e8653920c31c780644f7d2f6d9a06e3346c0794e748c15628d61566020a2d25b96ceee9a5b
7
- data.tar.gz: 93b67975bc2cffb0f473f1a79a7e18e30d3dd745af5283e13b5220bafafcd9cfc68dbd21ce09cf407ea099c03c60d1467e7440f7aa1a44a4f08067190ceaab48
6
+ metadata.gz: 22492117f67d35017f0458d5d6b829378f8ae9583b4fcd1d7d6a36d88ddce51e8ea2f886c3b3060072c4834629ca8ef7a722c462dcd82aff232d77e0bcb0f495
7
+ data.tar.gz: ac85b869fe8d67e0f364ddf3054bb72bf107740c9fa45daf9a3c0174b0ed247a64af2835f8dad39a40c73e60511bd667f1ba00e6c920a371ff8c541b697bfe4d
@@ -1,12 +1,14 @@
1
1
  # A wrapper for all low level http client stuff
2
2
 
3
3
  require 'uri'
4
- require 'curl'
4
+ require 'excon'
5
5
  require 'yajl/json_gem'
6
6
  require 'queryparams'
7
7
  require 'nokogiri'
8
8
  require 'pathbuilder'
9
9
  require 'active_support'
10
+ require 'timeout'
11
+ require 'resolv'
10
12
 
11
13
  module Pebblebed
12
14
 
@@ -32,35 +34,43 @@ module Pebblebed
32
34
  end
33
35
  end
34
36
 
37
+ class HttpTransportError < StandardError
38
+ def initialize(e = nil)
39
+ super e
40
+ set_backtrace e.backtrace if e
41
+ end
42
+ end
43
+
35
44
  class HttpNotFoundError < HttpError; end
45
+ class HttpSocketError < HttpTransportError; end
46
+ class HttpTimeoutError < HttpTransportError; end
36
47
 
37
48
  module Http
38
49
 
39
50
  DEFAULT_CONNECT_TIMEOUT = 30
40
- DEFAULT_REQUEST_TIMEOUT = nil
41
51
  DEFAULT_READ_TIMEOUT = 30
52
+ DEFAULT_WRITE_TIMEOUT = 60
42
53
 
43
54
  class << self
44
- attr_reader :connect_timeout, :request_timeout, :read_timeout
55
+ attr_reader :connect_timeout, :read_timeout, :write_timeout
45
56
  def connect_timeout=(value)
46
57
  @connect_timeout = value
47
- self.current_easy = nil
48
- end
49
- def request_timeout=(value)
50
- @request_timeout = value
51
- self.current_easy = nil
58
+ Thread.current[:pebblebed_excon] = {}
52
59
  end
53
60
  def read_timeout=(value)
54
61
  @read_timeout = value
55
- self.current_easy = nil
62
+ Thread.current[:pebblebed_excon] = {}
63
+ end
64
+ def write_timeout=(value)
65
+ @write_timeout = value
66
+ Thread.current[:pebblebed_excon] = {}
56
67
  end
57
68
  end
58
69
 
59
70
  class Response
60
- def initialize(url, header, body)
71
+ def initialize(url, status, body)
61
72
  @body = body
62
- # We parse it ourselves because Curl::Easy fails when there's no text message
63
- @status = header.scan(/HTTP\/\d\.\d\s(\d+)\s/).map(&:first).last.to_i
73
+ @status = status
64
74
  @url = url
65
75
  end
66
76
 
@@ -69,90 +79,123 @@ module Pebblebed
69
79
 
70
80
  def self.get(url = nil, params = nil, &block)
71
81
  url, params = url_and_params_from_args(url, params, &block)
72
- return do_easy { |easy|
73
- easy.url = url_with_params(url, params)
74
- easy.http_get
82
+ return do_request(url) { |connection|
83
+ connection.get(
84
+ :host => url.host,
85
+ :path => url.path,
86
+ :query => params,
87
+ :persistent => true
88
+ )
75
89
  }
76
90
  end
77
91
 
78
92
  def self.post(url, params, &block)
79
93
  url, params = url_and_params_from_args(url, params, &block)
80
94
  content_type, body = serialize_params(params)
81
- return do_easy { |easy|
82
- easy.url = url.to_s
83
- easy.headers['Accept'] = 'application/json'
84
- easy.headers['Content-Type'] = content_type
85
- easy.http_post(body)
95
+ return do_request(url, idempotent: false) { |connection|
96
+ connection.post(
97
+ :host => url.host,
98
+ :path => url.path,
99
+ :headers => {
100
+ 'Accept' => 'application/json',
101
+ 'Content-Type' => content_type
102
+ },
103
+ :body => body,
104
+ :persistent => true
105
+ )
86
106
  }
87
107
  end
88
108
 
89
109
  def self.put(url, params, &block)
90
110
  url, params = url_and_params_from_args(url, params, &block)
91
111
  content_type, body = serialize_params(params)
92
- return do_easy { |easy|
93
- easy.url = url.to_s
94
- easy.headers['Accept'] = 'application/json'
95
- easy.headers['Content-Type'] = content_type
96
- easy.http_put(body)
112
+ return do_request(url) { |connection|
113
+ connection.put(
114
+ :host => url.host,
115
+ :path => url.path,
116
+ :headers => {
117
+ 'Accept' => 'application/json',
118
+ 'Content-Type' => content_type
119
+ },
120
+ :body => body,
121
+ :persistent => true
122
+ )
97
123
  }
98
124
  end
99
125
 
100
126
  def self.delete(url, params, &block)
101
127
  url, params = url_and_params_from_args(url, params, &block)
102
- return do_easy { |easy|
103
- easy.url = url_with_params(url, params)
104
- easy.http_delete
128
+ return do_request(url) { |connection|
129
+ connection.delete(
130
+ :host => url.host,
131
+ :path => url.path,
132
+ :query => params,
133
+ :persistent => true
134
+ )
105
135
  }
106
136
  end
107
137
 
108
- def self.stream_get(url = nil, params = nil, options = {})
109
- return do_easy(cache: false) { |easy|
110
- on_data = options[:on_data] or raise "Option :on_data must be specified"
111
-
112
- url, params = url_and_params_from_args(url, params)
138
+ def self.streamer(on_data)
139
+ lambda do |chunk, remaining_bytes, total_bytes|
140
+ on_data.call(chunk)
141
+ total_bytes
142
+ end
143
+ end
113
144
 
114
- easy.url = url_with_params(url, params)
115
- easy.on_body do |data|
116
- on_data.call(data)
117
- data.length
118
- end
119
- easy.http_get
145
+ def self.stream_get(url = nil, params = nil, options = {})
146
+ on_data = options[:on_data] or raise "Option :on_data must be specified"
147
+
148
+ url, params = url_and_params_from_args(url, params)
149
+ return do_request(url) { |connection|
150
+ connection.get(
151
+ :host => url.host,
152
+ :path => url.path,
153
+ :query => params,
154
+ :persistent => false,
155
+ :response_block => streamer(on_data)
156
+ )
120
157
  }
121
158
  end
122
159
 
123
160
  def self.stream_post(url, params, options = {})
124
- return do_easy(cache: false) { |easy|
125
- on_data = options[:on_data] or raise "Option :on_data must be specified"
126
-
127
- url, params = url_and_params_from_args(url, params)
128
- content_type, body = serialize_params(params)
129
-
130
- easy.url = url.to_s
131
- easy.headers['Accept'] = 'application/json'
132
- easy.headers['Content-Type'] = content_type
133
- easy.on_body do |data|
134
- on_data.call(data)
135
- data.length
136
- end
137
- easy.http_post(body)
161
+ on_data = options[:on_data] or raise "Option :on_data must be specified"
162
+
163
+ url, params = url_and_params_from_args(url, params)
164
+ content_type, body = serialize_params(params)
165
+
166
+ return do_request(url) { |connection|
167
+ connection.post(
168
+ :host => url.host,
169
+ :path => url.path,
170
+ :headers => {
171
+ 'Accept' => 'application/json',
172
+ 'Content-Type' => content_type
173
+ },
174
+ :body => body,
175
+ :persistent => false,
176
+ :response_block => streamer(on_data)
177
+ )
138
178
  }
139
179
  end
140
180
 
141
181
  def self.stream_put(url, params, options = {})
142
- return do_easy(cache: false) { |easy|
143
- on_data = options[:on_data] or raise "Option :on_data must be specified"
144
-
145
- url, params = url_and_params_from_args(url, params)
146
- content_type, body = serialize_params(params)
147
-
148
- easy.url = url.to_s
149
- easy.headers['Accept'] = 'application/json'
150
- easy.headers['Content-Type'] = content_type
151
- easy.on_body do |data|
152
- on_data.call(data)
153
- data.length
154
- end
155
- easy.http_put(body)
182
+ on_data = options[:on_data] or raise "Option :on_data must be specified"
183
+
184
+ url, params = url_and_params_from_args(url, params)
185
+ content_type, body = serialize_params(params)
186
+
187
+ return do_request(url) { |connection|
188
+ connection.put(
189
+ :host => url.host,
190
+ :path => url.path,
191
+ :headers => {
192
+ 'Accept' => 'application/json',
193
+ 'Content-Type' => content_type
194
+ },
195
+ :body => body,
196
+ :persistent => false,
197
+ :response_block => streamer(on_data)
198
+ )
156
199
  }
157
200
  end
158
201
 
@@ -191,44 +234,72 @@ module Pebblebed
191
234
  response
192
235
  end
193
236
 
194
- def self.do_easy(cache: true, &block)
195
- with_easy(cache: cache) do |easy|
196
- yield easy
197
- response = Response.new(easy.url, easy.header_str, easy.body_str)
198
- return handle_http_errors(response)
237
+ def self.do_request(url, idempotent: true, &block)
238
+ with_connection(url) { |connection|
239
+ begin
240
+ request = block.call(connection)
241
+ response = Response.new(url, request.status, request.body)
242
+ return handle_http_errors(response)
243
+ rescue Excon::Errors::Timeout => error
244
+ raise HttpTimeoutError.new(error)
245
+ rescue Excon::Errors::SocketError => error
246
+ raise HttpSocketError.new(error) unless idempotent
247
+ # Connection failed, close the connection and try again
248
+ connection.reset
249
+ begin
250
+ request = block.call(connection)
251
+ response = Response.new(url, request.status, request.body)
252
+ return handle_http_errors(response)
253
+ rescue Excon::Errors::SocketError => error
254
+ raise HttpSocketError.new(error)
255
+ end
256
+ end
257
+ }
258
+ end
259
+
260
+ def self.with_connection(url, &block)
261
+ connection = self.current_connection(url) || new_connection(url)
262
+ self.current_connection={:url => url, :connection => connection}
263
+ yield connection
264
+ end
265
+
266
+ def self.base_url(url)
267
+ if url.is_a?(URI)
268
+ uri = url
269
+ else
270
+ uri = URI.parse(url)
199
271
  end
272
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
200
273
  end
201
274
 
202
- def self.with_easy(cache: true, &block)
203
- if cache
204
- easy = self.current_easy ||= new_easy
275
+ def self.cache_key(url)
276
+ if url.is_a?(URI)
277
+ uri = url
205
278
  else
206
- easy = new_easy
279
+ uri = URI.parse(url)
207
280
  end
208
- yield easy
281
+ ip = Resolv.getaddress(uri.host)
282
+ "#{uri.scheme}://#{ip}:#{uri.port}"
209
283
  end
210
284
 
211
- def self.current_easy
212
- Thread.current[:pebblebed_curb_easy]
285
+ def self.current_connection(url)
286
+ Thread.current[:pebblebed_excon] ||= {}
287
+ Thread.current[:pebblebed_excon][cache_key(url)]
213
288
  end
214
289
 
215
- def self.current_easy=(value)
216
- if (current = Thread.current[:pebblebed_curb_easy])
217
- # Reset old instance
218
- current.reset
219
- end
220
- Thread.current[:pebblebed_curb_easy] = value
290
+ def self.current_connection=(value)
291
+ Thread.current[:pebblebed_excon] ||= {}
292
+ Thread.current[:pebblebed_excon][cache_key(value[:url])] = value[:connection]
221
293
  end
222
294
 
223
- # Returns new Easy instance from current configuration.
224
- def self.new_easy
225
- easy = Curl::Easy.new
226
- easy.connect_timeout = connect_timeout || DEFAULT_CONNECT_TIMEOUT
227
- easy.timeout = request_timeout || DEFAULT_REQUEST_TIMEOUT
228
- easy.low_speed_time = read_timeout || DEFAULT_READ_TIMEOUT
229
- easy.low_speed_limit = 1
230
- easy.follow_location = true
231
- easy
295
+ # Returns new Excon conection from current configuration.
296
+ def self.new_connection(url)
297
+ connection = Excon.new(base_url(url), {
298
+ :read_timeout => read_timeout || DEFAULT_READ_TIMEOUT,
299
+ :write_timeout => write_timeout || DEFAULT_WRITE_TIMEOUT,
300
+ :connect_timeout => connect_timeout || DEFAULT_CONNECT_TIMEOUT,
301
+ :thread_safe_sockets => false
302
+ })
232
303
  end
233
304
 
234
305
  def self.url_with_params(url, params)
@@ -1,3 +1,3 @@
1
1
  module Pebblebed
2
- VERSION = "0.3.26"
2
+ VERSION = "0.4.0"
3
3
  end
data/pebblebed.gemspec CHANGED
@@ -25,9 +25,10 @@ Gem::Specification.new do |s|
25
25
  s.add_development_dependency "sinatra" # for testing purposes
26
26
  s.add_development_dependency "rack-test" # for testing purposes
27
27
  s.add_development_dependency "memcache_mock"
28
+ s.add_development_dependency "webmock"
28
29
 
29
30
  s.add_runtime_dependency "deepstruct", ">= 0.0.4"
30
- s.add_runtime_dependency "curb", ">= 0.8.8"
31
+ s.add_runtime_dependency "excon", ">= 0.52.0"
31
32
  s.add_runtime_dependency "yajl-ruby"
32
33
  s.add_runtime_dependency "queryparams"
33
34
  s.add_runtime_dependency "futurevalue"
data/spec/http_spec.rb CHANGED
@@ -22,8 +22,8 @@ describe Pebblebed::Http do
22
22
 
23
23
  before :all do
24
24
  Pebblebed::Http.connect_timeout = nil
25
- Pebblebed::Http.request_timeout = nil
26
25
  Pebblebed::Http.read_timeout = nil
26
+ Pebblebed::Http.write_timeout = nil
27
27
 
28
28
  # Starts the mock pebble at localhost:8666/api/mock/v1
29
29
  mock_pebble.start
@@ -101,7 +101,7 @@ describe Pebblebed::Http do
101
101
  })
102
102
  result = JSON.parse(buf)
103
103
  expect(result["QUERY_STRING"]).to eq "hello=world"
104
- expect(response.body).to eq nil
104
+ expect(response.body).to eq ''
105
105
  end
106
106
 
107
107
  it "supports multiple sequential streaming request" do
@@ -113,31 +113,53 @@ describe Pebblebed::Http do
113
113
  })
114
114
  result = JSON.parse(buf)
115
115
  expect(result["QUERY_STRING"]).to eq "hello=world"
116
- expect(response.body).to eq nil
116
+ expect(response.body).to eq ''
117
117
  end
118
118
  end
119
119
  end
120
120
  end
121
-
122
- it "enforces request timeout" do
123
- Pebblebed::Http.request_timeout = 1
124
- expect {
125
- Pebblebed::Http.get(pebble_url, {slow: '2'})
126
- }.to raise_error(Curl::Err::TimeoutError)
127
- expect {
128
- Pebblebed::Http.get(pebble_url, {slow: '0.5'})
129
- }.not_to raise_error
130
- end
131
-
132
121
  it "enforces read timeout" do
133
- Pebblebed::Http.request_timeout = 1000
134
122
  Pebblebed::Http.read_timeout = 1
135
123
  expect {
136
124
  Pebblebed::Http.get(pebble_url, {slow: '30'})
137
- }.to raise_error(Curl::Err::TimeoutError)
125
+ }.to raise_error(Pebblebed::HttpTimeoutError)
138
126
  expect {
139
127
  Pebblebed::Http.get(pebble_url, {slow: '0.5'})
140
128
  }.not_to raise_error
141
129
  end
142
130
 
131
+ describe 'retrying' do
132
+ run_count = 0
133
+
134
+ before do
135
+ Excon.stub({:method => :post}) {
136
+ raise Excon::Errors::SocketError.new(Exception.new "Mock Error")
137
+ }
138
+ Excon.stub({:method => :get}) { |params|
139
+ run_count += 1
140
+ if run_count <= 2
141
+ raise Excon::Errors::SocketError.new(Exception.new "Mock Error")
142
+ end
143
+ {:body => params[:body], :headers => params[:headers], :status => 200}
144
+ }
145
+ end
146
+
147
+ after do
148
+ Excon.stubs.clear
149
+ end
150
+
151
+ it "post with error doesn't try again" do
152
+ expect {
153
+ Pebblebed::Http.post(pebble_url, {hello: 'world'})
154
+ }.to raise_error(Pebblebed::HttpSocketError)
155
+ end
156
+
157
+ it "get request tries one more time" do
158
+ expect {
159
+ Pebblebed::Http.get(pebble_url, {hello: 'world'})
160
+ }.to raise_error(Pebblebed::HttpSocketError)
161
+ expect(run_count).to equal(2)
162
+ end
163
+
164
+ end
143
165
  end
data/spec/spec_helper.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  require 'simplecov'
2
2
  require 'rspec'
3
+ require 'webmock/rspec'
3
4
 
4
5
  SimpleCov.add_filter 'spec'
5
6
  SimpleCov.add_filter 'config'
@@ -11,6 +12,7 @@ require './spec/mock_pebble'
11
12
  RSpec.configure do |c|
12
13
  c.mock_with :rspec
13
14
  c.before(:each) do
15
+ WebMock.allow_net_connect!
14
16
  ::Pebblebed.memcached = MemcacheMock.new
15
17
  end
16
18
  c.around(:each) do |example|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pebblebed
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.26
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katrina Owen
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2018-01-25 00:00:00.000000000 Z
12
+ date: 2018-03-22 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -95,6 +95,20 @@ dependencies:
95
95
  - - ">="
96
96
  - !ruby/object:Gem::Version
97
97
  version: '0'
98
+ - !ruby/object:Gem::Dependency
99
+ name: webmock
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - ">="
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ type: :development
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
98
112
  - !ruby/object:Gem::Dependency
99
113
  name: deepstruct
100
114
  requirement: !ruby/object:Gem::Requirement
@@ -110,19 +124,19 @@ dependencies:
110
124
  - !ruby/object:Gem::Version
111
125
  version: 0.0.4
112
126
  - !ruby/object:Gem::Dependency
113
- name: curb
127
+ name: excon
114
128
  requirement: !ruby/object:Gem::Requirement
115
129
  requirements:
116
130
  - - ">="
117
131
  - !ruby/object:Gem::Version
118
- version: 0.8.8
132
+ version: 0.52.0
119
133
  type: :runtime
120
134
  prerelease: false
121
135
  version_requirements: !ruby/object:Gem::Requirement
122
136
  requirements:
123
137
  - - ">="
124
138
  - !ruby/object:Gem::Version
125
- version: 0.8.8
139
+ version: 0.52.0
126
140
  - !ruby/object:Gem::Dependency
127
141
  name: yajl-ruby
128
142
  requirement: !ruby/object:Gem::Requirement