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
@@ -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(
|
15
|
+
def get(name, urls: [])
|
15
16
|
_check_for_fork
|
16
|
-
@_connections[
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
12
|
+
@client = client || Routemaster::APIClient.new
|
11
13
|
end
|
12
14
|
|
13
15
|
def create(params)
|
14
|
-
@client.
|
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.
|
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.
|
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.
|
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 '
|
2
|
-
require '
|
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.
|
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 '
|
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 '
|
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:
|
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
|
-
|
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
|
data/routemaster-drain.gemspec
CHANGED
@@ -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 '
|
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 '
|
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
|
-
|
14
|
-
|
15
|
-
|
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 '
|
104
|
-
expect(subject).to
|
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
|