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.
- checksums.yaml +4 -4
- data/.codeclimate.yml +19 -0
- data/.env.test +2 -2
- data/.rubocop.yml +1156 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -0
- data/Appraisals +3 -3
- data/CHANGELOG.md +31 -5
- data/Gemfile +7 -6
- data/Gemfile.lock +23 -17
- data/README.md +19 -0
- data/appraise +28 -0
- data/gemfiles/rails_3.gemfile +8 -8
- data/gemfiles/rails_3.gemfile.lock +64 -58
- data/gemfiles/rails_4.gemfile +8 -8
- data/gemfiles/rails_4.gemfile.lock +121 -92
- data/gemfiles/rails_5.gemfile +8 -8
- data/gemfiles/rails_5.gemfile.lock +78 -72
- data/lib/core_ext/forwardable.rb +14 -0
- data/lib/routemaster/api_client.rb +65 -36
- data/lib/routemaster/cache.rb +7 -1
- data/lib/routemaster/cache_key.rb +7 -0
- data/lib/routemaster/config.rb +12 -13
- data/lib/routemaster/dirty/map.rb +1 -1
- data/lib/routemaster/drain.rb +1 -1
- data/lib/routemaster/event_index.rb +21 -0
- data/lib/routemaster/jobs.rb +2 -0
- data/lib/routemaster/jobs/cache_and_sweep.rb +2 -1
- data/lib/routemaster/jobs/job.rb +2 -0
- data/lib/routemaster/middleware/cache.rb +2 -5
- data/lib/routemaster/middleware/parse.rb +2 -2
- data/lib/routemaster/middleware/response_caching.rb +54 -24
- data/lib/routemaster/null_logger.rb +16 -0
- data/lib/routemaster/redis_broker.rb +8 -7
- data/lib/routemaster/resources/rest_resource.rb +18 -7
- data/lib/routemaster/responses/future_response.rb +37 -17
- data/lib/routemaster/responses/hateoas_enumerable_response.rb +47 -0
- data/lib/routemaster/responses/hateoas_response.rb +9 -12
- data/routemaster-drain.gemspec +2 -2
- data/spec/routemaster/api_client_spec.rb +118 -44
- data/spec/routemaster/drain/caching_spec.rb +4 -3
- data/spec/routemaster/integration/api_client_spec.rb +266 -102
- data/spec/routemaster/integration/cache_spec.rb +52 -39
- data/spec/routemaster/middleware/cache_spec.rb +4 -6
- data/spec/routemaster/redis_broker_spec.rb +11 -11
- data/spec/routemaster/resources/rest_resource_spec.rb +4 -2
- data/spec/routemaster/responses/future_response_spec.rb +18 -0
- data/spec/routemaster/responses/hateoas_enumerable_response_spec.rb +78 -0
- data/spec/routemaster/responses/hateoas_response_spec.rb +52 -53
- data/spec/spec_helper.rb +2 -1
- data/spec/support/breakpoint_class.rb +14 -0
- data/spec/support/server.rb +52 -0
- data/spec/support/uses_redis.rb +2 -2
- metadata +26 -10
- data/test.rb +0 -17
@@ -34,8 +34,10 @@ describe Routemaster::Drain::Caching do
|
|
34
34
|
perform
|
35
35
|
end
|
36
36
|
|
37
|
-
it '
|
38
|
-
|
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 '
|
8
|
+
require 'routemaster/cache'
|
9
|
+
require 'dogstatsd'
|
6
10
|
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
20
|
+
let(:port) { 8000 }
|
20
21
|
let(:service) do
|
21
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
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
|
-
|
45
|
-
|
46
|
-
Process.wait(@pid)
|
47
|
-
end
|
115
|
+
before { service.start }
|
116
|
+
after { service.stop }
|
48
117
|
|
49
|
-
|
50
|
-
|
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
|
-
|
58
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
66
|
-
|
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
|
-
|
70
|
-
|
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
|
-
|
74
|
-
|
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
|
-
|
78
|
-
|
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
|
-
|
82
|
-
|
244
|
+
let(:first_timestamp) do
|
245
|
+
processes[0].return_value.updated_at
|
83
246
|
end
|
84
247
|
|
85
|
-
|
86
|
-
|
248
|
+
let(:second_timestamp) do
|
249
|
+
processes[1].return_value.updated_at
|
87
250
|
end
|
88
|
-
end
|
89
251
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
let(:url) { "#{host}/success" }
|
252
|
+
let(:fresh_timestamp) do
|
253
|
+
subject.get(url).body.updated_at
|
254
|
+
end
|
94
255
|
|
95
|
-
|
96
|
-
|
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
|
-
|
99
|
-
|
262
|
+
context 'the cache is busted between requests' do
|
263
|
+
let(:cache_method) { :bust }
|
100
264
|
|
101
|
-
|
102
|
-
expect(
|
265
|
+
it 'should return the first_timestamp' do
|
266
|
+
expect(first_timestamp).to eq fresh_timestamp
|
267
|
+
end
|
103
268
|
|
104
|
-
|
105
|
-
expect(
|
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
|
-
|
111
|
-
|
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
|
-
|
116
|
-
|
117
|
-
|
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
|
-
|
281
|
+
it 'returns an invalid first request' do
|
282
|
+
expect(first_timestamp).to be < fresh_timestamp
|
283
|
+
end
|
124
284
|
|
125
|
-
|
126
|
-
expect(
|
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 '
|
132
|
-
let(:
|
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
|
-
|
137
|
-
|
138
|
-
|
294
|
+
subject do
|
295
|
+
Routemaster::APIClient.new(response_class: Routemaster::Responses::HateoasResponse)
|
296
|
+
end
|
139
297
|
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
-
|
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
|
-
|
147
|
-
|
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 '
|
153
|
-
let(:metrics_client) {
|
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
|
-
|
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 '
|
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 '
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|