routemaster-drain 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.env.test +6 -0
  3. data/.gitignore +7 -0
  4. data/.gitmodules +3 -0
  5. data/.rspec +3 -0
  6. data/.ruby-version +1 -0
  7. data/.travis.yml +16 -0
  8. data/.yardopts +6 -0
  9. data/Gemfile +17 -0
  10. data/Gemfile.lock +122 -0
  11. data/Guardfile +10 -0
  12. data/LICENSE.txt +22 -0
  13. data/README.md +261 -0
  14. data/Rakefile +1 -0
  15. data/lib/routemaster/cache.rb +133 -0
  16. data/lib/routemaster/config.rb +57 -0
  17. data/lib/routemaster/dirty/filter.rb +49 -0
  18. data/lib/routemaster/dirty/map.rb +63 -0
  19. data/lib/routemaster/dirty/state.rb +33 -0
  20. data/lib/routemaster/drain.rb +5 -0
  21. data/lib/routemaster/drain/basic.rb +40 -0
  22. data/lib/routemaster/drain/caching.rb +43 -0
  23. data/lib/routemaster/drain/mapping.rb +43 -0
  24. data/lib/routemaster/drain/terminator.rb +31 -0
  25. data/lib/routemaster/fetcher.rb +63 -0
  26. data/lib/routemaster/jobs/cache.rb +12 -0
  27. data/lib/routemaster/jobs/cache_and_sweep.rb +16 -0
  28. data/lib/routemaster/middleware/authenticate.rb +59 -0
  29. data/lib/routemaster/middleware/cache.rb +29 -0
  30. data/lib/routemaster/middleware/dirty.rb +33 -0
  31. data/lib/routemaster/middleware/filter.rb +26 -0
  32. data/lib/routemaster/middleware/parse.rb +43 -0
  33. data/lib/routemaster/middleware/root_post_only.rb +18 -0
  34. data/lib/routemaster/redis_broker.rb +42 -0
  35. data/routemaster-drain.gemspec +28 -0
  36. data/spec/routemaster/cache_spec.rb +115 -0
  37. data/spec/routemaster/dirty/filter_spec.rb +77 -0
  38. data/spec/routemaster/dirty/map_spec.rb +122 -0
  39. data/spec/routemaster/dirty/state_spec.rb +41 -0
  40. data/spec/routemaster/drain/basic_spec.rb +37 -0
  41. data/spec/routemaster/drain/caching_spec.rb +47 -0
  42. data/spec/routemaster/drain/mapping_spec.rb +51 -0
  43. data/spec/routemaster/drain/terminator_spec.rb +61 -0
  44. data/spec/routemaster/fetcher_spec.rb +56 -0
  45. data/spec/routemaster/middleware/authenticate_spec.rb +59 -0
  46. data/spec/routemaster/middleware/cache_spec.rb +35 -0
  47. data/spec/routemaster/middleware/dirty_spec.rb +33 -0
  48. data/spec/routemaster/middleware/filter_spec.rb +35 -0
  49. data/spec/routemaster/middleware/parse_spec.rb +69 -0
  50. data/spec/routemaster/middleware/root_post_only_spec.rb +30 -0
  51. data/spec/spec_helper.rb +16 -0
  52. data/spec/support/events.rb +9 -0
  53. data/spec/support/rack_test.rb +23 -0
  54. data/spec/support/uses_dotenv.rb +11 -0
  55. data/spec/support/uses_redis.rb +15 -0
  56. data/spec/support/uses_webmock.rb +12 -0
  57. data/test.rb +17 -0
  58. metadata +247 -0
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/uses_redis'
3
+ require 'routemaster/dirty/filter'
4
+
5
+ describe Routemaster::Dirty::Filter do
6
+ uses_redis
7
+
8
+ def make_url(idx) ; "https://example.com/#{idx}" ; end
9
+
10
+ describe '#initialize' do
11
+ it 'passes without options' do
12
+ expect { described_class.new }.not_to raise_error
13
+ end
14
+
15
+ it 'accepts :redis' do
16
+ expect { described_class.new(redis: double) }.not_to raise_error
17
+ end
18
+ end
19
+
20
+ describe '#run' do
21
+ let(:options) {{ redis: redis }}
22
+ subject { described_class.new(options) }
23
+
24
+ let(:result) { subject.run(payload) }
25
+ let(:payload) { [] }
26
+
27
+ context 'blank slate' do
28
+ %w(create delete update).each do |event|
29
+ let(:url) { make_url(1) }
30
+
31
+ it "keeps a '#{event}' event" do
32
+ event = { 'topic' => 'stuff', 'type' => event, 'url' => url, 't' => 1234 }
33
+ payload.push event
34
+ expect(result).to include(event)
35
+ end
36
+ end
37
+ end
38
+
39
+ context 'with a prior event' do
40
+ let(:url) { make_url(1) }
41
+ let(:prior_event) {{ 'topic' => 'stuff', 'type' => 'update', 'url' => url, 't' => 1234 }}
42
+
43
+ before { payload.push prior_event }
44
+
45
+ it "keeps a newer event" do
46
+ newer_event = { 'topic' => 'stuff', 'type' => 'update', 'url' => url, 't' => 1235 }
47
+ payload.push newer_event
48
+ expect(result).to eq([newer_event])
49
+ end
50
+
51
+ it "does keep an older event" do
52
+ older_event = { 'topic' => 'stuff', 'type' => 'update', 'url' => url, 't' => 1233 }
53
+ payload.push older_event
54
+ expect(result).to eq([prior_event])
55
+ end
56
+ end
57
+
58
+ context 'with a prior state' do
59
+ let(:url) { make_url(1) }
60
+ let(:prior_event) {{ 'topic' => 'stuff', 'type' => 'update', 'url' => url, 't' => 1234 }}
61
+
62
+ before { subject.run([prior_event]) }
63
+
64
+ it "keeps a newer event" do
65
+ newer_event = { 'topic' => 'stuff', 'type' => 'update', 'url' => url, 't' => 1235 }
66
+ payload.push newer_event
67
+ expect(result).to eq([newer_event])
68
+ end
69
+
70
+ it "does not keep an older event" do
71
+ older_event = { 'topic' => 'stuff', 'type' => 'update', 'url' => url, 't' => 1233 }
72
+ payload.push older_event
73
+ expect(result).to be_empty
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,122 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/uses_redis'
3
+ require 'routemaster/dirty/map'
4
+
5
+ describe Routemaster::Dirty::Map do
6
+ uses_redis
7
+
8
+ subject { described_class.new(redis: redis) }
9
+
10
+ def url(idx) ; "https://example.com/#{idx}" ; end
11
+
12
+ def mark_urls(count)
13
+ 1.upto(count) do |idx|
14
+ subject.mark(url(idx))
15
+ end
16
+ end
17
+
18
+ describe '#mark' do
19
+ it 'passes' do
20
+ expect { subject.mark(url(1)) }.not_to raise_error
21
+ end
22
+
23
+ it 'returns true if marking for the first time' do
24
+ expect(subject.mark(url(1))).to eq(true)
25
+ end
26
+
27
+ it 'returns false if re-marking' do
28
+ subject.mark(url(1))
29
+ expect(subject.mark(url(1))).to eq(false)
30
+ end
31
+
32
+ context 'with a listener' do
33
+ let(:handler) { double }
34
+ before { subject.subscribe(handler, prefix: true) }
35
+
36
+
37
+ it 'broadcasts :dirty_entity on new mark' do
38
+ expect(handler).to receive(:on_dirty_entity).exactly(10).times
39
+ mark_urls(10)
40
+ end
41
+
42
+ it 'does not broadcast on re-marks' do
43
+ mark_urls(5)
44
+ expect(handler).to receive(:on_dirty_entity).exactly(5).times
45
+ mark_urls(10)
46
+ end
47
+ end
48
+ end
49
+
50
+ describe '#sweep' do
51
+ it 'does not yield with no marks' do
52
+ expect { |b| subject.sweep(&b) }.not_to yield_control
53
+ end
54
+
55
+ it 'yields marked URLs' do
56
+ mark_urls(3)
57
+ expect { |b| subject.sweep(&b) }.to yield_control.exactly(3).times
58
+ end
59
+
60
+ it 'does not yield if called again' do
61
+ mark_urls(3)
62
+ subject.sweep { |url| true }
63
+ expect { |b| subject.sweep(&b) }.not_to yield_control
64
+ end
65
+
66
+ it 'honours "next"' do
67
+ mark_urls(10)
68
+ subject.sweep { |url| next if url =~ /3/ ; true }
69
+ expect { |b| subject.sweep(&b) }.to yield_control.exactly(1).times
70
+ end
71
+
72
+ it 'yields the same URL again if the block returns falsy' do
73
+ mark_urls(10)
74
+ subject.sweep { |url| url =~ /7/ ? false : true }
75
+ expect { |b| subject.sweep(&b) }.to yield_with_args(/7/)
76
+ end
77
+
78
+ it 'yields again if the block fails' do
79
+ mark_urls(1)
80
+ expect {
81
+ subject.sweep { |url| raise }
82
+ }.to raise_error(RuntimeError)
83
+ expect { |b| subject.sweep(&b) }.to yield_control.exactly(1).times
84
+ end
85
+ end
86
+
87
+ describe '#sweep_one' do
88
+ it 'takes one URL' do
89
+ mark_urls(3)
90
+ expect { |b| subject.sweep_one(url(1), &b) }.to yield_control.exactly(:once)
91
+ end
92
+
93
+ it 'processes exactly one URL' do
94
+ mark_urls(3)
95
+ subject.sweep_one(url(1)) { true }
96
+ expect(subject.count).to eq(2)
97
+ end
98
+
99
+ it 'does not sweep if block returns falsy' do
100
+ mark_urls(3)
101
+ subject.sweep_one(url(1)) { nil }
102
+ expect(subject.count).to eq(3)
103
+ end
104
+ end
105
+
106
+ describe '#count' do
107
+ it 'is 0 by default' do
108
+ expect(subject.count).to eq(0)
109
+ end
110
+
111
+ it 'increases when marking' do
112
+ expect { mark_urls(10) }.to change { subject.count }.by(10)
113
+ end
114
+
115
+ it 'decreases when sweeping' do
116
+ mark_urls(10)
117
+ limit = 4
118
+ subject.sweep { |url| (limit -= 1) < 0 ? false : true }
119
+ expect(subject.count).to eq(6)
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/uses_redis'
3
+ require 'routemaster/dirty/state'
4
+
5
+ describe Routemaster::Dirty::State do
6
+ uses_redis
7
+
8
+ let(:argv) {[ 'https://example.com/1', 1234 ]}
9
+ subject { described_class.new(*argv) }
10
+
11
+ describe '#initialize' do
12
+ it 'accepts url, exists flag, timestamp in order' do
13
+ expect(subject.url).to eq('https://example.com/1')
14
+ expect(subject.t).to eq(1234)
15
+ end
16
+ end
17
+
18
+ describe '.get' do
19
+ it 'uses different keys for different URLs' do
20
+ keys = Set.new
21
+ allow(redis).to receive(:get) { |key| keys.add(key) ; nil }
22
+ described_class.get(redis, 'https://example.com/1')
23
+ described_class.get(redis, 'https://example.com/2')
24
+ expect(keys.size).to eq(2)
25
+ end
26
+
27
+ it 'return a null state when cache is empty' do
28
+ state = described_class.get(redis, 'https://example.com/1')
29
+ expect(state.url).to eq('https://example.com/1')
30
+ expect(state.t).to eq(0)
31
+ end
32
+ end
33
+
34
+ describe '#save' do
35
+ it 'saves data that can be .get' do
36
+ subject.save(redis, 1)
37
+ loaded = described_class.get(redis, subject.url)
38
+ expect(loaded).to eq(subject)
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,37 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/rack_test'
3
+ require 'spec/support/uses_redis'
4
+ require 'spec/support/uses_dotenv'
5
+ require 'spec/support/events'
6
+ require 'routemaster/drain/basic'
7
+ require 'json'
8
+
9
+ describe Routemaster::Drain::Basic do
10
+ uses_dotenv
11
+ uses_redis
12
+
13
+ let(:app) { described_class.new }
14
+ let(:listener) { double 'listener' }
15
+
16
+ before { app.subscribe(listener, prefix: true) }
17
+
18
+ let(:path) { '/' }
19
+ let(:payload) { [1,2,3,1].map { |idx| make_event(idx) }.to_json }
20
+ let(:environment) {{ 'CONTENT_TYPE' => 'application/json' }}
21
+ let(:perform) { post path, payload, environment }
22
+
23
+ before { authorize 'd3m0', 'x' }
24
+
25
+ it 'succeeds' do
26
+ perform
27
+ expect(last_response.status).to eq(204)
28
+ end
29
+
30
+ it 'emits events' do
31
+ expect(listener).to receive(:on_events_received) do |payload|
32
+ expect(payload.size).to eq(4)
33
+ end
34
+ perform
35
+ end
36
+ end
37
+
@@ -0,0 +1,47 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/rack_test'
3
+ require 'spec/support/uses_redis'
4
+ require 'spec/support/uses_dotenv'
5
+ require 'spec/support/events'
6
+ require 'routemaster/drain/caching'
7
+ require 'json'
8
+
9
+ describe Routemaster::Drain::Caching do
10
+ uses_dotenv
11
+ uses_redis
12
+
13
+ let(:app) { described_class.new }
14
+ let(:listener) { double 'listener' }
15
+
16
+ before { app.subscribe(listener, prefix: true) }
17
+
18
+ let(:path) { '/' }
19
+ let(:payload) { [1,2,3,1].map { |idx| make_event(idx) }.to_json }
20
+ let(:environment) {{ 'CONTENT_TYPE' => 'application/json' }}
21
+ let(:perform) { post path, payload, environment }
22
+
23
+ before { authorize 'd3m0', 'x' }
24
+
25
+ it 'succeeds' do
26
+ perform
27
+ expect(last_response.status).to eq(204)
28
+ end
29
+
30
+ it 'emits events' do
31
+ expect(listener).to receive(:on_events_received) do |payload|
32
+ expect(payload.size).to eq(3)
33
+ end
34
+ perform
35
+ end
36
+
37
+ it 'busts the cache' do
38
+ expect_any_instance_of(Routemaster::Cache).to receive(:bust).exactly(3).times
39
+ perform
40
+ end
41
+
42
+ it 'schedules caching jobs' do
43
+ expect(Resque).to receive(:enqueue_to).exactly(3).times
44
+ perform
45
+ end
46
+ end
47
+
@@ -0,0 +1,51 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/rack_test'
3
+ require 'spec/support/uses_redis'
4
+ require 'spec/support/uses_dotenv'
5
+ require 'spec/support/events'
6
+ require 'routemaster/drain/mapping'
7
+ require 'json'
8
+
9
+ describe Routemaster::Drain::Mapping do
10
+ uses_dotenv
11
+ uses_redis
12
+
13
+ let(:app) { described_class.new }
14
+ let(:listener) { double 'listener' }
15
+
16
+ before { app.subscribe(listener, prefix: true) }
17
+
18
+ let(:path) { '/' }
19
+ let(:payload) { [1,2,3,1].map { |idx| make_event(idx) }.to_json }
20
+ let(:environment) {{ 'CONTENT_TYPE' => 'application/json' }}
21
+ let(:perform) { post path, payload, environment }
22
+
23
+ before { authorize 'd3m0', 'x' }
24
+
25
+ it 'succeeds' do
26
+ perform
27
+ expect(last_response.status).to eq(204)
28
+ end
29
+
30
+ it 'filters events' do
31
+ expect(listener).to receive(:on_events_received) do |payload|
32
+ expect(payload.size).to eq(3) # the second 1 is filtered
33
+ end
34
+ perform
35
+ end
36
+
37
+ let(:map) { Routemaster::Dirty::Map.new }
38
+
39
+ it 'uses dirty map' do
40
+ payload1 = [1,2,3,1].map { |idx| make_event(idx) }.to_json
41
+ payload2 = [1,4,5,2].map { |idx| make_event(idx) }.to_json
42
+
43
+ post path, payload1, environment
44
+ expect(map.count).to eq(3)
45
+ map.sweep_one("https://example.com/stuff/1") { true }
46
+
47
+ post path, payload2, environment
48
+ expect(map.count).to eq(4) # 2, 3, 4, 5 (1 is a repeat event, filtered out)
49
+ end
50
+ end
51
+
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/rack_test'
3
+ require 'spec/support/events'
4
+ require 'routemaster/drain/terminator'
5
+
6
+ describe Routemaster::Drain::Terminator do
7
+ let(:app) { described_class.new }
8
+ let(:listener) { double }
9
+
10
+ let(:perform) do
11
+ post '/whatever', '', 'routemaster.payload' => payload
12
+ end
13
+
14
+ before do
15
+ app.subscribe(listener, prefix: true)
16
+ end
17
+
18
+
19
+ context 'when a payload is present' do
20
+ let(:payload) { [double('event')] }
21
+
22
+ it 'responds 204' do
23
+ perform
24
+ expect(last_response.status).to eq(204)
25
+ end
26
+
27
+ it 'broadcasts :events_received' do
28
+ expect(listener).to receive(:on_events_received).with(payload)
29
+ perform
30
+ end
31
+ end
32
+
33
+ context 'when a payload is present but empty' do
34
+ let(:payload) { [] }
35
+
36
+ it 'responds 204' do
37
+ perform
38
+ expect(last_response.status).to eq(204)
39
+ end
40
+
41
+ it 'does not broadcast :events_received' do
42
+ expect(listener).not_to receive(:on_events_received)
43
+ perform
44
+ end
45
+ end
46
+
47
+ context 'when there is no payload' do
48
+ let(:payload) { nil }
49
+
50
+ it 'responds 400' do
51
+ perform
52
+ expect(last_response.status).to eq(400)
53
+ end
54
+
55
+ it 'does not broadcast' do
56
+ expect(listener).not_to receive(:on_events_received)
57
+ perform
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,56 @@
1
+ require 'spec_helper'
2
+ require 'spec/support/uses_dotenv'
3
+ require 'spec/support/uses_webmock'
4
+ require 'routemaster/fetcher'
5
+ require 'json'
6
+
7
+ describe Routemaster::Fetcher do
8
+ uses_dotenv
9
+ uses_webmock
10
+
11
+ describe '.get' do
12
+ let(:url) { 'https://example.com/widgets/132' }
13
+ let(:headers) {{}}
14
+ subject { described_class.get(url, headers: headers) }
15
+
16
+ before do
17
+ @req = stub_request(:get, /example\.com/).to_return(
18
+ status: 200,
19
+ body: { id: 132, type: 'widget' }.to_json,
20
+ headers: {
21
+ 'content-type' => 'application/json;v=1'
22
+ }
23
+ )
24
+ end
25
+
26
+ it 'GETs from the URL' do
27
+ subject
28
+ expect(@req).to have_been_requested
29
+ end
30
+
31
+ it 'has :status, :headers, :body' do
32
+ expect(subject.status).to eq(200)
33
+ expect(subject.headers).to have_key('content-type')
34
+ expect(subject.body).not_to be_nil
35
+ end
36
+
37
+ it 'mashifies body' do
38
+ expect(subject.body.id).to eq(132)
39
+ end
40
+
41
+ it 'uses auth' do
42
+ subject
43
+ assert_requested(:get, /example/) do |req|
44
+ expect(req.uri.userinfo).to eq('username:s3cr3t')
45
+ end
46
+ end
47
+
48
+ it 'passes headers' do
49
+ headers['x-custom-header'] = 'why do you even'
50
+ subject
51
+ assert_requested(:get, /example/) do |req|
52
+ expect(req.headers).to include('X-Custom-Header')
53
+ end
54
+ end
55
+ end
56
+ end