routemaster-drain 2.3.0 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +19 -0
  3. data/.env.test +2 -2
  4. data/.rubocop.yml +1156 -0
  5. data/.ruby-version +1 -1
  6. data/.travis.yml +8 -0
  7. data/Appraisals +3 -3
  8. data/CHANGELOG.md +31 -5
  9. data/Gemfile +7 -6
  10. data/Gemfile.lock +23 -17
  11. data/README.md +19 -0
  12. data/appraise +28 -0
  13. data/gemfiles/rails_3.gemfile +8 -8
  14. data/gemfiles/rails_3.gemfile.lock +64 -58
  15. data/gemfiles/rails_4.gemfile +8 -8
  16. data/gemfiles/rails_4.gemfile.lock +121 -92
  17. data/gemfiles/rails_5.gemfile +8 -8
  18. data/gemfiles/rails_5.gemfile.lock +78 -72
  19. data/lib/core_ext/forwardable.rb +14 -0
  20. data/lib/routemaster/api_client.rb +65 -36
  21. data/lib/routemaster/cache.rb +7 -1
  22. data/lib/routemaster/cache_key.rb +7 -0
  23. data/lib/routemaster/config.rb +12 -13
  24. data/lib/routemaster/dirty/map.rb +1 -1
  25. data/lib/routemaster/drain.rb +1 -1
  26. data/lib/routemaster/event_index.rb +21 -0
  27. data/lib/routemaster/jobs.rb +2 -0
  28. data/lib/routemaster/jobs/cache_and_sweep.rb +2 -1
  29. data/lib/routemaster/jobs/job.rb +2 -0
  30. data/lib/routemaster/middleware/cache.rb +2 -5
  31. data/lib/routemaster/middleware/parse.rb +2 -2
  32. data/lib/routemaster/middleware/response_caching.rb +54 -24
  33. data/lib/routemaster/null_logger.rb +16 -0
  34. data/lib/routemaster/redis_broker.rb +8 -7
  35. data/lib/routemaster/resources/rest_resource.rb +18 -7
  36. data/lib/routemaster/responses/future_response.rb +37 -17
  37. data/lib/routemaster/responses/hateoas_enumerable_response.rb +47 -0
  38. data/lib/routemaster/responses/hateoas_response.rb +9 -12
  39. data/routemaster-drain.gemspec +2 -2
  40. data/spec/routemaster/api_client_spec.rb +118 -44
  41. data/spec/routemaster/drain/caching_spec.rb +4 -3
  42. data/spec/routemaster/integration/api_client_spec.rb +266 -102
  43. data/spec/routemaster/integration/cache_spec.rb +52 -39
  44. data/spec/routemaster/middleware/cache_spec.rb +4 -6
  45. data/spec/routemaster/redis_broker_spec.rb +11 -11
  46. data/spec/routemaster/resources/rest_resource_spec.rb +4 -2
  47. data/spec/routemaster/responses/future_response_spec.rb +18 -0
  48. data/spec/routemaster/responses/hateoas_enumerable_response_spec.rb +78 -0
  49. data/spec/routemaster/responses/hateoas_response_spec.rb +52 -53
  50. data/spec/spec_helper.rb +2 -1
  51. data/spec/support/breakpoint_class.rb +14 -0
  52. data/spec/support/server.rb +52 -0
  53. data/spec/support/uses_redis.rb +2 -2
  54. metadata +26 -10
  55. data/test.rb +0 -17
@@ -34,8 +34,10 @@ describe Routemaster::Drain::Caching do
34
34
  perform
35
35
  end
36
36
 
37
- it 'busts the cache' do
38
- expect_any_instance_of(Routemaster::Cache).to receive(:bust).exactly(3).times
37
+ it 'increments the event index' do
38
+ ei_double = double(increment: 1)
39
+ allow(Routemaster::EventIndex).to receive(:new).and_return(ei_double)
40
+ expect(ei_double).to receive(:increment).exactly(3).times
39
41
  perform
40
42
  end
41
43
 
@@ -44,4 +46,3 @@ describe Routemaster::Drain::Caching do
44
46
  perform
45
47
  end
46
48
  end
47
-
@@ -1,24 +1,25 @@
1
1
  require 'spec_helper'
2
2
  require 'spec/support/uses_redis'
3
3
  require 'spec/support/uses_dotenv'
4
+ require 'spec/support/uses_webmock'
5
+ require 'spec/support/server'
6
+ require 'spec/support/breakpoint_class'
4
7
  require 'routemaster/api_client'
5
- require 'webrick'
8
+ require 'routemaster/cache'
9
+ require 'dogstatsd'
6
10
 
