pebblebed 0.3.26 → 0.4.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
  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