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.
@@ -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)