7
- RSpec.describe 'Api client integration specs' do
8
- module WEBrick
9
- module HTTPServlet
10
- class ProcHandler
11
- alias do_PATCH do_GET
12
- end
13
- end
11
+ describe Routemaster::APIClient do
12
+ def now
13
+ (Time.now.to_f * 1e6).to_i
14
14
  end
15
15
 
16
16
  uses_dotenv
17
17
  uses_redis
18
+ uses_webmock
18
19
 
19
- let!(:log) { WEBrick::Log.new '/dev/null' }
20
+ let(:port) { 8000 }
20
21
  let(:service) do
21
- WEBrick::HTTPServer.new(Port: 8000, DocumentRoot: Dir.pwd, Logger: log).tap do |server|
22
+ TestServer.new(port) do |server|
22
23
  [400, 401, 403, 404, 409, 412, 413, 429, 500].each do |status_code|
23
24
  server.mount_proc "/#{status_code}" do |req, res|
24
25
  res.status = status_code
@@ -30,127 +31,285 @@ RSpec.describe 'Api client integration specs' do
30
31
  res.status = 200
31
32
  res.body = { field: 'test' }.to_json
32
33
  end
33
- end
34
- end
35
34
 
36
- before do
37
- @pid = fork do
38
- trap 'INT' do service.shutdown end
39
- service.start
35
+ server.mount_proc "/resources/1" do |req, res|
36
+ res['Content-Type'] = 'application/json'
37
+ res.status = 200
38
+ res.body = { attribute: 'value', updated_at: now }.to_json
39
+ end
40
+
41
+ server.mount_proc "/discover" do |req, res|
42
+ res['Content-Type'] = 'application/json'
43
+ res.status = 200
44
+ res.body = { _links: { resources: { href: "http://localhost:#{port}/resources" } } }.to_json
45
+ end
46
+
47
+ server.mount_proc "/resources" do |req, res|
48
+ res['Content-Type'] = 'application/json'
49
+ res.status = 200
50
+ case req.query_string
51
+ when "first_name=roo"
52
+ res.body = {
53
+ _links: {
54
+ self: {
55
+ href: "http://localhost:#{port}/resourcess?first_name=roo&page=1&per_page=2"
56
+ },
57
+ first: {
58
+ href: "http://localhost:#{port}/resources?first_name=roo&page=1&per_page=2"
59
+ },
60
+ last: {
61
+ href: "http://localhost:#{port}/resources?first_name=roo&page=3&per_page=2"
62
+ },
63
+ next: {
64
+ href: "http://localhost:#{port}/resources?first_name=roo&page=2&per_page=2"
65
+ },
66
+ resources: [
67
+ { href: "http://localhost:#{port}/resources/1" },
68
+ { href: "http://localhost:#{port}/resources/1" }
69
+ ]
70
+ }
71
+ }.to_json
72
+ when "first_name=roo&page=2&per_page=2"
73
+ res.body = {
74
+ _links: {
75
+ self: {
76
+ href: "http://localhost:#{port}/resourcess?first_name=roo&page=2&per_page=2"
77
+ },
78
+ first: {
79
+ href: "http://localhost:#{port}/resources?first_name=roo&page=1&per_page=2"
80
+ },
81
+ last: {
82
+ href: "http://localhost:#{port}/resources?first_name=roo&page=3&per_page=2"
83
+ },
84
+ next: {
85
+ href: "http://localhost:#{port}/resources?first_name=roo&page=3&per_page=2"
86
+ },
87
+ resources: [
88
+ { href: "http://localhost:#{port}/resources/1" },
89
+ { href: "http://localhost:#{port}/resources/1" }
90
+ ]
91
+ }
92
+ }.to_json
93
+ when "first_name=roo&page=3&per_page=2"
94
+ res.body = {
95
+ _links: {
96
+ self: {
97
+ href: "http://localhost:#{port}/resourcess?first_name=roo&page=3&per_page=2"
98
+ },
99
+ first: {
100
+ href: "http://localhost:#{port}/resources?first_name=roo&page=1&per_page=2"
101
+ },
102
+ last: {
103
+ href: "http://localhost:#{port}/resources?first_name=roo&page=3&per_page=2"
104
+ },
105
+ resources: [
106
+ { href: "http://localhost:#{port}/resources/1" }
107
+ ]
108
+ }
109
+ }.to_json
110
+ end
111
+ end
40
112
  end
41
- sleep(0.5) # leave sometime for the previous webrick to teardown
42
113
  end
43
114
 
