routemaster-drain 1.0.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.
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