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
@@ -0,0 +1,16 @@
1
+ module Routemaster
2
+ class NullLogger
3
+ def false
4
+ false
5
+ end
6
+
7
+ def noop
8
+
9
+ end
10
+
11
+ [:debug, :info, :warn, :error, :fatal].each do |method|
12
+ alias method :noop
13
+ alias :"#{method}?" :false
14
+ end
15
+ end
16
+ end
@@ -1,4 +1,5 @@
1
1
  require 'redis-namespace'
2
+ require 'redis/distributed'
2
3
  require 'uri'
3
4
  require 'singleton'
4
5
 
@@ -11,13 +12,13 @@ module Routemaster
11
12
  _cleanup
12
13
  end
13
14
 
14
- def get(url)
15
+ def get(name, urls: [])
15
16
  _check_for_fork
16
- @_connections[url] ||= begin
17
- parsed_url = URI.parse(url)
18
- namespace = parsed_url.path.split('/')[2] || 'rm'
19
- Redis::Namespace.new(namespace, redis: Redis.new(url: url))
20
- end
17
+ @_connections[name] ||= begin
18
+ parsed_url = URI.parse(urls.first)
19
+ namespace = parsed_url.path.split('/')[2] || 'rm'
20
+ Redis::Namespace.new(namespace, redis: Redis::Distributed.new(urls))
21
+ end
21
22
  end
22
23
 
23
24
  def cleanup
@@ -32,7 +33,7 @@ module Routemaster
32
33
 
33
34
  def _cleanup
34
35
  @_pid = Process.pid
35
- @_connections.each_value(&:quit)
36
+ @_connections.each_value.map(&:redis).each(&:quit)
36
37
  @_connections = {}
37
38
  end
38
39
 
@@ -1,4 +1,6 @@
1
1
  require 'routemaster/api_client'
2
+ require 'routemaster/responses/hateoas_enumerable_response'
3
+ require 'routemaster/responses/hateoas_response'
2
4
 
3
5
  module Routemaster
4
6
  module Resources
@@ -7,26 +9,35 @@ module Routemaster
7
9
 
8
10
  def initialize(url, client: nil)
9
11
  @url = url
10
- @client = client || Routemaster::APIClient.new(response_class: Responses::HateoasResponse)
12
+ @client = client || Routemaster::APIClient.new
11
13
  end
12
14
 
13
15
  def create(params)
14
- @client.post(@url, body: params)
16
+ @client.with_response(Responses::HateoasResponse) do
17
+ @client.post(@url, body: params)
18
+ end
15
19
  end
16
20
 
17
- def show(id=nil)
18
- @client.get(@url.gsub('{id}', id.to_s))
21
+ def show(id=nil, enable_caching: true)
22
+ @client.with_response(Responses::HateoasResponse) do
23
+ @client.get(@url.gsub('{id}', id.to_s), options: { enable_caching: enable_caching })
24
+ end
19
25
  end
20
26
 
21
- def index
22
- @client.get(@url)
27
+ def index(params: {}, filters: {}, enable_caching: false)
28
+ @client.with_response(Responses::HateoasEnumerableResponse) do
29
+ @client.get(@url, params: params.merge(filters), options: { enable_caching: enable_caching })
30
+ end
23
31
  end
24
32
 
25
33
  def update(id=nil, params)
26
- @client.patch(@url.gsub('{id}', id.to_s), body: params)
34
+ @client.with_response(Responses::HateoasResponse) do
35
+ @client.patch(@url.gsub('{id}', id.to_s), body: params)
36
+ end
27
37
  end
28
38
 
29
39
  def destroy(id=nil)
40
+ # no response wrapping as DELETE is supposed to 204.
30
41
  @client.delete(@url.gsub('{id}', id.to_s))
31
42
  end
32
43
  end
@@ -1,30 +1,16 @@
1
- require 'thread/pool'
2
- require 'thread/future'
1
+ require 'concurrent/future'
2
+ require 'concurrent/executor/cached_thread_pool'
3
3
  require 'singleton'
4
4
  require 'delegate'
5
5
 
6
6
  module Routemaster
7
7
  module Responses
