makara 0.3.10 → 0.4.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/CHANGELOG.md +11 -4
- data/README.md +22 -24
- data/gemfiles/ar50.gemfile +1 -1
- data/gemfiles/ar51.gemfile +1 -1
- data/lib/makara.rb +1 -0
- data/lib/makara/cache.rb +4 -40
- data/lib/makara/config_parser.rb +12 -2
- data/lib/makara/context.rb +108 -38
- data/lib/makara/cookie.rb +52 -0
- data/lib/makara/error_handler.rb +2 -2
- data/lib/makara/logging/logger.rb +1 -1
- data/lib/makara/middleware.rb +12 -75
- data/lib/makara/pool.rb +4 -11
- data/lib/makara/proxy.rb +20 -25
- data/lib/makara/version.rb +2 -2
- data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +0 -4
- data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +1 -1
- data/spec/cache_spec.rb +2 -74
- data/spec/config_parser_spec.rb +21 -1
- data/spec/context_spec.rb +163 -100
- data/spec/cookie_spec.rb +72 -0
- data/spec/middleware_spec.rb +26 -55
- data/spec/proxy_spec.rb +51 -36
- data/spec/spec_helper.rb +2 -9
- data/spec/support/helpers.rb +6 -2
- data/spec/support/mock_objects.rb +1 -1
- data/spec/support/proxy_extensions.rb +1 -1
- metadata +5 -4
- data/lib/makara/cache/memory_store.rb +0 -31
- data/lib/makara/cache/noop_store.rb +0 -15
@@ -0,0 +1,52 @@
|
|
1
|
+
module Makara
|
2
|
+
module Cookie
|
3
|
+
extend self
|
4
|
+
|
5
|
+
IDENTIFIER = '_mkra_stck'.freeze
|
6
|
+
|
7
|
+
DEFAULT_OPTIONS = {
|
8
|
+
path: "/",
|
9
|
+
http_only: true
|
10
|
+
}
|
11
|
+
|
12
|
+
MAX_AGE_BUFFER = 5
|
13
|
+
|
14
|
+
def fetch(request)
|
15
|
+
parse(request.cookies[IDENTIFIER].to_s)
|
16
|
+
end
|
17
|
+
|
18
|
+
def store(context_data, headers, options = {})
|
19
|
+
unless context_data.nil?
|
20
|
+
Rack::Utils.set_cookie_header! headers, IDENTIFIER, build_cookie(context_data, options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
# Pairs of {proxy_id}:{timestamp}, separated by "|"
|
26
|
+
# proxy_id1:1518270031.3132212|proxy_id2:1518270030.313232 ..
|
27
|
+
def parse(cookie_string)
|
28
|
+
return {} if cookie_string.empty?
|
29
|
+
|
30
|
+
states = cookie_string.split("|")
|
31
|
+
states.each_with_object({}) do |state, context_data|
|
32
|
+
proxy_id, timestamp = state.split(":")
|
33
|
+
context_data[proxy_id] = timestamp.to_f if proxy_id && timestamp
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_cookie(context_data, options)
|
38
|
+
cookie = DEFAULT_OPTIONS.merge(options)
|
39
|
+
now = Time.now
|
40
|
+
|
41
|
+
cookie[:max_age] = if context_data.any?
|
42
|
+
(context_data.values.max - now.to_f).ceil + MAX_AGE_BUFFER
|
43
|
+
else
|
44
|
+
0
|
45
|
+
end
|
46
|
+
cookie[:expires] = now + cookie[:max_age]
|
47
|
+
cookie[:value] = context_data.collect { |proxy_id, ttl| "#{proxy_id}:#{ttl}" }.join('|')
|
48
|
+
|
49
|
+
cookie
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
data/lib/makara/error_handler.rb
CHANGED
@@ -25,13 +25,13 @@ module Makara
|
|
25
25
|
|
26
26
|
def gracefully(connection, e)
|
27
27
|
err = Makara::Errors::BlacklistConnection.new(connection, e)
|
28
|
-
::Makara::Logging::Logger.log("
|
28
|
+
::Makara::Logging::Logger.log("Gracefully handling: #{err}")
|
29
29
|
raise err
|
30
30
|
end
|
31
31
|
|
32
32
|
|
33
33
|
def harshly(e)
|
34
|
-
::Makara::Logging::Logger.log("
|
34
|
+
::Makara::Logging::Logger.log("Harshly handling: #{e}\n#{e.backtrace.join("\n\t")}")
|
35
35
|
raise e
|
36
36
|
end
|
37
37
|
|
data/lib/makara/middleware.rb
CHANGED
@@ -1,37 +1,20 @@
|
|
1
1
|
require 'rack'
|
2
2
|
|
3
|
-
# Persists the Makara::Context across requests ensuring the same master pool is used on
|
4
|
-
# Simply sets the cookie with the current context and the status code of this request. The next request then sets
|
5
|
-
# the Makara::Context's previous context based on the the previous request. If a redirect is encountered the middleware
|
6
|
-
# will defer generation of a new context until a non-redirect request occurs.
|
7
|
-
|
3
|
+
# Persists the Makara::Context across requests ensuring the same master pool is used on subsequent requests.
|
8
4
|
module Makara
|
9
5
|
class Middleware
|
10
6
|
|
11
|
-
IDENTIFIER = '_mkra_ctxt'
|
12
|
-
|
13
|
-
DEFAULT_COOKIE = {
|
14
|
-
:path => '/',
|
15
|
-
:http_only => true,
|
16
|
-
:max_age => '5'
|
17
|
-
}
|
18
|
-
|
19
7
|
def initialize(app, cookie_options = {})
|
20
8
|
@app = app
|
21
|
-
@
|
9
|
+
@cookie_options = cookie_options
|
22
10
|
end
|
23
11
|
|
24
|
-
|
25
12
|
def call(env)
|
26
|
-
|
27
13
|
return @app.call(env) if ignore_request?(env)
|
28
|
-
|
29
|
-
Makara::Context.set_previous previous_context(env)
|
30
|
-
Makara::Context.set_current new_context(env)
|
14
|
+
set_current_context(env)
|
31
15
|
|
32
16
|
status, headers, body = @app.call(env)
|
33
|
-
|
34
|
-
store_context(status, headers)
|
17
|
+
store_new_context(headers)
|
35
18
|
|
36
19
|
[status, headers, body]
|
37
20
|
end
|
@@ -39,6 +22,14 @@ module Makara
|
|
39
22
|
|
40
23
|
protected
|
41
24
|
|
25
|
+
def set_current_context(env)
|
26
|
+
context_data = Makara::Cookie.fetch(Rack::Request.new(env))
|
27
|
+
Makara::Context.set_current(context_data)
|
28
|
+
end
|
29
|
+
|
30
|
+
def store_new_context(headers)
|
31
|
+
Makara::Cookie.store(Makara::Context.next, headers, @cookie_options)
|
32
|
+
end
|
42
33
|
|
43
34
|
# ignore asset paths
|
44
35
|
# consider allowing a filter proc to be provided in an initializer
|
@@ -50,59 +41,5 @@ module Makara
|
|
50
41
|
end
|
51
42
|
false
|
52
43
|
end
|
53
|
-
|
54
|
-
|
55
|
-
# generate a new context based on the request
|
56
|
-
# if the previous request was a redirect, we keep the same context
|
57
|
-
def new_context(env)
|
58
|
-
|
59
|
-
makara_context, makara_status = makara_values(env)
|
60
|
-
|
61
|
-
context = nil
|
62
|
-
|
63
|
-
# if the previous request was a redirect, let's keep that context
|
64
|
-
if makara_status.to_s =~ /^3/ # 300+ redirect
|
65
|
-
context = makara_context
|
66
|
-
end
|
67
|
-
|
68
|
-
context ||= Makara::Context.get_current if env['rack.test']
|
69
|
-
context ||= Makara::Context.generate(env["action_dispatch.request_id"])
|
70
|
-
context
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
# pulls the previous context out of the request
|
75
|
-
def previous_context(env)
|
76
|
-
context = makara_values(env).first
|
77
|
-
context ||= Makara::Context.get_previous if env['rack.test']
|
78
|
-
context ||= Makara::Context.generate
|
79
|
-
context
|
80
|
-
end
|
81
|
-
|
82
|
-
|
83
|
-
# retrieve the stored content from the cookie or query
|
84
|
-
# The value contains the hexdigest and status code of the previous
|
85
|
-
# response in the format: $digest--$status
|
86
|
-
def makara_values(env)
|
87
|
-
regex = /#{IDENTIFIER}=([\-a-z0-9A-Z]+)/
|
88
|
-
|
89
|
-
env['HTTP_COOKIE'].to_s =~ regex
|
90
|
-
return $1.split('--') if $1
|
91
|
-
|
92
|
-
env['QUERY_STRING'].to_s =~ regex
|
93
|
-
return $1.split('--') if $1
|
94
|
-
|
95
|
-
[nil, nil]
|
96
|
-
end
|
97
|
-
|
98
|
-
|
99
|
-
# push the current context into the cookie
|
100
|
-
# it should always be for the same path, only
|
101
|
-
# accessible via http and live for a short amount
|
102
|
-
# of time
|
103
|
-
def store_context(status, headers)
|
104
|
-
cookie = @cookie.merge(:value => "#{Makara::Context.get_current}--#{status}")
|
105
|
-
Rack::Utils.set_cookie_header! headers, IDENTIFIER, cookie
|
106
|
-
end
|
107
44
|
end
|
108
45
|
end
|
data/lib/makara/pool.rb
CHANGED
@@ -1,7 +1,6 @@
|
|
1
1
|
require 'active_support/core_ext/hash/keys'
|
2
2
|
|
3
3
|
# Wraps a collection of similar connections and chooses which one to use
|
4
|
-
# Uses the Makara::Context to determine if the connection needs rotation.
|
5
4
|
# Provides convenience methods for accessing underlying connections
|
6
5
|
|
7
6
|
module Makara
|
@@ -18,7 +17,6 @@ module Makara
|
|
18
17
|
def initialize(role, proxy)
|
19
18
|
@role = role
|
20
19
|
@proxy = proxy
|
21
|
-
@context = Makara::Context.get_current
|
22
20
|
@connections = []
|
23
21
|
@blacklist_errors = []
|
24
22
|
@disabled = false
|
@@ -137,16 +135,11 @@ module Makara
|
|
137
135
|
# to be sticky, provide back the current connection assuming it is
|
138
136
|
# not blacklisted.
|
139
137
|
def next
|
140
|
-
if @proxy.sticky &&
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
con = @strategy.next
|
146
|
-
if con
|
147
|
-
@context = Makara::Context.get_current
|
138
|
+
if @proxy.sticky && @strategy.current
|
139
|
+
@strategy.current
|
140
|
+
else
|
141
|
+
@strategy.next
|
148
142
|
end
|
149
|
-
con
|
150
143
|
end
|
151
144
|
end
|
152
145
|
end
|
data/lib/makara/proxy.rb
CHANGED
@@ -59,22 +59,21 @@ module Makara
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def without_sticking
|
62
|
-
before_context = @master_context
|
63
|
-
@master_context = nil
|
64
62
|
@skip_sticking = true
|
65
63
|
yield
|
66
64
|
ensure
|
67
65
|
@skip_sticking = false
|
68
|
-
@master_context ||= before_context
|
69
66
|
end
|
70
67
|
|
71
68
|
def hijacked?
|
72
69
|
@hijacked
|
73
70
|
end
|
74
71
|
|
75
|
-
|
76
|
-
|
77
|
-
|
72
|
+
# If persist is true, we stick the proxy to master for subsequent requests
|
73
|
+
# up to master_ttl duration. Otherwise we just stick it for the current request
|
74
|
+
def stick_to_master!(persist = true)
|
75
|
+
stickiness_duration = persist ? @ttl : 0
|
76
|
+
Makara::Context.stick(@id, stickiness_duration)
|
78
77
|
end
|
79
78
|
|
80
79
|
def strategy_for(role)
|
@@ -201,15 +200,11 @@ module Makara
|
|
201
200
|
stick_to_master(method_name, args)
|
202
201
|
@master_pool
|
203
202
|
|
204
|
-
|
205
|
-
elsif Makara::Context.get_current == @master_context
|
206
|
-
@master_pool
|
207
|
-
|
208
|
-
elsif previously_stuck_to_master?
|
203
|
+
elsif stuck_to_master?
|
209
204
|
|
210
|
-
# we're
|
211
|
-
#
|
212
|
-
|
205
|
+
# we're on master because we already stuck this proxy in this
|
206
|
+
# request or because we got stuck in previous requests and the
|
207
|
+
# stickiness is still valid
|
213
208
|
@master_pool
|
214
209
|
|
215
210
|
# all slaves are down (or empty)
|
@@ -247,28 +242,28 @@ module Makara
|
|
247
242
|
end
|
248
243
|
|
249
244
|
|
250
|
-
def
|
251
|
-
|
245
|
+
def stuck_to_master?
|
246
|
+
sticky? && Makara::Context.stuck?(@id)
|
252
247
|
end
|
253
248
|
|
254
|
-
|
255
|
-
def stick_to_master(method_name, args, write_to_cache = true)
|
256
|
-
# if we're already stuck to master, don't bother doing it again
|
257
|
-
return if @master_context == Makara::Context.get_current
|
258
|
-
|
249
|
+
def stick_to_master(method_name, args)
|
259
250
|
# check to see if we're configured, bypassed, or some custom implementation has input
|
260
251
|
return unless should_stick?(method_name, args)
|
261
252
|
|
262
253
|
# do the sticking
|
263
|
-
stick_to_master!
|
254
|
+
stick_to_master!
|
264
255
|
end
|
265
256
|
|
266
|
-
|
267
|
-
#
|
257
|
+
# For the generic proxy implementation, we stick if we are sticky,
|
258
|
+
# method and args don't matter
|
268
259
|
def should_stick?(method_name, args)
|
269
|
-
|
260
|
+
sticky?
|
270
261
|
end
|
271
262
|
|
263
|
+
# If we are configured to be sticky and we aren't bypassing stickiness,
|
264
|
+
def sticky?
|
265
|
+
@sticky && !@skip_sticking
|
266
|
+
end
|
272
267
|
|
273
268
|
# use the config parser to generate a master and slave pool
|
274
269
|
def instantiate_connections
|
data/lib/makara/version.rb
CHANGED
@@ -82,7 +82,7 @@ describe 'MakaraMysql2Adapter' do
|
|
82
82
|
ActiveRecord::Base.connection
|
83
83
|
|
84
84
|
load(File.dirname(__FILE__) + '/../../support/schema.rb')
|
85
|
-
|
85
|
+
change_context
|
86
86
|
|
87
87
|
allow(ActiveRecord::Base).to receive(:mysql2_connection) do |config|
|
88
88
|
config[:username] = db_username
|
data/spec/cache_spec.rb
CHANGED
@@ -2,80 +2,8 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Makara::Cache do
|
4
4
|
|
5
|
-
it '
|
6
|
-
|
7
|
-
|
8
|
-
expect(
|
9
|
-
described_class.send(:store)
|
10
|
-
).to be_nil
|
11
|
-
|
12
|
-
expect{
|
13
|
-
described_class.read('test')
|
14
|
-
}.not_to raise_error
|
15
|
-
end
|
16
|
-
|
17
|
-
it 'provides a few stores for testing purposes' do
|
18
|
-
described_class.store = :memory
|
19
|
-
described_class.write('test', 'value', 10)
|
20
|
-
expect(described_class.read('test')).to eq('value')
|
21
|
-
|
5
|
+
it 'shows a warning' do
|
6
|
+
expect(Makara::Logging::Logger).to receive(:log).with(/Setting the Makara::Cache\.store won't have any effects/, :warn)
|
22
7
|
described_class.store = :noop
|
23
|
-
described_class.write('test', 'value', 10)
|
24
|
-
expect(described_class.read('test')).to be_nil
|
25
8
|
end
|
26
|
-
|
27
|
-
|
28
|
-
# this will be used in tests so we have to ensure this works as expected
|
29
|
-
context Makara::Cache::MemoryStore do
|
30
|
-
|
31
|
-
let(:store){ Makara::Cache::MemoryStore.new }
|
32
|
-
let(:data){ store.instance_variable_get('@data') }
|
33
|
-
|
34
|
-
it 'should read and write keys' do
|
35
|
-
expect(store.read('test')).to be_nil
|
36
|
-
store.write('test', 'value')
|
37
|
-
expect(store.read('test')).to eq('value')
|
38
|
-
end
|
39
|
-
|
40
|
-
it 'provides time based expiration' do
|
41
|
-
store.write('test', 'value', :expires_in => 5)
|
42
|
-
expect(store.read('test')).to eq('value')
|
43
|
-
|
44
|
-
Timecop.travel Time.now + 6 do
|
45
|
-
expect(store.read('test')).to be_nil
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
it 'cleans the data' do
|
50
|
-
store.write('test', 'value', :expires_in => -5)
|
51
|
-
expect(store.read('test')).to be_nil
|
52
|
-
expect(data).not_to have_key('test')
|
53
|
-
end
|
54
|
-
|
55
|
-
it 'has thread-safety' do
|
56
|
-
store = Makara::Cache::MemoryStore.new
|
57
|
-
previous_value = Thread.abort_on_exception
|
58
|
-
|
59
|
-
begin
|
60
|
-
Thread.abort_on_exception = true
|
61
|
-
|
62
|
-
workers = 2.times.map do
|
63
|
-
Thread.new do
|
64
|
-
100.times do |n|
|
65
|
-
store.write(n, 'value', expires_in: 0.5)
|
66
|
-
sleep(0.01)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
expect { workers.map(&:join) }.to_not raise_error
|
72
|
-
ensure
|
73
|
-
Thread.abort_on_exception = previous_value
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
end
|
78
|
-
|
79
|
-
|
80
|
-
|
81
9
|
end
|
data/spec/config_parser_spec.rb
CHANGED
@@ -79,7 +79,7 @@ describe Makara::ConfigParser do
|
|
79
79
|
|
80
80
|
end
|
81
81
|
|
82
|
-
it 'should provide
|
82
|
+
it 'should provide a default proxy id based on the recursively sorted config' do
|
83
83
|
parsera = described_class.new(config)
|
84
84
|
parserb = described_class.new(config.merge(:other => 'value'))
|
85
85
|
parserc = described_class.new(config)
|
@@ -88,6 +88,26 @@ describe Makara::ConfigParser do
|
|
88
88
|
expect(parsera.id).to eq(parserc.id)
|
89
89
|
end
|
90
90
|
|
91
|
+
it 'should use provided proxy id instead of default' do
|
92
|
+
config_with_custom_id = config.dup
|
93
|
+
config_with_custom_id[:makara][:id] = 'my_proxy'
|
94
|
+
|
95
|
+
parser = described_class.new(config_with_custom_id)
|
96
|
+
|
97
|
+
expect(parser.id).to eq('my_proxy')
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'should replace reserved characters and show a warning for provided proxy ids' do
|
101
|
+
config_with_custom_id = config.dup
|
102
|
+
config_with_custom_id[:makara][:id] = "my|proxy|id:with:reserved:characters"
|
103
|
+
warning = "Proxy id 'my|proxy|id:with:reserved:characters' changed to 'myproxyidwithreservedcharacters'"
|
104
|
+
expect(Makara::Logging::Logger).to receive(:log).with(warning, :warn)
|
105
|
+
|
106
|
+
parser = described_class.new(config_with_custom_id)
|
107
|
+
|
108
|
+
expect(parser.id).to eq('myproxyidwithreservedcharacters')
|
109
|
+
end
|
110
|
+
|
91
111
|
context 'master and slave configs' do
|
92
112
|
it 'should provide master and slave configs' do
|
93
113
|
parser = described_class.new(config)
|