44
- after do
45
- Process.kill('KILL', @pid)
46
- Process.wait(@pid)
47
- end
115
+ before { service.start }
116
+ after { service.stop }
48
117
 
49
- subject { Routemaster::APIClient.new }
50
- let(:host) { 'http://localhost:8000' }
118
+ before { WebMock.disable_net_connect!(allow_localhost: true) }
119
+
120
+ let(:host) { "http://localhost:#{port}" }
51
121
 
52
122
  describe 'error handling' do
53
- it 'raises an ResourceNotFound on 404' do
54
- expect { subject.get(host + '/404') }.to raise_error(Routemaster::Errors::ResourceNotFound)
55
- end
56
123
 
57
- it 'raises an InvalidResource on 400' do
58
- expect { subject.get(host + '/400') }.to raise_error(Routemaster::Errors::InvalidResource)
124
+ shared_examples 'exception raiser' do
125
+ it 'raises an ResourceNotFound on 404' do
126
+ expect { perform.(host + '/404') }.to raise_error(Routemaster::Errors::ResourceNotFound)
127
+ end
128
+
129
+ it 'raises an InvalidResource on 400' do
130
+ expect { perform.(host + '/400') }.to raise_error(Routemaster::Errors::InvalidResource)
131
+ end
132
+
133
+ it 'raises an UnauthorizedResourceAccess on 401' do
134
+ expect { perform.(host + '/401') }.to raise_error(Routemaster::Errors::UnauthorizedResourceAccess)
135
+ end
136
+
137
+ it 'raises an UnauthorizedResourceAccess on 403' do
138
+ expect { perform.(host + '/403') }.to raise_error(Routemaster::Errors::UnauthorizedResourceAccess)
139
+ end
140
+
141
+ it 'raises an ConflictResource on 409' do
142
+ expect { perform.(host + '/409') }.to raise_error(Routemaster::Errors::ConflictResource)
143
+ end
144
+
145
+ it 'raises an IncompatibleVersion on 412' do
146
+ expect { perform.(host + '/412') }.to raise_error(Routemaster::Errors::IncompatibleVersion)
147
+ end
148
+
149
+ it 'raises an InvalidResource on 413' do
150
+ expect { perform.(host + '/413') }.to raise_error(Routemaster::Errors::InvalidResource)
151
+ end
152
+
153
+ it 'raises an ResourceThrottling on 429' do
154
+ expect { perform.(host + '/429') }.to raise_error(Routemaster::Errors::ResourceThrottling)
155
+ end
156
+
157
+ it 'raises an FatalResource on 500' do
158
+ expect { perform.(host + '/500') }.to raise_error(Routemaster::Errors::FatalResource)
159
+ end
160
+ end
161
+
162
+ describe '#get' do
163
+ let(:perform) { ->(uri) { subject.get(uri) } }
164
+ include_examples 'exception raiser'
59
165
  end
60
166
 
61
- it 'raises an UnauthorizedResourceAccess on 401' do
62
- expect { subject.get(host + '/401') }.to raise_error(Routemaster::Errors::UnauthorizedResourceAccess)
167
+ describe '#fget' do
168
+ let(:perform) { ->(uri) { subject.fget(uri).value } }
169
+ include_examples 'exception raiser'
63
170
  end
171
+ end
172
+
173
+
174
+
175
+ describe 'caching behaviour' do
176
+ let(:url) { "#{host}/resources/1" }
177
+ def timestamp ; subject.get(url).body.updated_at ; end
178
+
179
+ describe 'GET requests' do
180
+ context 'when the resource was fetched' do
181
+ let!(:cached_stamp) { timestamp }
182
+ let(:fetched_stamp) { timestamp }
183
+
184
+ it 'returns the cached response' do
185
+ expect(fetched_stamp).to eq(cached_stamp)
186
+ end
64
187
 
65
- it 'raises an UnauthorizedResourceAccess on 403' do
66
- expect { subject.get(host + '/403') }.to raise_error(Routemaster::Errors::UnauthorizedResourceAccess)
188
+ context 'when the cache gets busted' do
189
+ before { Routemaster::Cache.new.bust(url) }
190
+
191
+ it 'returns a fresh response' do
192
+ expect(fetched_stamp).to be > cached_stamp
193
+ end
194
+ end
195
+
196
+ context 'when the cache gets invalidated' do
197
+ before { Routemaster::Cache.new.invalidate(url) }
198
+
199
+ it 'returns a fresh response' do
200
+ expect(fetched_stamp).to be > cached_stamp
201
+ end
202
+ end
203
+ end
67
204
  end
68
205
 
