makara 0.3.9 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (92) hide show
  1. checksums.yaml +5 -5
  2. data/.github/dependabot.yml +11 -0
  3. data/.github/workflows/CI.yml +88 -0
  4. data/.github/workflows/gem-publish-public.yml +36 -0
  5. data/.rspec +1 -1
  6. data/.rubocop.yml +15 -0
  7. data/.rubocop_todo.yml +670 -0
  8. data/CHANGELOG.md +88 -32
  9. data/Gemfile +1 -16
  10. data/README.md +39 -35
  11. data/Rakefile +1 -1
  12. data/gemfiles/activerecord_5.2.gemfile +8 -0
  13. data/gemfiles/activerecord_6.0.gemfile +8 -0
  14. data/gemfiles/activerecord_6.1.gemfile +8 -0
  15. data/gemfiles/activerecord_head.gemfile +6 -0
  16. data/lib/active_record/connection_adapters/jdbcmysql_makara_adapter.rb +4 -18
  17. data/lib/active_record/connection_adapters/jdbcpostgresql_makara_adapter.rb +4 -18
  18. data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +111 -33
  19. data/lib/active_record/connection_adapters/makara_jdbcmysql_adapter.rb +4 -18
  20. data/lib/active_record/connection_adapters/makara_jdbcpostgresql_adapter.rb +4 -18
  21. data/lib/active_record/connection_adapters/makara_mysql2_adapter.rb +4 -20
  22. data/lib/active_record/connection_adapters/makara_postgis_adapter.rb +4 -19
  23. data/lib/active_record/connection_adapters/makara_postgresql_adapter.rb +4 -20
  24. data/lib/active_record/connection_adapters/mysql2_makara_adapter.rb +4 -20
  25. data/lib/active_record/connection_adapters/postgresql_makara_adapter.rb +4 -20
  26. data/lib/makara.rb +14 -5
  27. data/lib/makara/cache.rb +4 -42
  28. data/lib/makara/config_parser.rb +18 -16
  29. data/lib/makara/connection_wrapper.rb +43 -22
  30. data/lib/makara/context.rb +108 -37
  31. data/lib/makara/cookie.rb +53 -0
  32. data/lib/makara/error_handler.rb +2 -11
  33. data/lib/makara/errors/all_connections_blacklisted.rb +0 -2
  34. data/lib/makara/errors/blacklist_connection.rb +0 -2
  35. data/lib/makara/errors/blacklisted_while_in_transaction.rb +12 -0
  36. data/lib/makara/errors/invalid_shard.rb +14 -0
  37. data/lib/makara/errors/makara_error.rb +0 -1
  38. data/lib/makara/errors/no_connections_available.rb +0 -2
  39. data/lib/makara/logging/logger.rb +1 -5
  40. data/lib/makara/logging/subscriber.rb +0 -2
  41. data/lib/makara/middleware.rb +12 -76
  42. data/lib/makara/pool.rb +55 -47
  43. data/lib/makara/proxy.rb +76 -56
  44. data/lib/makara/railtie.rb +0 -8
  45. data/lib/makara/strategies/abstract.rb +1 -0
  46. data/lib/makara/strategies/priority_failover.rb +2 -0
  47. data/lib/makara/strategies/round_robin.rb +7 -3
  48. data/lib/makara/strategies/shard_aware.rb +45 -0
  49. data/lib/makara/version.rb +2 -4
  50. data/makara.gemspec +26 -3
  51. data/spec/active_record/connection_adapters/makara_abstract_adapter_error_handling_spec.rb +1 -6
  52. data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +10 -14
  53. data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +18 -16
  54. data/spec/active_record/connection_adapters/makara_postgis_adapter_spec.rb +6 -9
  55. data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +70 -10
  56. data/spec/cache_spec.rb +2 -53
  57. data/spec/config_parser_spec.rb +75 -57
  58. data/spec/connection_wrapper_spec.rb +6 -4
  59. data/spec/context_spec.rb +163 -100
  60. data/spec/cookie_spec.rb +72 -0
  61. data/spec/middleware_spec.rb +27 -56
  62. data/spec/pool_spec.rb +25 -14
  63. data/spec/proxy_spec.rb +50 -39
  64. data/spec/spec_helper.rb +10 -10
  65. data/spec/strategies/priority_failover_spec.rb +3 -4
  66. data/spec/strategies/round_robin_spec.rb +4 -8
  67. data/spec/strategies/shard_aware_spec.rb +218 -0
  68. data/spec/support/deep_dup.rb +1 -1
  69. data/spec/support/helpers.rb +10 -6
  70. data/spec/support/mock_objects.rb +6 -5
  71. data/spec/support/mysql2_database.yml +3 -2
  72. data/spec/support/mysql2_database_with_custom_errors.yml +6 -1
  73. data/spec/support/pool_extensions.rb +0 -3
  74. data/spec/support/postgis_database.yml +2 -0
  75. data/spec/support/postgis_schema.rb +6 -3
  76. data/spec/support/postgresql_database.yml +2 -2
  77. data/spec/support/proxy_extensions.rb +1 -3
  78. data/spec/support/schema.rb +6 -6
  79. data/spec/support/user.rb +4 -0
  80. metadata +170 -22
  81. data/.travis.yml +0 -70
  82. data/gemfiles/ar-head.gemfile +0 -15
  83. data/gemfiles/ar30.gemfile +0 -32
  84. data/gemfiles/ar31.gemfile +0 -31
  85. data/gemfiles/ar32.gemfile +0 -31
  86. data/gemfiles/ar40.gemfile +0 -17
  87. data/gemfiles/ar41.gemfile +0 -17
  88. data/gemfiles/ar42.gemfile +0 -17
  89. data/gemfiles/ar50.gemfile +0 -15
  90. data/gemfiles/ar51.gemfile +0 -15
  91. data/lib/makara/cache/memory_store.rb +0 -28
  92. data/lib/makara/cache/noop_store.rb +0 -15