8
- # A pool of threads, used for parallel/future request processing.
9
- class Pool < SimpleDelegator
10
- include Singleton
11
-
12
- def initialize
13
- Thread.pool(5, 20).tap do |p|
14
- # TODO: configurable pool size and trim timeout?
15
- p.auto_trim!
16
- p.idle_trim! 10 # 10 seconds
17
- super p
18
- end
19
- end
20
- end
21
-
22
8
  class FutureResponse
23
9
  extend Forwardable
24
10
 
25
11
  # The `block` is expected to return a {Response}
26
12
  def initialize(&block)
27
- @future = Pool.instance.future(&block)
13
+ @future = Concurrent::Future.execute(executor: Pool.current, &block)
28
14
  end
29
15
 
30
16
  # @!attribute status
@@ -41,6 +27,40 @@ module Routemaster
41
27
 
42
28
  delegate :value => :@future
43
29
  delegate %i(status headers body) => :value
30
+ delegate :respond_to_missing? => :value
31
+
32
+ def method_missing(m, *args, &block)
33
+ value.public_send(m, *args, &block)
34
+ end
35
+
36
+ def value
37
+ @future.value.tap do
38
+ raise @future.reason if @future.rejected?
39
+ end
40
+ end
41
+
42
+ module Pool
43
+ LOCK = Mutex.new
44
+
45
+ def self.current
46
+ LOCK.synchronize do
47
+ @pool ||= _build_pool
48
+ end
49
+ end
50
+
51
+ def self.reset
52
+ LOCK.synchronize do
53
+ return unless @pool
54
+ @pool.tap(&:shutdown).wait_for_termination
55
+ @pool = nil
56
+ end
57
+ self
58
+ end
59
+
60
+ def self._build_pool
61
+ Concurrent::CachedThreadPool.new(min_length: 5, max_length: 20, max_queue: 0, fallback_policy: :caller_runs)
62
+ end
63
+ end
44
64
  end
45
65
  end
46
66
  end
@@ -0,0 +1,47 @@
1
+ require 'forwardable'
2
+ require 'routemaster/responses/hateoas_response'
3
+
4
+ module Routemaster
5
+ module Responses
6
+ # Yields all resources listed in a collection endpoint in a non-greedy,
7
+ # non-recursive manner.
8
+ #
9
+ # Each yielded resource is a future; synchronous requests are performed for
10
+ # each page.
11
+ #
12
+ # NB: the first named collection in the _links section of the payload will
13
+ # be enumerated. Any other named collections will simply be ignored.
14
+ class HateoasEnumerableResponse < HateoasResponse
15
+ include Enumerable
16
+
17
+ def each(&block)
18
+ each_page do |items|
19
+ items.each(&block)
20
+ end
21
+ end
22
+
23
+ def each_page
24
+ current_page = self
25
+ loop do
26
+ yield _page_items(current_page)
27
+ break unless current_page.has?(:next)
28
+ current_page = current_page.next.index
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def _resource_name
35
+ _links.find { |k,v|
36
+ !%w[curies self].include?(k) && v.kind_of?(Array)
37
+ }.first
38
+ end
39
+
40
+ def _page_items(page)
41
+ page.body._links.fetch(_resource_name).map do |link|
42
+ @client.fget(link.href)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -1,9 +1,10 @@
1
- require 'faraday_middleware'
2
- require 'routemaster/api_client'
3
- require 'routemaster/responses/hateoas_response'
4
- require 'routemaster/resources/rest_resource'
1
+ require 'core_ext/forwardable'
5
2
  require 'forwardable'
6
- require 'json'
3
+ require 'routemaster/api_client'
4
+
5
+ # While this depends on `RestResource`, we can't laod it as there is a circular
6
+ # dependency.
7
+ # require 'routemaster/resources/rest_resource'
7
8
 
8
9
  module Routemaster
9
10
  module Responses
@@ -15,7 +16,7 @@ module Routemaster
15
16
 
16
17
  def initialize(response, client: nil)
17
18
  @response = response
18
- @client = client || Routemaster::APIClient.new(response_class: Routemaster::Responses::HateoasResponse)
19
+ @client = client || Routemaster::APIClient.new(response_class: self.class)
19
20
  end
20
21
 
21
22
  def method_missing(m, *args, &block)
