makara 0.3.5
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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +2 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +28 -0
- data/CHANGELOG.md +27 -0
- data/Gemfile +18 -0
- data/LICENSE.txt +22 -0
- data/README.md +278 -0
- data/Rakefile +9 -0
- data/gemfiles/ar30.gemfile +30 -0
- data/gemfiles/ar31.gemfile +29 -0
- data/gemfiles/ar32.gemfile +29 -0
- data/gemfiles/ar40.gemfile +15 -0
- data/gemfiles/ar41.gemfile +15 -0
- data/gemfiles/ar42.gemfile +15 -0
- data/lib/active_record/connection_adapters/jdbcmysql_makara_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/jdbcpostgresql_makara_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +209 -0
- data/lib/active_record/connection_adapters/makara_jdbcmysql_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/makara_jdbcpostgresql_adapter.rb +25 -0
- data/lib/active_record/connection_adapters/makara_mysql2_adapter.rb +44 -0
- data/lib/active_record/connection_adapters/makara_postgresql_adapter.rb +44 -0
- data/lib/active_record/connection_adapters/mysql2_makara_adapter.rb +44 -0
- data/lib/active_record/connection_adapters/postgresql_makara_adapter.rb +44 -0
- data/lib/makara.rb +25 -0
- data/lib/makara/cache.rb +53 -0
- data/lib/makara/cache/memory_store.rb +28 -0
- data/lib/makara/cache/noop_store.rb +15 -0
- data/lib/makara/config_parser.rb +200 -0
- data/lib/makara/connection_wrapper.rb +170 -0
- data/lib/makara/context.rb +46 -0
- data/lib/makara/error_handler.rb +39 -0
- data/lib/makara/errors/all_connections_blacklisted.rb +13 -0
- data/lib/makara/errors/blacklist_connection.rb +14 -0
- data/lib/makara/errors/no_connections_available.rb +14 -0
- data/lib/makara/logging/logger.rb +23 -0
- data/lib/makara/logging/subscriber.rb +38 -0
- data/lib/makara/middleware.rb +109 -0
- data/lib/makara/pool.rb +188 -0
- data/lib/makara/proxy.rb +277 -0
- data/lib/makara/railtie.rb +14 -0
- data/lib/makara/version.rb +15 -0
- data/makara.gemspec +19 -0
- data/spec/active_record/connection_adapters/makara_abstract_adapter_error_handling_spec.rb +92 -0
- data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +114 -0
- data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +183 -0
- data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +121 -0
- data/spec/cache_spec.rb +59 -0
- data/spec/config_parser_spec.rb +102 -0
- data/spec/connection_wrapper_spec.rb +33 -0
- data/spec/context_spec.rb +107 -0
- data/spec/middleware_spec.rb +84 -0
- data/spec/pool_spec.rb +158 -0
- data/spec/proxy_spec.rb +182 -0
- data/spec/spec_helper.rb +46 -0
- data/spec/support/configurator.rb +13 -0
- data/spec/support/deep_dup.rb +12 -0
- data/spec/support/mock_objects.rb +67 -0
- data/spec/support/mysql2_database.yml +17 -0
- data/spec/support/mysql2_database_with_custom_errors.yml +17 -0
- data/spec/support/pool_extensions.rb +14 -0
- data/spec/support/postgresql_database.yml +13 -0
- data/spec/support/proxy_extensions.rb +33 -0
- data/spec/support/schema.rb +7 -0
- metadata +144 -0
@@ -0,0 +1,170 @@
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
2
|
+
|
3
|
+
# Makara::ConnectionWrapper wraps the instance of an underlying connection.
|
4
|
+
# The wrapper provides methods for tracking blacklisting and individual makara configurations.
|
5
|
+
# Upon creation, the wrapper defines methods in the underlying object giving it access to the
|
6
|
+
# Makara::Proxy.
|
7
|
+
|
8
|
+
module Makara
|
9
|
+
class ConnectionWrapper
|
10
|
+
|
11
|
+
attr_accessor :initial_error, :config
|
12
|
+
|
13
|
+
# invalid queries caused by connections switching that needs to be replaced
|
14
|
+
SQL_REPLACE = {"SET client_min_messages TO ''".freeze => "SET client_min_messages TO 'warning'".freeze}.freeze
|
15
|
+
|
16
|
+
def initialize(proxy, connection, config)
|
17
|
+
@config = config.symbolize_keys
|
18
|
+
@connection = connection
|
19
|
+
@proxy = proxy
|
20
|
+
|
21
|
+
if connection.nil?
|
22
|
+
_makara_blacklist!
|
23
|
+
else
|
24
|
+
_makara_decorate_connection(connection)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# the weight of the current node
|
29
|
+
def _makara_weight
|
30
|
+
@config[:weight] || 1
|
31
|
+
end
|
32
|
+
|
33
|
+
# the name of this node
|
34
|
+
def _makara_name
|
35
|
+
@config[:name]
|
36
|
+
end
|
37
|
+
|
38
|
+
# has this node been blacklisted?
|
39
|
+
def _makara_blacklisted?
|
40
|
+
@blacklisted_until.to_i > Time.now.to_i
|
41
|
+
end
|
42
|
+
|
43
|
+
# blacklist this node for @config[:blacklist_duration] seconds
|
44
|
+
def _makara_blacklist!
|
45
|
+
@connection.disconnect! if @connection
|
46
|
+
@connection = nil
|
47
|
+
@blacklisted_until = Time.now.to_i + @config[:blacklist_duration]
|
48
|
+
end
|
49
|
+
|
50
|
+
# release the blacklist
|
51
|
+
def _makara_whitelist!
|
52
|
+
@blacklisted_until = nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# custom error messages
|
56
|
+
def _makara_custom_error_matchers
|
57
|
+
@custom_error_matchers ||= (@config[:connection_error_matchers] || [])
|
58
|
+
end
|
59
|
+
|
60
|
+
def _makara_connected?
|
61
|
+
_makara_connection.present?
|
62
|
+
rescue Makara::Errors::BlacklistConnection
|
63
|
+
false
|
64
|
+
end
|
65
|
+
|
66
|
+
def _makara_connection
|
67
|
+
current = @connection
|
68
|
+
|
69
|
+
if current
|
70
|
+
current
|
71
|
+
else # blacklisted connection or initial error
|
72
|
+
new_connection = @proxy.graceful_connection_for(@config)
|
73
|
+
|
74
|
+
# Already wrapped because of initial failure
|
75
|
+
if new_connection.is_a?(Makara::ConnectionWrapper)
|
76
|
+
_makara_blacklist!
|
77
|
+
raise Makara::Errors::BlacklistConnection.new(self, new_connection.initial_error)
|
78
|
+
else
|
79
|
+
@connection = new_connection
|
80
|
+
_makara_decorate_connection(new_connection)
|
81
|
+
new_connection
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def execute(*args)
|
87
|
+
SQL_REPLACE.each do |find, replace|
|
88
|
+
if args[0] == find
|
89
|
+
args[0] = replace
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
_makara_connection.execute(*args)
|
94
|
+
end
|
95
|
+
|
96
|
+
# we want to forward all private methods, since we could have kicked out from a private scenario
|
97
|
+
def method_missing(m, *args, &block)
|
98
|
+
if _makara_connection.respond_to?(m)
|
99
|
+
_makara_connection.public_send(m, *args, &block)
|
100
|
+
else # probably private method
|
101
|
+
_makara_connection.__send__(m, *args, &block)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
|
107
|
+
def respond_to#{RUBY_VERSION.to_s =~ /^1.8/ ? nil : '_missing'}?(m, include_private = false)
|
108
|
+
_makara_connection.respond_to?(m, true)
|
109
|
+
end
|
110
|
+
RUBY_EVAL
|
111
|
+
|
112
|
+
|
113
|
+
protected
|
114
|
+
|
115
|
+
# once the underlying connection is present we must evaluate extra functionality into it.
|
116
|
+
# all extra functionality is in the format of _makara*
|
117
|
+
def _makara_decorate_connection(con)
|
118
|
+
|
119
|
+
extension = %Q{
|
120
|
+
# the proxy object controlling this connection
|
121
|
+
def _makara
|
122
|
+
@_makara
|
123
|
+
end
|
124
|
+
|
125
|
+
def _makara=(m)
|
126
|
+
@_makara = m
|
127
|
+
end
|
128
|
+
|
129
|
+
# if the proxy has already decided the correct connection to use, yield nil.
|
130
|
+
# if the proxy has yet to decide, yield the proxy
|
131
|
+
def _makara_hijack
|
132
|
+
if _makara.hijacked?
|
133
|
+
yield nil
|
134
|
+
else
|
135
|
+
yield _makara
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
# for logging, errors, and debugging
|
140
|
+
def _makara_name
|
141
|
+
#{@config[:name].inspect}
|
142
|
+
end
|
143
|
+
}
|
144
|
+
|
145
|
+
# Each method the Makara::Proxy needs to hijack should be redefined in the underlying connection.
|
146
|
+
# The new definition should allow for the proxy to intercept the invocation if required.
|
147
|
+
@proxy.class.hijack_methods.each do |meth|
|
148
|
+
extension << %Q{
|
149
|
+
def #{meth}(*args)
|
150
|
+
_makara_hijack do |proxy|
|
151
|
+
if proxy
|
152
|
+
proxy.#{meth}(*args)
|
153
|
+
else
|
154
|
+
super
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
}
|
159
|
+
end
|
160
|
+
|
161
|
+
# extend the instance
|
162
|
+
con.instance_eval(extension)
|
163
|
+
# set the makara context
|
164
|
+
con._makara = @proxy
|
165
|
+
|
166
|
+
con._makara
|
167
|
+
end
|
168
|
+
|
169
|
+
end
|
170
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'digest/md5'
|
2
|
+
|
3
|
+
# Keeps track of the current and previous context (hexdigests)
|
4
|
+
# If a new context is needed it can be generated via Makara::Context.generate
|
5
|
+
|
6
|
+
module Makara
|
7
|
+
class Context
|
8
|
+
class << self
|
9
|
+
|
10
|
+
def generate(seed = nil)
|
11
|
+
seed ||= "#{Time.now.to_i}#{Thread.current.object_id}#{rand(99999)}"
|
12
|
+
Digest::MD5.hexdigest(seed)
|
13
|
+
end
|
14
|
+
|
15
|
+
def get_previous
|
16
|
+
get_current_thread_local_for(:makara_context_previous)
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_previous(context)
|
20
|
+
set_current_thread_local(:makara_context_previous,context)
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_current
|
24
|
+
get_current_thread_local_for(:makara_context_current)
|
25
|
+
end
|
26
|
+
|
27
|
+
def set_current(context)
|
28
|
+
set_current_thread_local(:makara_context_current,context)
|
29
|
+
end
|
30
|
+
|
31
|
+
protected
|
32
|
+
|
33
|
+
def get_current_thread_local_for(type)
|
34
|
+
t = Thread.current
|
35
|
+
current = t.respond_to?(:thread_variable_get) ? t.thread_variable_get(type) : t[type]
|
36
|
+
current ||= set_current_thread_local(type,generate)
|
37
|
+
end
|
38
|
+
|
39
|
+
def set_current_thread_local(type,context)
|
40
|
+
t = Thread.current
|
41
|
+
t.respond_to?(:thread_variable_set) ? t.thread_variable_set(type,context) : t[type]=context
|
42
|
+
end
|
43
|
+
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# Base class to handle errors when invoking an underlying connection from a makara proxy.
|
2
|
+
# Each concrete implementation of a MakaraProxy can provide it's own ErrorHandler which should inherit
|
3
|
+
# from this class.
|
4
|
+
|
5
|
+
module Makara
|
6
|
+
class ErrorHandler
|
7
|
+
|
8
|
+
|
9
|
+
def handle(connection)
|
10
|
+
yield
|
11
|
+
|
12
|
+
rescue Exception => e
|
13
|
+
|
14
|
+
if e.class.name =~ /^Makara::/
|
15
|
+
harshly(e)
|
16
|
+
else
|
17
|
+
gracefully(connection, e)
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
protected
|
24
|
+
|
25
|
+
|
26
|
+
def gracefully(connection, e)
|
27
|
+
err = Makara::Errors::BlacklistConnection.new(connection, e)
|
28
|
+
::Makara::Logging::Logger.log("[Makara] Gracefully handling: #{err}")
|
29
|
+
raise err
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def harshly(e)
|
34
|
+
::Makara::Logging::Logger.log("[Makara] Harshly handling: #{e}")
|
35
|
+
raise e
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Makara
|
2
|
+
module Errors
|
3
|
+
class AllConnectionsBlacklisted < StandardError
|
4
|
+
|
5
|
+
def initialize(pool, errors)
|
6
|
+
errors = [*errors]
|
7
|
+
messages = errors.empty? ? 'No error details' : errors.map(&:message).join(' -> ')
|
8
|
+
super "[Makara/#{pool.role}] All connections are blacklisted -> " + messages
|
9
|
+
end
|
10
|
+
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
module Makara
|
2
|
+
module Errors
|
3
|
+
class BlacklistConnection < ::StandardError
|
4
|
+
|
5
|
+
attr_reader :original_error
|
6
|
+
|
7
|
+
def initialize(connection, error)
|
8
|
+
@original_error = error
|
9
|
+
super "[Makara/#{connection._makara_name}] #{error.message}"
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module Makara
|
2
|
+
module Logging
|
3
|
+
class Logger
|
4
|
+
|
5
|
+
class << self
|
6
|
+
|
7
|
+
def log(msg, format = :error)
|
8
|
+
logger.send(format, msg) if logger
|
9
|
+
end
|
10
|
+
|
11
|
+
def logger
|
12
|
+
@logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def logger=(l)
|
16
|
+
@logger = l
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Makara
|
2
|
+
module Logging
|
3
|
+
|
4
|
+
module Subscriber
|
5
|
+
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
|
6
|
+
|
7
|
+
def sql(event)
|
8
|
+
name = event.payload[:name]
|
9
|
+
unless IGNORE_PAYLOAD_NAMES.include?(name)
|
10
|
+
name = [current_wrapper_name(event), name].compact.join(' ')
|
11
|
+
event.payload[:name] = name
|
12
|
+
end
|
13
|
+
super(event)
|
14
|
+
end
|
15
|
+
|
16
|
+
protected
|
17
|
+
|
18
|
+
# grabs the adapter used in this event via it's object_id
|
19
|
+
# uses the adapter's connection proxy to modify the name of the event
|
20
|
+
# the name of the used connection will be prepended to the sql log
|
21
|
+
###
|
22
|
+
### [Master|Slave] User Load (1.3ms) SELECT * FROM `users`;
|
23
|
+
###
|
24
|
+
def current_wrapper_name(event)
|
25
|
+
connection_object_id = event.payload[:connection_id]
|
26
|
+
return nil unless connection_object_id
|
27
|
+
|
28
|
+
adapter = ObjectSpace._id2ref(connection_object_id)
|
29
|
+
|
30
|
+
return nil unless adapter
|
31
|
+
return nil unless adapter.respond_to?(:_makara_name)
|
32
|
+
|
33
|
+
"[#{adapter._makara_name}]"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'rack'
|
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
|
+
|
8
|
+
module Makara
|
9
|
+
class Middleware
|
10
|
+
|
11
|
+
IDENTIFIER = '_mkra_ctxt'
|
12
|
+
|
13
|
+
|
14
|
+
def initialize(app)
|
15
|
+
@app = app
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def call(env)
|
20
|
+
|
21
|
+
return @app.call(env) if ignore_request?(env)
|
22
|
+
|
23
|
+
Makara::Context.set_previous previous_context(env)
|
24
|
+
Makara::Context.set_current new_context(env)
|
25
|
+
|
26
|
+
status, headers, body = @app.call(env)
|
27
|
+
|
28
|
+
store_context(status, headers)
|
29
|
+
|
30
|
+
[status, headers, body]
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
protected
|
35
|
+
|
36
|
+
|
37
|
+
# ignore asset paths
|
38
|
+
# consider allowing a filter proc to be provided in an initializer
|
39
|
+
def ignore_request?(env)
|
40
|
+
if defined?(Rails)
|
41
|
+
if env['PATH_INFO'].to_s =~ /^#{Rails.application.config.assets.prefix}/
|
42
|
+
return true
|
43
|
+
end
|
44
|
+
end
|
45
|
+
false
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
# generate a new context based on the request
|
50
|
+
# if the previous request was a redirect, we keep the same context
|
51
|
+
def new_context(env)
|
52
|
+
|
53
|
+
makara_context, makara_status = makara_values(env)
|
54
|
+
|
55
|
+
context = nil
|
56
|
+
|
57
|
+
# if the previous request was a redirect, let's keep that context
|
58
|
+
if makara_status.to_s =~ /^3/ # 300+ redirect
|
59
|
+
context = makara_context
|
60
|
+
end
|
61
|
+
|
62
|
+
context ||= Makara::Context.get_current if env['rack.test']
|
63
|
+
context ||= Makara::Context.generate(env["action_dispatch.request_id"])
|
64
|
+
context
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
# pulls the previous context out of the request
|
69
|
+
def previous_context(env)
|
70
|
+
context = makara_values(env).first
|
71
|
+
context ||= Makara::Context.get_previous if env['rack.test']
|
72
|
+
context ||= Makara::Context.generate
|
73
|
+
context
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# retrieve the stored content from the cookie or query
|
78
|
+
# The value contains the hexdigest and status code of the previous
|
79
|
+
# response in the format: $digest--$status
|
80
|
+
def makara_values(env)
|
81
|
+
regex = /#{IDENTIFIER}=([\-a-z0-9A-Z]+)/
|
82
|
+
|
83
|
+
env['HTTP_COOKIE'].to_s =~ regex
|
84
|
+
return $1.split('--') if $1
|
85
|
+
|
86
|
+
env['QUERY_STRING'].to_s =~ regex
|
87
|
+
return $1.split('--') if $1
|
88
|
+
|
89
|
+
[nil, nil]
|
90
|
+
end
|
91
|
+
|
92
|
+
|
93
|
+
# push the current context into the cookie
|
94
|
+
# it should always be for the same path, only
|
95
|
+
# accessible via http and live for a short amount
|
96
|
+
# of time
|
97
|
+
def store_context(status, header)
|
98
|
+
|
99
|
+
cookie_value = {
|
100
|
+
:path => '/',
|
101
|
+
:value => "#{Makara::Context.get_current}--#{status}",
|
102
|
+
:http_only => true,
|
103
|
+
:max_age => '5'
|
104
|
+
}
|
105
|
+
|
106
|
+
Rack::Utils.set_cookie_header!(header, IDENTIFIER, cookie_value)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|