69
- it 'raises an ConflictResource on 409' do
70
- expect { subject.get(host + '/409') }.to raise_error(Routemaster::Errors::ConflictResource)
206
+ describe 'PATCH request' do
207
+ context 'when the resource was fetched' do
208
+ let!(:cached_stamp) { timestamp }
209
+ let(:fetched_stamp) { timestamp }
210
+
211
+ it 'invalidates the cache on update' do
212
+ subject.patch(url, body: {})
213
+ expect(fetched_stamp).to be > cached_stamp
214
+ end
215
+ end
71
216
  end
72
217
 
73
- it 'raises an IncompatibleVersion on 412' do
74
- expect { subject.get(host + '/412') }.to raise_error(Routemaster::Errors::IncompatibleVersion)
218
+ describe 'DELETE request' do
219
+ context 'when the resource was fetched' do
220
+ let!(:cached_stamp) { timestamp }
221
+ let(:fetched_stamp) { timestamp }
222
+
223
+ it 'invalidates the cache on destroy' do
224
+ subject.delete(url)
225
+ expect(fetched_stamp).to be > cached_stamp
226
+ end
227
+ end
75
228
  end
229
+ end
76
230
 
77
- it 'raises an InvalidResource on 413' do
78
- expect { subject.get(host + '/413') }.to raise_error(Routemaster::Errors::InvalidResource)
231
+ describe 'interleaved requests' do
232
+ let(:url) { "#{host}/resources/1" }
233
+
234
+ let(:processes) do
235
+ Array.new(2) do
236
+ ForkBreak::Process.new do
237
+ breakpoint_class(Routemaster::Middleware::ResponseCaching, :fetch_from_service)
238
+ Routemaster::Cache.new.send(cache_method, url)
239
+ subject.get(url).body
240
+ end
241
+ end
79
242
  end
80
243
 
81
- it 'raises an ResourceThrottling on 429' do
82
- expect { subject.get(host + '/429') }.to raise_error(Routemaster::Errors::ResourceThrottling)
244
+ let(:first_timestamp) do
245
+ processes[0].return_value.updated_at
83
246
  end
84
247
 
85
- it 'raises an FatalResource on 500' do
86
- expect { subject.get(host + '/500') }.to raise_error(Routemaster::Errors::FatalResource)
248
+ let(:second_timestamp) do
249
+ processes[1].return_value.updated_at
87
250
  end
88
- end
89
251
 
90
- describe 'Future GET request' do
91
- let(:body_cache_keys) { ["cache:#{url}", "v:,l:,body"] }
92
- let(:headers_cache_keys) { ["cache:#{url}", "v:,l:,headers"] }
93
- let(:url) { "#{host}/success" }
252
+ let(:fresh_timestamp) do
253
+ subject.get(url).body.updated_at
254
+ end
94
255
 
95
- context 'when there is a previous cached resource' do
96
- let(:cache) { Routemaster::Config.cache_redis }
256
+ before do
257
+ processes.first.run_until(:before_fetch_from_service).wait
258
+ processes.last.finish.wait
259
+ processes.first.finish.wait
260
+ end
97
261
 
98
- it 'returns the response from within the future' do
99
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be_nil
262
+ context 'the cache is busted between requests' do
263
+ let(:cache_method) { :bust }
100
264
 
101
- future = subject.fget(url)
102
- expect(future).to be_an_instance_of(Routemaster::Responses::FutureResponse)
265
+ it 'should return the first_timestamp' do
266
+ expect(first_timestamp).to eq fresh_timestamp
267
+ end
103
268
 
104
- future.value
105
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be
269
+ it 'returns a second timestamp older than the first' do
270
+ expect(second_timestamp).to be < first_timestamp
106
271
  end
107
272
  end
108
- end
109
273
 
110
- describe 'PATCH request' do
111
- let(:body_cache_keys) { ["cache:#{url}", "v:,l:,body"] }
112
- let(:headers_cache_keys) { ["cache:#{url}", "v:,l:,headers"] }
113
- let(:url) { "#{host}/success" }
274
+ context 'the cache is invalidated between requests' do
275
+ let(:cache_method) { :invalidate }
114
276
 
115
- context 'when there is a previous cached resource' do
116
- before { subject.get(url) }
117
- let(:cache) { Routemaster::Config.cache_redis }
118
-
119
- it 'invalidates the cache on update' do
120
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be
121
- subject.patch(url, body: {})
277
+ it 'returns a second timestamp older than the first' do
278
+ expect(second_timestamp).to be < first_timestamp
279
+ end
122
280
 
