makara 0.3.10 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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("[Makara] Gracefully handling: #{err}")
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("[Makara] Harshly handling: #{e}\n#{e.backtrace.join("\n\t")}")
34
+ ::Makara::Logging::Logger.log("Harshly handling: #{e}\n#{e.backtrace.join("\n\t")}")
35
35
  raise e
36
36
  end
37
37
 
@@ -5,7 +5,7 @@ module Makara
5
5
  class << self
6
6
 
7
7
  def log(msg, format = :error)
8
- logger.send(format, msg) if logger
8
+ logger.send(format, "[Makara] #{msg}") if logger
9
9
  end
10
10
 
11
11
  def logger
@@ -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 the subsequent request.
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
- @cookie = DEFAULT_COOKIE.merge(cookie_options)
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
@@ -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 && Makara::Context.get_current == @context
141
- con = @strategy.current
142
- return con if con
143
- end
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
@@ -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
- def stick_to_master!(write_to_cache = true)
76
- @master_context = Makara::Context.get_current
77
- Makara::Context.stick(@master_context, @id, @ttl) if write_to_cache
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
- # in this context, we've already stuck to master
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 only on master because of the previous context so
211
- # behave like we're sticking to master but store the current context
212
- stick_to_master(method_name, args, false)
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 previously_stuck_to_master?
251
- @sticky && Makara::Context.previously_stuck?(@id)
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!(write_to_cache)
254
+ stick_to_master!
264
255
  end
265
256
 
266
-
267
- # if we are configured to be sticky and we aren't bypassing stickiness
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
- @sticky && !@skip_sticking
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
@@ -2,8 +2,8 @@ module Makara
2
2
  module VERSION
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 3
6
- PATCH = 10
5
+ MINOR = 4
6
+ PATCH = 0
7
7
  PRE = nil
8
8
 
9
9
  def self.to_s
@@ -74,10 +74,6 @@ describe ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter do
74
74
  end
75
75
 
76
76
  proxy.execute(sql)
77
-
78
- if should_send_to_all_connections
79
- expect(proxy.master_context).to be_nil
80
- end
81
77
  end
82
78
 
83
79
  end
@@ -82,7 +82,7 @@ describe 'MakaraMysql2Adapter' do
82
82
  ActiveRecord::Base.connection
83
83
 
84
84
  load(File.dirname(__FILE__) + '/../../support/schema.rb')
85
- Makara::Context.set_current Makara::Context.generate
85
+ change_context
86
86
 
87
87
  allow(ActiveRecord::Base).to receive(:mysql2_connection) do |config|
88
88
  config[:username] = db_username
@@ -2,80 +2,8 @@ require 'spec_helper'
2
2
 
3
3
  describe Makara::Cache do
4
4
 
5
- it 'should not require a store be set' do
6
- described_class.store = nil
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
@@ -79,7 +79,7 @@ describe Makara::ConfigParser do
79
79
 
80
80
  end
81
81
 
82
- it 'should provide an id based on the recursively sorted config' do
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)