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.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +2 -0
  4. data/.ruby-gemset +1 -0
  5. data/.ruby-version +1 -0
  6. data/.travis.yml +28 -0
  7. data/CHANGELOG.md +27 -0
  8. data/Gemfile +18 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +278 -0
  11. data/Rakefile +9 -0
  12. data/gemfiles/ar30.gemfile +30 -0
  13. data/gemfiles/ar31.gemfile +29 -0
  14. data/gemfiles/ar32.gemfile +29 -0
  15. data/gemfiles/ar40.gemfile +15 -0
  16. data/gemfiles/ar41.gemfile +15 -0
  17. data/gemfiles/ar42.gemfile +15 -0
  18. data/lib/active_record/connection_adapters/jdbcmysql_makara_adapter.rb +25 -0
  19. data/lib/active_record/connection_adapters/jdbcpostgresql_makara_adapter.rb +25 -0
  20. data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +209 -0
  21. data/lib/active_record/connection_adapters/makara_jdbcmysql_adapter.rb +25 -0
  22. data/lib/active_record/connection_adapters/makara_jdbcpostgresql_adapter.rb +25 -0
  23. data/lib/active_record/connection_adapters/makara_mysql2_adapter.rb +44 -0
  24. data/lib/active_record/connection_adapters/makara_postgresql_adapter.rb +44 -0
  25. data/lib/active_record/connection_adapters/mysql2_makara_adapter.rb +44 -0
  26. data/lib/active_record/connection_adapters/postgresql_makara_adapter.rb +44 -0
  27. data/lib/makara.rb +25 -0
  28. data/lib/makara/cache.rb +53 -0
  29. data/lib/makara/cache/memory_store.rb +28 -0
  30. data/lib/makara/cache/noop_store.rb +15 -0
  31. data/lib/makara/config_parser.rb +200 -0
  32. data/lib/makara/connection_wrapper.rb +170 -0
  33. data/lib/makara/context.rb +46 -0
  34. data/lib/makara/error_handler.rb +39 -0
  35. data/lib/makara/errors/all_connections_blacklisted.rb +13 -0
  36. data/lib/makara/errors/blacklist_connection.rb +14 -0
  37. data/lib/makara/errors/no_connections_available.rb +14 -0
  38. data/lib/makara/logging/logger.rb +23 -0
  39. data/lib/makara/logging/subscriber.rb +38 -0
  40. data/lib/makara/middleware.rb +109 -0
  41. data/lib/makara/pool.rb +188 -0
  42. data/lib/makara/proxy.rb +277 -0
  43. data/lib/makara/railtie.rb +14 -0
  44. data/lib/makara/version.rb +15 -0
  45. data/makara.gemspec +19 -0
  46. data/spec/active_record/connection_adapters/makara_abstract_adapter_error_handling_spec.rb +92 -0
  47. data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +114 -0
  48. data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +183 -0
  49. data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +121 -0
  50. data/spec/cache_spec.rb +59 -0
  51. data/spec/config_parser_spec.rb +102 -0
  52. data/spec/connection_wrapper_spec.rb +33 -0
  53. data/spec/context_spec.rb +107 -0
  54. data/spec/middleware_spec.rb +84 -0
  55. data/spec/pool_spec.rb +158 -0
  56. data/spec/proxy_spec.rb +182 -0
  57. data/spec/spec_helper.rb +46 -0
  58. data/spec/support/configurator.rb +13 -0
  59. data/spec/support/deep_dup.rb +12 -0
  60. data/spec/support/mock_objects.rb +67 -0
  61. data/spec/support/mysql2_database.yml +17 -0
  62. data/spec/support/mysql2_database_with_custom_errors.yml +17 -0
  63. data/spec/support/pool_extensions.rb +14 -0
  64. data/spec/support/postgresql_database.yml +13 -0
  65. data/spec/support/proxy_extensions.rb +33 -0
  66. data/spec/support/schema.rb +7 -0
  67. 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,14 @@
1
+ module Makara
2
+ module Errors
3
+ class NoConnectionsAvailable < ::StandardError
4
+
5
+ attr_reader :role
6
+
7
+ def initialize(role)
8
+ @role = role
9
+ super "[Makara] No connections are available in the #{role} pool"
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