123
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be_nil
281
+ it 'returns an invalid first request' do
282
+ expect(first_timestamp).to be < fresh_timestamp
283
+ end
124
284
 
125
- subject.get(url)
126
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be
285
+ it 'returns an invalid second request' do
286
+ expect(second_timestamp).to be < fresh_timestamp
127
287
  end
128
288
  end
129
289
  end
130
290
 
131
- describe 'DELETE request' do
132
- let(:body_cache_keys) { ["cache:#{url}", "v:,l:,body"] }
133
- let(:headers_cache_keys) { ["cache:#{url}", "v:,l:,headers"] }
134
- let(:url) { "#{host}/success" }
291
+ describe 'INDEX request' do
292
+ let(:url) { "http://localhost:#{port}/discover" }
135
293
 
136
- context 'when there is a previous cached resource' do
137
- before { subject.get(url) }
138
- let(:cache) { Routemaster::Config.cache_redis }
294
+ subject do
295
+ Routemaster::APIClient.new(response_class: Routemaster::Responses::HateoasResponse)
296
+ end
139
297
 
140
- it 'invalidates the cache on destroy' do
141
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be
142
- subject.delete(url)
298
+ it 'traverses through pagination next all links that match the request params' do
299
+ res = subject.discover(url)
300
+ expect(res.resources.index(filters: { first_name: 'roo' }).count).to eq(5)
301
+ end
143
302
 
144
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be_nil
303
+ it 'does not make any http requests to fetch resources any if just the index method is called' do
304
+ resources = subject.discover(url).resources
145
305
 
146
- subject.get(url)
147
- expect(cache.hget("cache:#{url}", "v:,l:,body")).to be
148
- end
306
+ expect(subject).to receive(:get).with("http://localhost:#{port}/resources", anything).once
307
+ resources.index
149
308
  end
150
309
  end
151
310
 
152
- describe 'Telemetry' do
153
- let(:metrics_client) { double('MetricsClient') }
311
+ describe 'telemetry' do
312
+ let(:metrics_client) { Dogstatsd.new }
154
313
  let(:source_peer) { 'test_service' }
155
314
  let(:url) { "#{host}/success" }
156
315
 
@@ -159,34 +318,39 @@ RSpec.describe 'Api client integration specs' do
159
318
  source_peer: source_peer)
160
319
  end
161
320
 
321
+ before do
322
+ allow(metrics_client).to receive(:increment).and_call_original
323
+ allow(metrics_client).to receive(:time).and_call_original
324
+ end
325
+
162
326
  context 'when metrics source peer is absent' do
163
- subject { Routemaster::APIClient.new(metrics_client: metrics_client) }
327
+ let(:source_peer) { nil }
164
328
 
165
329
  it 'does not send metrics' do
166
- expect(metrics_client).to receive(:increment).never
167
330
  subject.get(url)
331
+ expect(metrics_client).not_to have_received(:increment)
168
332
  end
169
333
  end
170
334
 
171
- it 'does send request metrics' do
172
- allow(metrics_client).to receive(:time).and_yield
173
- allow(metrics_client).to receive(:increment)
174
- expected_req_count_tags = ["source:test_service", "destination:localhost", "verb:get"]
175
-
176
- expect(metrics_client).to receive(:increment).with('api_client.request.count', tags: expected_req_count_tags)
177
-
335
+ it 'sends request metrics' do
178
336
  subject.get(url)
337
+ expect(metrics_client).to have_received(:increment).with(
338
+ 'api_client.request.count', tags: %w[source:test_service destination:localhost verb:get]
339
+ )
179
340
  end
180
341
 
181
- it 'does send response metrics' do
182
- allow(metrics_client).to receive(:increment)
183
- expected_res_count_tags = ["source:test_service", "destination:localhost", "status:200"]
184
- expected_latency_tags = ["source:test_service", "destination:localhost", "verb:get"]
185
-
186
- expect(metrics_client).to receive(:increment).with('api_client.response.count', tags: expected_res_count_tags)
187
- expect(metrics_client).to receive(:time).with('api_client.latency', tags: expected_latency_tags).and_yield
342
+ it 'sends response metrics' do
343
+ subject.get(url)
344
+ expect(metrics_client).to have_received(:increment).with(
345
+ 'api_client.response.count', tags: %w[source:test_service destination:localhost status:200]
346
+ )
347
+ end
188
348
 
349
+ it 'sends timing metrics' do
189
350
  subject.get(url)
351
+ expect(metrics_client).to have_received(:time).with(
352
+ 'api_client.latency', tags: %w[source:test_service destination:localhost verb:get]
353
+ )
190
354
  end
191
355
  end
192
356
  end