@@ -25,12 +26,8 @@ module Routemaster
25
26
  if _links.keys.include?(normalized_method_name)
26
27
  unless respond_to?(method_name)
27
28
  resource = Resources::RestResource.new(_links[normalized_method_name]['href'], client: @client)
28
-
29
- define_singleton_method(method_name) do |*m_args|
30
- resource
31
- end
32
-
33
- resource
29
+ define_singleton_method(method_name) { resource }
30
+ public_send method_name
34
31
  end
35
32
  else
36
33
  super
@@ -18,10 +18,10 @@ Gem::Specification.new do |spec|
18
18
 
19
19
  spec.add_runtime_dependency 'faraday', '>= 0.9.0'
20
20
  spec.add_runtime_dependency 'faraday_middleware'
21
- spec.add_runtime_dependency 'net-http-persistent', '< 3' # 3.x is currently incompatible with faraday
21
+ spec.add_runtime_dependency 'typhoeus', '~> 1.1'
22
22
  spec.add_runtime_dependency 'rack', '>= 1.4.5'
23
23
  spec.add_runtime_dependency 'wisper', '~> 1.6.1'
24
24
  spec.add_runtime_dependency 'hashie'
25
25
  spec.add_runtime_dependency 'redis-namespace'
26
- spec.add_runtime_dependency 'thread'
26
+ spec.add_runtime_dependency 'concurrent-ruby'
27
27
  end
@@ -10,12 +10,11 @@ describe Routemaster::APIClient do
10
10
  uses_redis
11
11
  uses_webmock
12
12
 
13
- describe '.get' do
14
- let(:url) { 'https://example.com/widgets/132' }
15
- let(:headers) {{}}
16
- let(:fetcher) { described_class.new }
17
- subject { fetcher.get(url, headers: headers) }
13
+ let(:url) { 'https://example.com/widgets/132' }
14
+ let(:headers) {{}}
15
+ let(:fetcher) { described_class.new }
18
16
 
17
+ shared_examples 'a GET requester' do
19
18
  before do
20
19
  @req = stub_request(:get, /example\.com/).to_return(
21
20
  status: 200,
@@ -24,47 +23,13 @@ describe Routemaster::APIClient do
24
23
  'content-type' => 'application/json;v=1'
25
24
  }
26
25
  )
27
-
28
- @post_req = stub_request(:post, /example\.com/).to_return(
29
- status: 200,
30
- body: { id: 132, type: 'widget' }.to_json,
31
- headers: {
32
- 'content-type' => 'application/json;v=1'
33
- }
34
- )
35
-
36
- @patch_req = stub_request(:patch, /example\.com/).to_return(
37
- status: 200,
38
- body: { id: 132, type: 'widget' }.to_json,
39
- headers: {
40
- 'content-type' => 'application/json;v=1'
41
- }
42
- )
43
26
  end
44
27
 
45
28
  it 'GETs from the URL' do
46
- subject
29
+ subject.status
47
30
  expect(@req).to have_been_requested
48
31
  end
49
32
 
50
- context 'POST request' do
51
- subject { fetcher.post(url, body: {}, headers: headers) }
52
-
53
- it 'POSTs from the URL' do
54
- subject
55
- expect(@post_req).to have_been_requested
56
- end
57
- end
58
-
59
- context 'PATCH request' do
60
- subject { fetcher.patch(url, body: {}, headers: headers) }
61
-
62
- it 'PATCH from the URL' do
63
- subject
64
- expect(@patch_req).to have_been_requested
65
- end
66
- end
67
-
68
33
  it 'has :status, :headers, :body' do
69
34
  expect(subject.status).to eq(200)
70
35
  expect(subject.headers).to have_key('content-type')
@@ -76,7 +41,7 @@ describe Routemaster::APIClient do
76
41
  end
77
42
 
78
43
  it 'uses auth' do
79
- subject
44
+ subject.status
80
45
  assert_requested(:get, /example/) do |req|
81
46
  credentials = Base64.strict_encode64('username:s3cr3t')
82
47
  expect(req.headers['Authorization']).to eq("Basic #{credentials}")
@@ -85,7 +50,7 @@ describe Routemaster::APIClient do
85
50
 
86
51
  it 'passes headers' do
87
52
  headers['x-custom-header'] = 'why do you even'