data/lib/makara/proxy.rb CHANGED
@@ -11,11 +11,11 @@ require 'active_support/core_ext/string/inflections'
11
11
 
12
12
  module Makara
13
13
  class Proxy < ::SimpleDelegator
14
-
15
14
  METHOD_MISSING_SKIP = [ :byebug, :puts ]
16
15
 
17
- class_attribute :hijack_methods
16
+ class_attribute :hijack_methods, :control_methods
18
17
  self.hijack_methods = []
18
+ self.control_methods = []
19
19
 
20
20
  class << self
21
21
  def hijack_method(*method_names)
@@ -23,27 +23,44 @@ module Makara
23
23
  self.hijack_methods |= method_names
24
24
 
25
25
  method_names.each do |method_name|
26
- define_method method_name do |*args, &block|
26
+ define_method(method_name) do |*args, &block|
27
27
  appropriate_connection(method_name, args) do |con|
28
28
  con.send(method_name, *args, &block)
29
29
  end
30
30
  end
31
+
32
+ ruby2_keywords method_name if Module.private_method_defined?(:ruby2_keywords)
31
33
  end
32
34
  end
33
35
 
34
36
  def send_to_all(*method_names)
35
37
  method_names.each do |method_name|
36
- define_method method_name do |*args|
37
- send_to_all method_name, *args
38
+ define_method(method_name) do |*args|
39
+ send_to_all(method_name, *args)
38
40
  end
41
+
42
+ ruby2_keywords method_name if Module.private_method_defined?(:ruby2_keywords)
39
43
  end
40
44
  end
41
- end
42
45
 
46
+ def control_method(*method_names)
47
+ self.control_methods = self.control_methods || []
48
+ self.control_methods |= method_names
49
+
50
+ method_names.each do |method_name|
51
+ define_method(method_name) do |*args, &block|
52
+ control&.send(method_name, *args, &block)
53
+ end
54
+
55
+ ruby2_keywords method_name if Module.private_method_defined?(:ruby2_keywords)
56
+ end
57
+ end
58
+ end
43
59
 
44
60
  attr_reader :error_handler
45
61
  attr_reader :sticky
46
62
  attr_reader :config_parser
63
+ attr_reader :control
47
64
 
48
65
  def initialize(config)
49
66
  @config = config.symbolize_keys
@@ -59,22 +76,21 @@ module Makara
59
76
  end
60
77
 
61
78
  def without_sticking
62
- before_context = @master_context
63
- @master_context = nil
64
79
  @skip_sticking = true
65
80
  yield
66
81
  ensure
67
82
  @skip_sticking = false
68
- @master_context ||= before_context
69
83
  end
70
84
 
71
85
  def hijacked?
72
86
  @hijacked
73
87
  end
74
88
 
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
89
+ # If persist is true, we stick the proxy to master for subsequent requests
90
+ # up to master_ttl duration. Otherwise we just stick it for the current request
91
+ def stick_to_master!(persist = true)
92
+ stickiness_duration = persist ? @ttl : 0
93
+ Makara::Context.stick(@id, stickiness_duration)
78
94
  end
