routemaster-drain 1.0.5 → 1.1.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.
- checksums.yaml +4 -4
- data/.gitignore +5 -1
- data/.gitmodules +0 -3
- data/.ruby-version +1 -1
- data/.travis.yml +4 -10
- data/Gemfile +14 -10
- data/Gemfile.lock +96 -62
- data/LICENSE.txt +2 -1
- data/README.md +13 -11
- data/lib/routemaster/cache.rb +16 -41
- data/lib/routemaster/config.rb +4 -0
- data/lib/routemaster/dirty/map.rb +1 -1
- data/lib/routemaster/drain.rb +1 -1
- data/lib/routemaster/fetcher.rb +29 -28
- data/lib/routemaster/jobs/backends/resque.rb +25 -0
- data/lib/routemaster/jobs/backends/sidekiq.rb +27 -0
- data/lib/routemaster/jobs/cache.rb +2 -2
- data/lib/routemaster/jobs/cache_and_sweep.rb +1 -1
- data/lib/routemaster/jobs/client.rb +30 -0
- data/lib/routemaster/jobs/job.rb +23 -0
- data/lib/routemaster/middleware/cache.rb +4 -4
- data/lib/routemaster/middleware/caching.rb +71 -0
- data/routemaster-drain.gemspec +3 -4
- data/spec/routemaster/cache_spec.rb +69 -81
- data/spec/routemaster/drain/caching_spec.rb +2 -2
- data/spec/routemaster/fetcher_spec.rb +6 -2
- data/spec/routemaster/integration/cache_spec.rb +68 -0
- data/spec/routemaster/jobs/backends/backend_examples.rb +21 -0
- data/spec/routemaster/jobs/backends/resque_spec.rb +16 -0
- data/spec/routemaster/jobs/backends/sidekiq_spec.rb +11 -0
- data/spec/routemaster/jobs/client_spec.rb +46 -0
- data/spec/routemaster/middleware/cache_spec.rb +7 -8
- data/spec/spec_helper.rb +5 -0
- metadata +28 -27
data/lib/routemaster/config.rb
CHANGED
@@ -8,7 +8,7 @@ module Routemaster
|
|
8
8
|
# refreshed.
|
9
9
|
# Typically +mark+ is called when notified state has changed (e.g. from the
|
10
10
|
# bus) and +sweep+ when one wants to know what has changed.
|
11
|
-
#
|
11
|
+
#
|
12
12
|
# Use case: when some entites are very volatile, the map will hold a "dirty"
|
13
13
|
# state for multiple updates until the client is ready to update.
|
14
14
|
#
|
data/lib/routemaster/drain.rb
CHANGED
data/lib/routemaster/fetcher.rb
CHANGED
@@ -1,30 +1,23 @@
|
|
1
|
+
require 'base64'
|
1
2
|
require 'faraday'
|
2
3
|
require 'faraday_middleware'
|
3
4
|
require 'hashie'
|
4
5
|
require 'routemaster/config'
|
6
|
+
require 'routemaster/middleware/caching'
|
5
7
|
|
6
8
|
module Routemaster
|
7
|
-
# Fetches URLs from JSON APIs.
|
8
9
|
class Fetcher
|
9
|
-
|
10
|
-
# Calls `get` with the same arguments on a memoized instance
|
11
|
-
# for the URL's host.
|
12
|
-
def get(url, params:nil, headers:nil)
|
13
|
-
_connection_for(url).get(url, params:params, headers:headers)
|
14
|
-
end
|
15
|
-
|
16
|
-
private
|
17
|
-
|
18
|
-
def _connection_for(url)
|
19
|
-
host = URI.parse(url).host
|
20
|
-
@connections ||= {}
|
21
|
-
@connections[host] ||= new(host)
|
22
|
-
end
|
23
|
-
end
|
24
|
-
extend ClassMethods
|
10
|
+
DEFAULT_MIDDLEWARE = Routemaster::Middleware::Caching
|
25
11
|
|
26
|
-
|
27
|
-
|
12
|
+
#
|
13
|
+
# Usage:
|
14
|
+
#
|
15
|
+
# You can extend Fetcher with custom middlewares like:
|
16
|
+
# Fetcher.new(middlewares: [[MyCustomMiddleWare, option1, option2]])
|
17
|
+
#
|
18
|
+
def initialize(middlewares: [], listener: nil)
|
19
|
+
@listener = listener
|
20
|
+
@middlewares = generate_middlewares(middlewares)
|
28
21
|
end
|
29
22
|
|
30
23
|
# Performs a GET HTTP request for the `url`, with optional
|
@@ -33,17 +26,21 @@ module Routemaster
|
|
33
26
|
# @return an object that responds to `status` (integer), `headers` (hash),
|
34
27
|
# and `body`. The body is a `Hashie::Mash` if the response was JSON, a
|
35
28
|
# string otherwise.
|
36
|
-
def get(url, params:
|
37
|
-
|
38
|
-
|
29
|
+
def get(url, params: {}, headers: {})
|
30
|
+
host = URI.parse(url).host
|
31
|
+
connection.get(url, params, headers.merge(auth_header(host)))
|
39
32
|
end
|
40
33
|
|
41
34
|
private
|
42
35
|
|
43
|
-
def
|
44
|
-
|
36
|
+
def generate_middlewares(middlewares)
|
37
|
+
default_middleware = [DEFAULT_MIDDLEWARE, listener: @listener]
|
38
|
+
middlewares.unshift(default_middleware)
|
39
|
+
end
|
40
|
+
|
41
|
+
def connection
|
42
|
+
@connection ||= Faraday.new do |f|
|
45
43
|
f.request :retry, max: 2, interval: 100e-3, backoff_factor: 2
|
46
|
-
f.request :basic_auth, *_uuid
|
47
44
|
f.response :mashify
|
48
45
|
f.response :json, content_type: /\bjson/
|
49
46
|
f.adapter :net_http_persistent
|
@@ -51,12 +48,16 @@ module Routemaster
|
|
51
48
|
f.options.timeout = ENV.fetch('ROUTEMASTER_CACHE_TIMEOUT', 1).to_f
|
52
49
|
f.options.open_timeout = ENV.fetch('ROUTEMASTER_CACHE_TIMEOUT', 1).to_f
|
53
50
|
f.ssl.verify = ENV.fetch('ROUTEMASTER_CACHE_VERIFY_SSL', 'false') == 'true'
|
51
|
+
|
52
|
+
@middlewares.each do |middleware|
|
53
|
+
f.use(*middleware)
|
54
|
+
end
|
54
55
|
end
|
55
56
|
end
|
56
57
|
|
57
|
-
def
|
58
|
-
|
58
|
+
def auth_header(host)
|
59
|
+
auth_string = Config.cache_auth.fetch(host, []).join(':')
|
60
|
+
{ 'Authorization' => "Basic #{Base64.strict_encode64(auth_string)}" }
|
59
61
|
end
|
60
62
|
end
|
61
63
|
end
|
62
|
-
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'resque'
|
2
|
+
require 'routemaster/jobs/job'
|
3
|
+
|
4
|
+
module Routemaster
|
5
|
+
module Jobs
|
6
|
+
module Backends
|
7
|
+
class Resque
|
8
|
+
def initialize(adapter = nil)
|
9
|
+
@adapter = adapter || ::Resque
|
10
|
+
end
|
11
|
+
|
12
|
+
def enqueue(queue, job_class, *args)
|
13
|
+
job_data = Job.data_for(job_class, args)
|
14
|
+
@adapter.enqueue_to(queue, JobWrapper, job_data)
|
15
|
+
end
|
16
|
+
|
17
|
+
class JobWrapper
|
18
|
+
def self.perform(job_data)
|
19
|
+
Job.execute(job_data)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'sidekiq'
|
2
|
+
require 'routemaster/jobs/job'
|
3
|
+
|
4
|
+
module Routemaster
|
5
|
+
module Jobs
|
6
|
+
module Backends
|
7
|
+
class Sidekiq
|
8
|
+
def initialize(adapter = nil)
|
9
|
+
@adapter = adapter || ::Sidekiq::Client
|
10
|
+
end
|
11
|
+
|
12
|
+
def enqueue(queue, job_class, *args)
|
13
|
+
job_data = Job.data_for(job_class, args)
|
14
|
+
@adapter.push('queue' => queue, 'class' => JobWrapper, 'args' => [job_data])
|
15
|
+
end
|
16
|
+
|
17
|
+
class JobWrapper
|
18
|
+
include ::Sidekiq::Worker
|
19
|
+
|
20
|
+
def perform(job_data)
|
21
|
+
Job.execute(job_data)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'routemaster/config'
|
2
|
+
|
3
|
+
module Routemaster
|
4
|
+
module Jobs
|
5
|
+
class Client
|
6
|
+
extend Forwardable
|
7
|
+
|
8
|
+
def_delegators :@backend, :enqueue
|
9
|
+
|
10
|
+
def initialize(adapter = nil)
|
11
|
+
@backend = build_backend(adapter)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def build_backend(adapter)
|
17
|
+
case Config.queue_adapter
|
18
|
+
when :resque
|
19
|
+
require 'routemaster/jobs/backends/resque'
|
20
|
+
Backends::Resque.new(adapter)
|
21
|
+
when :sidekiq
|
22
|
+
require 'routemaster/jobs/backends/sidekiq'
|
23
|
+
Backends::Sidekiq.new(adapter)
|
24
|
+
else
|
25
|
+
raise "Unsupported queue adapter '#{Config.queue_adapter}"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Routemaster
|
2
|
+
module Jobs
|
3
|
+
class Job
|
4
|
+
class << self
|
5
|
+
def data_for(job_class, args)
|
6
|
+
{ 'class' => job_class.to_s, 'args' => args }
|
7
|
+
end
|
8
|
+
|
9
|
+
def execute(job_data)
|
10
|
+
job = create_job(job_data)
|
11
|
+
job.new.perform(*job_data['args'])
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def create_job(job_data)
|
17
|
+
job_class = job_data['class']
|
18
|
+
Kernel.const_get(job_class)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -1,22 +1,22 @@
|
|
1
1
|
require 'routemaster/cache'
|
2
2
|
require 'routemaster/config'
|
3
|
+
require 'routemaster/jobs/client'
|
3
4
|
require 'routemaster/jobs/cache_and_sweep'
|
4
|
-
require 'resque'
|
5
5
|
|
6
6
|
module Routemaster
|
7
7
|
module Middleware
|
8
8
|
class Cache
|
9
|
-
def initialize(app, cache:nil,
|
9
|
+
def initialize(app, cache:nil, client:nil, queue:nil)
|
10
10
|
@app = app
|
11
11
|
@cache = cache || Routemaster::Cache.new
|
12
|
-
@
|
12
|
+
@client = client || Routemaster::Jobs::Client.new
|
13
13
|
@queue = queue || Config.queue_name
|
14
14
|
end
|
15
15
|
|
16
16
|
def call(env)
|
17
17
|
env.fetch('routemaster.dirty', []).each do |url|
|
18
18
|
@cache.bust(url)
|
19
|
-
@
|
19
|
+
@client.enqueue(@queue, Routemaster::Jobs::CacheAndSweep, url)
|
20
20
|
end
|
21
21
|
@app.call(env)
|
22
22
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require 'wisper'
|
2
|
+
|
3
|
+
module Routemaster
|
4
|
+
module Middleware
|
5
|
+
class Caching
|
6
|
+
KEY_TEMPLATE = 'cache:{url}'
|
7
|
+
FIELD_TEMPLATE = 'v:{version},l:{locale}'
|
8
|
+
VERSION_REGEX = /application\/json;v=(?<version>\S*)/.freeze
|
9
|
+
|
10
|
+
def initialize(app, cache: Config.cache_redis, listener: nil)
|
11
|
+
@app = app
|
12
|
+
@cache = cache
|
13
|
+
@expiry = Config.cache_expiry
|
14
|
+
@listener = listener
|
15
|
+
end
|
16
|
+
|
17
|
+
def call(env)
|
18
|
+
return @app.call(env) unless env.method == :get
|
19
|
+
|
20
|
+
fetch_from_cache(env) || fetch_from_service(env)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def fetch_from_service(env)
|
26
|
+
@app.call(env).on_complete do |response_env|
|
27
|
+
response = response_env.response
|
28
|
+
|
29
|
+
if response.success?
|
30
|
+
@cache.hset(cache_key(env), cache_field(env), response.body)
|
31
|
+
@cache.expire(cache_key(env), @expiry)
|
32
|
+
@listener._publish(:cache_miss, url(env)) if @listener
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def fetch_from_cache(env)
|
38
|
+
payload = @cache.hget(cache_key(env), cache_field(env))
|
39
|
+
|
40
|
+
if payload
|
41
|
+
@listener._publish(:cache_hit, url(env)) if @listener
|
42
|
+
Faraday::Response.new(status: 200,
|
43
|
+
body: payload,
|
44
|
+
response_headers: {})
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def cache_field(env)
|
49
|
+
FIELD_TEMPLATE
|
50
|
+
.gsub('{version}', version(env).to_s)
|
51
|
+
.gsub('{locale}', locale(env).to_s)
|
52
|
+
end
|
53
|
+
|
54
|
+
def cache_key(env)
|
55
|
+
KEY_TEMPLATE.gsub('{url}', url(env))
|
56
|
+
end
|
57
|
+
|
58
|
+
def url(env)
|
59
|
+
env.url.to_s
|
60
|
+
end
|
61
|
+
|
62
|
+
def version(env)
|
63
|
+
(env.request_headers['Accept'] || "")[VERSION_REGEX, 1]
|
64
|
+
end
|
65
|
+
|
66
|
+
def locale(env)
|
67
|
+
env.request_headers['Accept-Language']
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
data/routemaster-drain.gemspec
CHANGED
@@ -18,11 +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'
|
22
|
-
spec.add_runtime_dependency 'rack'
|
23
|
-
spec.add_runtime_dependency 'wisper', '
|
21
|
+
spec.add_runtime_dependency 'net-http-persistent', '< 3' # 3.x is currently incompatible with faraday
|
22
|
+
spec.add_runtime_dependency 'rack', '>= 1.6.2'
|
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 'resque'
|
27
26
|
spec.add_runtime_dependency 'thread'
|
28
27
|
end
|
@@ -1,115 +1,103 @@
|
|
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'
|
4
5
|
require 'routemaster/cache'
|
5
6
|
require 'routemaster/fetcher'
|
6
7
|
|
7
|
-
|
8
|
-
|
9
|
-
|
8
|
+
module Routemaster
|
9
|
+
describe Cache do
|
10
|
+
uses_dotenv
|
11
|
+
uses_redis
|
12
|
+
uses_webmock
|
10
13
|
|
11
|
-
|
12
|
-
let(:url) { make_url(1) }
|
13
|
-
let(:listener) { double 'listener' }
|
14
|
-
subject { described_class.new(fetcher: fetcher) }
|
14
|
+
let(:url) { 'https://www.example.com/widgets/123' }
|
15
15
|
|
16
|
-
def make_url(idx)
|
17
|
-
"https://example.com/stuff/#{idx}"
|
18
|
-
end
|
19
|
-
|
20
|
-
def make_response(idx)
|
21
|
-
Hashie::Mash.new(
|
22
|
-
status: 200,
|
23
|
-
headers: {},
|
24
|
-
body: { id: 123, t: idx }
|
25
|
-
)
|
26
|
-
end
|
27
|
-
|
28
|
-
shared_examples 'a response getter' do
|
29
16
|
before do
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
17
|
+
stub_request(:get, /example\.com/).to_return(
|
18
|
+
status: 200,
|
19
|
+
body: { id: 123, type: 'widget' }.to_json,
|
20
|
+
headers: {
|
21
|
+
'content-type' => 'application/json;v=1'
|
22
|
+
}
|
23
|
+
)
|
34
24
|
end
|
35
25
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
end
|
26
|
+
shared_examples 'a GET request' do
|
27
|
+
context 'with no options' do
|
28
|
+
let(:options) { {} }
|
40
29
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
performer.call(url, locale: 'fr').status
|
47
|
-
end
|
30
|
+
it 'calls get on the fetcher with no version and locale headers' do
|
31
|
+
expect_any_instance_of(Fetcher)
|
32
|
+
.to receive(:get)
|
33
|
+
.with(url, headers: { 'Accept' => 'application/json' })
|
34
|
+
.and_call_original
|
48
35
|
|
49
|
-
|
50
|
-
|
51
|
-
expect(options[:headers]['Accept']).to eq('application/json;v=33')
|
52
|
-
make_response(1)
|
36
|
+
perform.status
|
37
|
+
end
|
53
38
|
end
|
54
|
-
performer.call(url, version: 33).status
|
55
|
-
end
|
56
39
|
|
57
|
-
|
58
|
-
|
59
|
-
3.times { performer.call(url).status }
|
60
|
-
expect(performer.call(url).body.t).to eq(1)
|
61
|
-
end
|
40
|
+
context 'with a specific version' do
|
41
|
+
let(:options) { { version: 2 } }
|
62
42
|
|
63
|
-
|
64
|
-
|
43
|
+
it 'calls get on the fetcher with version header' do
|
44
|
+
expect_any_instance_of(Fetcher)
|
45
|
+
.to receive(:get)
|
46
|
+
.with(url, headers: { 'Accept' => 'application/json;v=2' })
|
47
|
+
.and_call_original
|
65
48
|
|
66
|
-
|
67
|
-
|
68
|
-
performer.call(url).status
|
49
|
+
perform.status
|
50
|
+
end
|
69
51
|
end
|
70
52
|
|
71
|
-
|
72
|
-
|
73
|
-
performer.call(url, locale: 'en').status
|
74
|
-
performer.call(url, locale: 'fr').status
|
75
|
-
end
|
53
|
+
context 'with a specific locale' do
|
54
|
+
let(:options) { { locale: 'fr' } }
|
76
55
|
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
56
|
+
it 'calls get on the fetcher with locale header' do
|
57
|
+
expect_any_instance_of(Fetcher)
|
58
|
+
.to receive(:get)
|
59
|
+
.with(url, headers: { 'Accept' => 'application/json', 'Accept-Language' => 'fr' })
|
60
|
+
.and_call_original
|
61
|
+
|
62
|
+
perform.status
|
63
|
+
end
|
82
64
|
end
|
83
65
|
end
|
84
|
-
end
|
85
66
|
|
67
|
+
describe '#get' do
|
68
|
+
let(:perform) { subject.get(url, **options) }
|
86
69
|
|
87
|
-
|
88
|
-
|
70
|
+
it_behaves_like 'a GET request'
|
71
|
+
end
|
89
72
|
|
90
|
-
|
91
|
-
|
73
|
+
describe '#fget' do
|
74
|
+
let(:perform) { subject.fget(url, **options) }
|
92
75
|
|
76
|
+
it_behaves_like 'a GET request'
|
77
|
+
end
|
93
78
|
|
94
|
-
|
95
|
-
|
79
|
+
describe '#bust' do
|
80
|
+
let(:cache) { Config.cache_redis }
|
81
|
+
let(:perform) { subject.bust(url) }
|
96
82
|
|
97
|
-
|
98
|
-
|
83
|
+
before do
|
84
|
+
cache.set("cache:#{url}", "cached response")
|
85
|
+
end
|
99
86
|
|
87
|
+
it 'busts the cache for a given URL' do
|
88
|
+
expect { perform }
|
89
|
+
.to change { cache.get("cache:#{url}") }
|
90
|
+
.from('cached response')
|
91
|
+
.to(nil)
|
92
|
+
end
|
100
93
|
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
subject.bust(url)
|
106
|
-
subject.get(url)
|
107
|
-
end
|
94
|
+
it 'publishes the cache_bust event for that URL' do
|
95
|
+
expect_any_instance_of(described_class)
|
96
|
+
.to receive(:publish)
|
97
|
+
.with(:cache_bust, url)
|
108
98
|
|
109
|
-
|
110
|
-
|
111
|
-
expect(listener).to receive(:on_cache_bust).once
|
112
|
-
subject.bust(url)
|
99
|
+
perform
|
100
|
+
end
|
113
101
|
end
|
114
102
|
end
|
115
103
|
end
|