88
- subject
53
+ subject.status
89
54
  assert_requested(:get, /example/) do |req|
90
55
  expect(req.headers).to include('X-Custom-Header')
91
56
  end
@@ -95,14 +60,123 @@ describe Routemaster::APIClient do
95
60
  before do
96
61
  class DummyResponse
97
62
  def initialize(res, client: nil); end
63
+ def dummy; true; end
98
64
  end
99
65
  end
100
66
 
101
67
  let(:fetcher) { described_class.new(response_class: DummyResponse) }
102
68
 
103
- it 'returns a response_class instance as a response' do
104
- expect(subject).to be_an_instance_of(DummyResponse)
69
+ it 'wraps the response in the response class' do
70
+ expect(subject.dummy).to be_truthy
105
71
  end
106
72
  end
107
73
  end
74
+
75
+ describe '#get' do
76
+ subject { fetcher.get(url, headers: headers) }
77
+ it_behaves_like 'a GET requester'
78
+ end
79
+
80
+ describe '#fget' do
81
+ subject { fetcher.fget(url, headers: headers) }
82
+ it_behaves_like 'a GET requester'
83
+ end
84
+
85
+ describe '#post' do
86
+ subject { fetcher.post(url, body: {}, headers: headers) }
87
+
88
+ before do
89
+ @post_req = stub_request(:post, /example\.com/).to_return(
90
+ status: 200,
91
+ body: { id: 132, type: 'widget' }.to_json,
92
+ headers: {
93
+ 'content-type' => 'application/json;v=1'
94
+ }
95
+ )
96
+ end
97
+
98
+ it 'POSTs from the URL' do
99
+ subject
100
+ expect(@post_req).to have_been_requested
101
+ end
102
+ end
103
+
104
+ describe '#patch' do
105
+ subject { fetcher.patch(url, body: {}, headers: headers) }
106
+
107
+ before do
108
+ @patch_req = stub_request(:patch, /example\.com/).to_return(
109
+ status: 200,
110
+ body: { id: 132, type: 'widget' }.to_json,
111
+ headers: {
112
+ 'content-type' => 'application/json;v=1'
113
+ }
114
+ )
115
+ end
116
+
117
+ it 'PATCH from the URL' do
118
+ subject
119
+ expect(@patch_req).to have_been_requested
120
+ end
121
+ end
122
+
123
+ describe '#delete' do
124
+ subject { fetcher.delete(url, headers: headers) }
125
+
126
+ before do
127
+ @delete_req = stub_request(:delete, /example\.com/).to_return(
128
+ status: 204,
129
+ )
130
+ end
131
+
132
+ it 'DELETES from the URL' do
133
+ subject
134
+ expect(@delete_req).to have_been_requested
135
+ end
136
+ end
137
+
138
+ describe '#discover' do
139
+ before do
140
+ @req = stub_request(:get, /example\.com/).to_return(
141
+ status: 200,
142
+ body: { id: 132, type: 'widget' }.to_json,
143
+ headers: {
144
+ 'content-type' => 'application/json;v=1'
145
+ }
146
+ )
147
+ end
148
+
149
+ it 'GETs from the URL' do
150
+ subject.discover('https://example.com')
151
+ expect(@req).to have_been_requested
152
+ end
153
+ end
154
+
155
+ describe '#with_response' do
156
+ before { stub_request(:any, //).to_return(status: 200) }
157
+
158
+ class DummyResponseA
159
+ def initialize(res, client: nil); end
160
+ def dummy_a; true; end
161
+ end
162
+
163
+ class DummyResponseB
164
+ def initialize(res, client: nil); end
165
+ def dummy_b; true; end
166
+ end
167
+
168
+ subject { described_class.new(response_class: DummyResponseA) }
169
+ let(:response) { subject.get('https://example.com') }
170
+
171
+ it 'changes the response wrapper during the block' do
172
+ subject.with_response(DummyResponseB) do
173
+ expect(response).to respond_to(:dummy_b)
174
+ end
175
+ end
176
+
177
+ it 'restores the original response wrapper after the block' do
178
+ subject.with_response(DummyResponseB) {}
179
+ expect(response).to respond_to(:dummy_a)
180
+ end
181
+ end
108
182
  end