79
95
 
80
96
  def strategy_for(role)
@@ -85,6 +101,14 @@ module Makara
85
101
  @config_parser.makara_config["#{role}_strategy".to_sym]
86
102
  end
87
103
 
104
+ def shard_aware_for(role)
105
+ @config_parser.makara_config["#{role}_shard_aware".to_sym]
106
+ end
107
+
108
+ def default_shard_for(role)
109
+ @config_parser.makara_config["#{role}_default_shard".to_sym]
110
+ end
111
+
88
112
  def strategy_class_for(strategy_name)
89
113
  case strategy_name
90
114
  when 'round_robin', 'roundrobin', nil, ''
@@ -98,27 +122,25 @@ module Makara
98
122
 
99
123
  def method_missing(m, *args, &block)
100
124
  if METHOD_MISSING_SKIP.include?(m)
101
- return super(m, *args, &block)
125
+ return super
102
126
  end
103
127
 
104
128
  any_connection do |con|
105
- if con.respond_to?(m)
106
- con.public_send(m, *args, &block)
107
- elsif con.respond_to?(m, true)
108
- con.__send__(m, *args, &block)
129
+ if con.respond_to?(m, true)
130
+ con.send(m, *args, &block)
109
131
  else
110
- super(m, *args, &block)
132
+ super
111
133
  end
112
134
  end
113
135
  end
114
136
 
115
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
116
- def respond_to#{RUBY_VERSION.to_s =~ /^1.8/ ? nil : '_missing'}?(m, include_private = false)
117
- any_connection do |con|
118
- con._makara_connection.respond_to?(m, true)
119
- end
137
+ ruby2_keywords :method_missing if Module.private_method_defined?(:ruby2_keywords)
138
+
139
+ def respond_to_missing?(m, include_private = false)
140
+ any_connection do |con|
141
+ con._makara_connection.respond_to?(m, true)
120
142
  end
121
- RUBY_EVAL
143
+ end
122
144
 
123
145
  def graceful_connection_for(config)
124
146
  fake_wrapper = Makara::ConnectionWrapper.new(self, nil, config)
@@ -139,18 +161,25 @@ module Makara
139
161
 
140
162
  protected
141
163
 
142
-
143
164
  def send_to_all(method_name, *args)
144
165
  # slave pool must run first to allow for slave-->master failover without running operations on master twice.
145
166
  handling_an_all_execution(method_name) do
146
- @slave_pool.send_to_all method_name, *args
147
- @master_pool.send_to_all method_name, *args
167
+ @slave_pool.send_to_all(method_name, *args)
168
+ @master_pool.send_to_all(method_name, *args)
148
169
  end
149
170
  end
150
171
 
172
+ ruby2_keywords :send_to_all if Module.private_method_defined?(:ruby2_keywords)
173
+
151
174
  def any_connection
152
- @master_pool.provide do |con|
153
- yield con
175
+ if @master_pool.disabled
176
+ @slave_pool.provide do |con|
177
+ yield con
178
+ end
179
+ else
180
+ @master_pool.provide do |con|
181
+ yield con
182
+ end
154
183
  end
155
184
  rescue ::Makara::Errors::AllConnectionsBlacklisted, ::Makara::Errors::NoConnectionsAvailable
156
185
  begin
@@ -176,15 +205,11 @@ module Makara
176
205
  end
177
206
  end
178
207
 
179
-
180
208
  # master or slave
181
209
  def appropriate_pool(method_name, args)
182
-
183
210
  # for testing purposes
184
211
  pool = _appropriate_pool(method_name, args)
185
-
186
212
  yield pool
187
-
188
213
  rescue ::Makara::Errors::AllConnectionsBlacklisted, ::Makara::Errors::NoConnectionsAvailable => e
189
214
  if pool == @master_pool
190
215
  @master_pool.connections.each(&:_makara_whitelist!)
@@ -202,15 +227,11 @@ module Makara
202
227
  stick_to_master(method_name, args)
203
228
  @master_pool
204
229
 
205
- # in this context, we've already stuck to master
206
- elsif Makara::Context.get_current == @master_context
207
- @master_pool
208
-
209
- elsif previously_stuck_to_master?
230
+ elsif stuck_to_master?
210
231
 
211
- # we're only on master because of the previous context so
212
- # behave like we're sticking to master but store the current context
213
- stick_to_master(method_name, args, false)
232
+ # we're on master because we already stuck this proxy in this
233
+ # request or because we got stuck in previous requests and the
234
+ # stickiness is still valid
214
235
  @master_pool
215
236
 
216
237
  # all slaves are down (or empty)
@@ -247,29 +268,28 @@ module Makara
247
268
  @hijacked = false
248
269
  end
249
270
 
250
-
251
- def previously_stuck_to_master?
252
- @sticky && Makara::Context.previously_stuck?(@id)
271
+ def stuck_to_master?
272
+ sticky? && Makara::Context.stuck?(@id)
253
273
  end
254
274
 
255
-
256
- def stick_to_master(method_name, args, write_to_cache = true)
257
- # if we're already stuck to master, don't bother doing it again
258
- return if @master_context == Makara::Context.get_current
259
-
275
+ def stick_to_master(method_name, args)
260
276
  # check to see if we're configured, bypassed, or some custom implementation has input
261
277
  return unless should_stick?(method_name, args)
262
278
 
263
279
  # do the sticking
264
- stick_to_master!(write_to_cache)
280
+ stick_to_master!
265
281
  end
266
282
 
267
-
268
- # if we are configured to be sticky and we aren't bypassing stickiness
283
+ # For the generic proxy implementation, we stick if we are sticky,
284
+ # method and args don't matter
269
285
  def should_stick?(method_name, args)
270
- @sticky && !@skip_sticking
286
+ sticky?
271
287
  end
272
288
 
289
+ # If we are configured to be sticky and we aren't bypassing stickiness,
290
+ def sticky?
291
+ @sticky && !@skip_sticking
292
+ end
273
293
 
274
294
  # use the config parser to generate a master and slave pool
275
295
  def instantiate_connections
@@ -292,18 +312,18 @@ module Makara
292
312
  yield
293
313
  rescue ::Makara::Errors::NoConnectionsAvailable => e
294
314
  if e.role == 'master'
295
- Kernel.raise ::Makara::Errors::NoConnectionsAvailable.new('master and slave')
315
+ # this means slave connections are good.
316
+ return
296
317
  end
318
+
297
319
  @slave_pool.disabled = true
298
320
  yield
299
321
  ensure
300
322
  @slave_pool.disabled = false
301
323
  end
302
324
 
303
-
304
325
  def connection_for(config)
305
326
  Kernel.raise NotImplementedError
306
327
  end
307
-
308
328
  end
309
329
  end
@@ -1,15 +1,7 @@
1
1
  module Makara
2
2
  class Railtie < ::Rails::Railtie
3
-
4
3
  initializer "makara.configure_rails_initialization" do |app|
5
4
  app.middleware.use Makara::Middleware
6
5
  end
7
-
8
- initializer "makara.initialize_logger" do |app|
9
- ActiveRecord::LogSubscriber.log_subscribers.each do |subscriber|
10
- subscriber.extend ::Makara::Logging::Subscriber
11
- end
12
- end
13
-
14
6
  end
15
7
  end
@@ -2,6 +2,7 @@ module Makara
2
2
  module Strategies
3
3
  class Abstract
4
4
  attr_reader :pool
5
+
5
6
  def initialize(pool)
6
7
  @pool = pool
7
8
  init
@@ -27,6 +27,7 @@ module Makara
27
27
  @weighted_connections.each_with_index do |con, index|
28
28
  check = safe_value(index)
29
29
  next unless check
30
+
30
31
  @current_idx = index
31
32
  return check
32
33
  end
@@ -41,6 +42,7 @@ module Makara
41
42
  con = @weighted_connections[idx]
42
43
  return nil unless con
43
44
  return nil if con._makara_blacklisted?
45
+
44
46
  con
45
47
  end
46
48
  end
@@ -23,9 +23,11 @@ module Makara
23
23
  end
24
24
 
25
25
  def next
26
+ return safe_value(0, true) if single_one?
27
+
26
28
  idx = @current_idx
27
- begin
28
29
 
30
+ begin
29
31
  idx = next_index(idx)
30
32
 
31
33
  # if we've looped all the way around, return our safe value
@@ -46,7 +48,6 @@ module Makara
46
48
  idx
47
49
  end
48
50
 
49
-
50
51
  # return the connection if it's not blacklisted
51
52
  # otherwise return nil
52
53
  # optionally, store the position and context we're returning
@@ -62,11 +63,14 @@ module Makara
62
63
  con
63
64
  end
64
65
 
65
-
66
66
  # stub in test mode to ensure consistency
67
67
  def should_shuffle?
68
68
  true
69
69
  end
70
+
71
+ def single_one?
72
+ false
73
+ end
70
74
  end
71
75
  end
72
76
  end
@@ -0,0 +1,45 @@
1
+ require 'makara/errors/invalid_shard'
2
+
3
+ module Makara
4
+ module Strategies
5
+ class ShardAware < ::Makara::Strategies::Abstract
6
+ def init
7
+ @shards = {}
8
+ @default_shard = pool.default_shard
9
+ end
10
+
11
+ def connection_added(wrapper)
12
+ id = wrapper._makara_shard_id
13
+ shard_strategy(id).connection_added(wrapper)
14
+ end
15
+
16
+ def shard_strategy(shard_id)
17
+ id = shard_id
18
+ shard_strategy = @shards[id]
19
+ unless shard_strategy
20
+ shard_strategy = pool.shard_strategy_class.new(pool)
21
+ @shards[id] = shard_strategy
22
+ end
23
+ shard_strategy
24
+ end
25
+
26
+ def current
27
+ id = shard_id
28
+ raise Makara::Errors::InvalidShard.new(pool.role, id) unless id && @shards[id]
29
+
30
+ @shards[id].current
31
+ end
32
+
33
+ def next
34
+ id = shard_id
35
+ raise Makara::Errors::InvalidShard.new(pool.role, id) unless id && @shards[id]
36
+
37
+ @shards[id].next
38
+ end
39
+
40
+ def shard_id
41
+ Thread.current['makara_shard_id'] || pool.default_shard
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,15 +1,13 @@
1
1
  module Makara
2
2
  module VERSION
3
-
4
3
  MAJOR = 0
5
- MINOR = 3
6
- PATCH = 9
4
+ MINOR = 5
5
+ PATCH = 1
7
6
  PRE = nil
8
7
 
9
8
  def self.to_s
10
9
  [MAJOR, MINOR, PATCH, PRE].compact.join('.')
11
10
  end
12
-
13
11
  end unless defined?(::Makara::VERSION)
14
12
  ::Makara::VERSION
15
13
  end
data/makara.gemspec CHANGED
@@ -1,4 +1,3 @@
1
- # -*- encoding: utf-8 -*-
2
1
  require File.expand_path('../lib/makara/version', __FILE__)
3
2
 
4
3
  Gem::Specification.new do |gem|
@@ -6,7 +5,11 @@ Gem::Specification.new do |gem|
6
5
  gem.email = ["mike@mikeonrails.com"]
7
6
  gem.description = %q{Read-write split your DB yo}
8
7
  gem.summary = %q{Read-write split your DB yo}
9
- gem.homepage = ""
8
+ gem.homepage = "https://github.com/instacart/makara"
9
+ gem.licenses = ['MIT']
10
+ gem.metadata = {
11
+ "source_code_uri" => 'https://github.com/instacart/makara'
12
+ }
10
13
 
11
14
  gem.files = `git ls-files`.split($\)
12
15
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -15,5 +18,25 @@ Gem::Specification.new do |gem|
15
18
  gem.require_paths = ["lib"]
16
19
  gem.version = Makara::VERSION
17
20
 
18
- gem.add_dependency 'activerecord', '>= 3.0.0'
21
+ gem.required_ruby_version = ">= 2.5.0"
22
+
23
+ gem.add_dependency "activerecord", ">= 5.2.0"
24
+
25
+ gem.add_development_dependency "rack"
26
+ gem.add_development_dependency "rake", "~> 13.0"
27
+ gem.add_development_dependency "rspec", "~> 3.9"
28
+ gem.add_development_dependency "timecop"
29
+ gem.add_development_dependency "rubocop", "~> 1.9.1"
30
+
31
+ if RUBY_ENGINE == "jruby"
32
+ gem.add_development_dependency "activerecord-jdbcmysql-adapter"
33
+ gem.add_development_dependency "activerecord-jdbcpostgresql-adapter"
34
+ gem.add_development_dependency "ruby-debug"
35
+ else
36
+ gem.add_development_dependency "activerecord-postgis-adapter"
37
+ gem.add_development_dependency "pry-byebug"
38
+ gem.add_development_dependency "mysql2"
39
+ gem.add_development_dependency "pg"
40
+ gem.add_development_dependency "rgeo"
41
+ end
19
42
  end