makara 0.3.8 → 0.5.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.
Files changed (64) hide show
  1. checksums.yaml +5 -5
  2. data/.github/workflows/gem-publish-public.yml +36 -0
  3. data/.travis.yml +71 -9
  4. data/CHANGELOG.md +84 -25
  5. data/Gemfile +4 -3
  6. data/README.md +37 -34
  7. data/gemfiles/ar-head.gemfile +9 -0
  8. data/gemfiles/ar30.gemfile +7 -1
  9. data/gemfiles/ar31.gemfile +8 -1
  10. data/gemfiles/ar32.gemfile +8 -1
  11. data/gemfiles/ar40.gemfile +10 -1
  12. data/gemfiles/ar41.gemfile +10 -1
  13. data/gemfiles/ar42.gemfile +10 -1
  14. data/gemfiles/ar50.gemfile +11 -2
  15. data/gemfiles/ar51.gemfile +11 -2
  16. data/gemfiles/ar52.gemfile +24 -0
  17. data/gemfiles/ar60.gemfile +24 -0
  18. data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +109 -3
  19. data/lib/active_record/connection_adapters/makara_postgis_adapter.rb +41 -0
  20. data/lib/makara.rb +15 -4
  21. data/lib/makara/cache.rb +4 -40
  22. data/lib/makara/config_parser.rb +14 -3
  23. data/lib/makara/connection_wrapper.rb +26 -2
  24. data/lib/makara/context.rb +108 -38
  25. data/lib/makara/cookie.rb +52 -0
  26. data/lib/makara/error_handler.rb +2 -2
  27. data/lib/makara/errors/blacklisted_while_in_transaction.rb +14 -0
  28. data/lib/makara/errors/invalid_shard.rb +16 -0
  29. data/lib/makara/logging/logger.rb +1 -1
  30. data/lib/makara/middleware.rb +12 -75
  31. data/lib/makara/pool.rb +53 -40
  32. data/lib/makara/proxy.rb +52 -30
  33. data/lib/makara/railtie.rb +0 -6
  34. data/lib/makara/strategies/round_robin.rb +6 -0
  35. data/lib/makara/strategies/shard_aware.rb +47 -0
  36. data/lib/makara/version.rb +2 -2
  37. data/makara.gemspec +5 -1
  38. data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +10 -5
  39. data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +17 -2
  40. data/spec/active_record/connection_adapters/makara_postgis_adapter_spec.rb +155 -0
  41. data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +76 -3
  42. data/spec/cache_spec.rb +2 -52
  43. data/spec/config_parser_spec.rb +27 -13
  44. data/spec/connection_wrapper_spec.rb +5 -2
  45. data/spec/context_spec.rb +163 -100
  46. data/spec/cookie_spec.rb +72 -0
  47. data/spec/middleware_spec.rb +26 -55
  48. data/spec/pool_spec.rb +24 -0
  49. data/spec/proxy_spec.rb +51 -36
  50. data/spec/spec_helper.rb +5 -9
  51. data/spec/strategies/shard_aware_spec.rb +219 -0
  52. data/spec/support/helpers.rb +6 -2
  53. data/spec/support/mock_objects.rb +5 -1
  54. data/spec/support/mysql2_database.yml +1 -0
  55. data/spec/support/mysql2_database_with_custom_errors.yml +5 -0
  56. data/spec/support/postgis_database.yml +15 -0
  57. data/spec/support/postgis_schema.rb +11 -0
  58. data/spec/support/postgresql_database.yml +2 -0
  59. data/spec/support/proxy_extensions.rb +1 -1
  60. data/spec/support/schema.rb +5 -5
  61. data/spec/support/user.rb +5 -0
  62. metadata +28 -9
  63. data/lib/makara/cache/memory_store.rb +0 -28
  64. data/lib/makara/cache/noop_store.rb +0 -15
@@ -1,7 +1,7 @@
1
1
  require 'active_support/core_ext/hash/keys'
2
+ require 'makara/strategies/shard_aware'
2
3
 
3
4
  # 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
5
  # Provides convenience methods for accessing underlying connections
6
6
 
7
7
  module Makara
@@ -14,15 +14,22 @@ module Makara
14
14
  attr_reader :role
15
15
  attr_reader :connections
16
16
  attr_reader :strategy
17
+ attr_reader :shard_strategy_class
18
+ attr_reader :default_shard
17
19
 
18
20
  def initialize(role, proxy)
19
21
  @role = role
20
22
  @proxy = proxy
21
- @context = Makara::Context.get_current
22
23
  @connections = []
23
24
  @blacklist_errors = []
24
25
  @disabled = false
25
- @strategy = proxy.strategy_for(role)
26
+ if proxy.shard_aware_for(role)
27
+ @strategy = Makara::Strategies::ShardAware.new(self)
28
+ @shard_strategy_class = proxy.strategy_class_for(proxy.strategy_name_for(role))
29
+ @default_shard = proxy.default_shard_for(role)
30
+ else
31
+ @strategy = proxy.strategy_for(role)
32
+ end
26
33
  end
27
34
 
28
35
 
@@ -64,17 +71,14 @@ module Makara
64
71
  @connections.each do |con|
65
72
  next if con._makara_blacklisted?
66
73
  begin
67
- if block
68
- value = @proxy.error_handler.handle(con) do
74
+ ret = @proxy.error_handler.handle(con) do
75
+ if block
69
76
  yield con
77
+ else
78
+ con.send(method, *args)
70
79
  end
71
80
  end
72
81
 
73
- if method
74
- ret = con.send(method, *args)
75
- else
76
- ret = value
77
- end
78
82
  one_worked = true
79
83
  rescue Makara::Errors::BlacklistConnection => e
80
84
  errors.insert(0, e)
@@ -96,33 +100,47 @@ module Makara
96
100
  # Provide a connection that is not blacklisted and connected. Handle any errors
97
101
  # that may occur within the block.
98
102
  def provide
99
- provided_connection = self.next
103
+ attempt = 0
104
+ begin
105
+ provided_connection = self.next
100
106
 
101
- # nil implies that it's blacklisted
102
- if provided_connection
107
+ # nil implies that it's blacklisted
108
+ if provided_connection
103
109
 
104
- value = @proxy.error_handler.handle(provided_connection) do
105
- yield provided_connection
106
- end
110
+ value = @proxy.error_handler.handle(provided_connection) do
111
+ yield provided_connection
112
+ end
107
113
 
108
- @blacklist_errors = []
114
+ @blacklist_errors = []
109
115
 
110
- value
116
+ value
111
117
 
112
- # if we've made any connections within this pool, we should report the blackout.
113
- elsif connection_made?
114
- err = Makara::Errors::AllConnectionsBlacklisted.new(self, @blacklist_errors)
115
- @blacklist_errors = []
116
- raise err
117
- else
118
- raise Makara::Errors::NoConnectionsAvailable.new(@role) unless @disabled
119
- end
118
+ # if we've made any connections within this pool, we should report the blackout.
119
+ elsif connection_made?
120
+ err = Makara::Errors::AllConnectionsBlacklisted.new(self, @blacklist_errors)
121
+ @blacklist_errors = []
122
+ raise err
123
+ else
124
+ raise Makara::Errors::NoConnectionsAvailable.new(@role) unless @disabled
125
+ end
120
126
 
121
- # when a connection causes a blacklist error within the provided block, we blacklist it then retry
122
- rescue Makara::Errors::BlacklistConnection => e
123
- @blacklist_errors.insert(0, e)
124
- provided_connection._makara_blacklist!
125
- retry
127
+ # when a connection causes a blacklist error within the provided block, we blacklist it then retry
128
+ rescue Makara::Errors::BlacklistConnection => e
129
+ @blacklist_errors.insert(0, e)
130
+ in_transaction = self.role == "master" && provided_connection._makara_in_transaction?
131
+ provided_connection._makara_blacklist!
132
+ raise Makara::Errors::BlacklistedWhileInTransaction.new(@role) if in_transaction
133
+ attempt += 1
134
+ if attempt < @connections.length
135
+ retry
136
+ elsif connection_made?
137
+ err = Makara::Errors::AllConnectionsBlacklisted.new(self, @blacklist_errors)
138
+ @blacklist_errors = []
139
+ raise err
140
+ else
141
+ raise Makara::Errors::NoConnectionsAvailable.new(@role) unless @disabled
142
+ end
143
+ end
126
144
  end
127
145
 
128
146
 
@@ -140,16 +158,11 @@ module Makara
140
158
  # to be sticky, provide back the current connection assuming it is
141
159
  # not blacklisted.
142
160
  def next
143
- if @proxy.sticky && Makara::Context.get_current == @context
144
- con = @strategy.current
145
- return con if con
146
- end
147
-
148
- con = @strategy.next
149
- if con
150
- @context = Makara::Context.get_current
161
+ if @proxy.sticky && (curr = @strategy.current)
162
+ curr
163
+ else
164
+ @strategy.next
151
165
  end
152
- con
153
166
  end
154
167
  end
155
168
  end
@@ -14,8 +14,9 @@ module Makara
14
14
 
15
15
  METHOD_MISSING_SKIP = [ :byebug, :puts ]
16
16
 
17
- class_attribute :hijack_methods
17
+ class_attribute :hijack_methods, :control_methods
18
18
  self.hijack_methods = []
19
+ self.control_methods = []
19
20
 
20
21
  class << self
21
22
  def hijack_method(*method_names)
@@ -38,12 +39,24 @@ module Makara
38
39
  end
39
40
  end
40
41
  end
42
+
43
+ def control_method(*method_names)
44
+ self.control_methods = self.control_methods || []
45
+ self.control_methods |= method_names
46
+
47
+ method_names.each do |method_name|
48
+ define_method method_name do |*args, &block|
49
+ control&.send(method_name, *args, &block)
50
+ end
51
+ end
52
+ end
41
53
  end
42
54
 
43
55
 
44
56
  attr_reader :error_handler
45
57
  attr_reader :sticky
46
58
  attr_reader :config_parser
59
+ attr_reader :control
47
60
 
48
61
  def initialize(config)
49
62
  @config = config.symbolize_keys
@@ -59,22 +72,21 @@ module Makara
59
72
  end
60
73
 
61
74
  def without_sticking
62
- before_context = @master_context
63
- @master_context = nil
64
75
  @skip_sticking = true
65
76
  yield
66
77
  ensure
67
78
  @skip_sticking = false
68
- @master_context ||= before_context
69
79
  end
70
80
 
71
81
  def hijacked?
72
82
  @hijacked
73
83
  end
74
84
 
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
85
+ # If persist is true, we stick the proxy to master for subsequent requests
86
+ # up to master_ttl duration. Otherwise we just stick it for the current request
87
+ def stick_to_master!(persist = true)
88
+ stickiness_duration = persist ? @ttl : 0
89
+ Makara::Context.stick(@id, stickiness_duration)
78
90
  end
79
91
 
80
92
  def strategy_for(role)
@@ -85,6 +97,14 @@ module Makara
85
97
  @config_parser.makara_config["#{role}_strategy".to_sym]
86
98
  end
87
99
 
100
+ def shard_aware_for(role)
101
+ @config_parser.makara_config["#{role}_shard_aware".to_sym]
102
+ end
103
+
104
+ def default_shard_for(role)
105
+ @config_parser.makara_config["#{role}_default_shard".to_sym]
106
+ end
107
+
88
108
  def strategy_class_for(strategy_name)
89
109
  case strategy_name
90
110
  when 'round_robin', 'roundrobin', nil, ''
@@ -149,8 +169,14 @@ module Makara
149
169
  end
150
170
 
151
171
  def any_connection
152
- @master_pool.provide do |con|
153
- yield con
172
+ if @master_pool.disabled
173
+ @slave_pool.provide do |con|
174
+ yield con
175
+ end
176
+ else
177
+ @master_pool.provide do |con|
178
+ yield con
179
+ end
154
180
  end
155
181
  rescue ::Makara::Errors::AllConnectionsBlacklisted, ::Makara::Errors::NoConnectionsAvailable
156
182
  begin
@@ -182,7 +208,6 @@ module Makara
182
208
 
183
209
  # for testing purposes
184
210
  pool = _appropriate_pool(method_name, args)
185
-
186
211
  yield pool
187
212
 
188
213
  rescue ::Makara::Errors::AllConnectionsBlacklisted, ::Makara::Errors::NoConnectionsAvailable => e
@@ -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)
@@ -248,28 +269,28 @@ module Makara
248
269
  end
249
270
 
250
271
 
251
- def previously_stuck_to_master?
252
- @sticky && Makara::Context.previously_stuck?(@id)
272
+ def stuck_to_master?
273
+ sticky? && Makara::Context.stuck?(@id)
253
274
  end
254
275
 
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
-
276
+ def stick_to_master(method_name, args)
260
277
  # check to see if we're configured, bypassed, or some custom implementation has input
261
278
  return unless should_stick?(method_name, args)
262
279
 
263
280
  # do the sticking
264
- stick_to_master!(write_to_cache)
281
+ stick_to_master!
265
282
  end
266
283
 
267
-
268
- # if we are configured to be sticky and we aren't bypassing stickiness
284
+ # For the generic proxy implementation, we stick if we are sticky,
285
+ # method and args don't matter
269
286
  def should_stick?(method_name, args)
270
- @sticky && !@skip_sticking
287
+ sticky?
271
288
  end
272
289
 
290
+ # If we are configured to be sticky and we aren't bypassing stickiness,
291
+ def sticky?
292
+ @sticky && !@skip_sticking
293
+ end
273
294
 
274
295
  # use the config parser to generate a master and slave pool
275
296
  def instantiate_connections
@@ -292,7 +313,8 @@ module Makara
292
313
  yield
293
314
  rescue ::Makara::Errors::NoConnectionsAvailable => e
294
315
  if e.role == 'master'
295
- Kernel.raise ::Makara::Errors::NoConnectionsAvailable.new('master and slave')
316
+ # this means slave connections are good.
317
+ return
296
318
  end
297
319
  @slave_pool.disabled = true
298
320
  yield
@@ -5,11 +5,5 @@ module Makara
5
5
  app.middleware.use Makara::Middleware
6
6
  end
7
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
8
  end
15
9
  end
@@ -23,6 +23,8 @@ 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
29
  begin
28
30
 
@@ -67,6 +69,10 @@ module Makara
67
69
  def should_shuffle?
68
70
  true
69
71
  end
72
+
73
+ def single_one?
74
+ false
75
+ end
70
76
  end
71
77
  end
72
78
  end
@@ -0,0 +1,47 @@
1
+ require 'makara/errors/invalid_shard'
2
+
3
+ module Makara
4
+ module Strategies
5
+ class ShardAware < ::Makara::Strategies::Abstract
6
+
7
+ def init
8
+ @shards = {}
9
+ @default_shard = pool.default_shard
10
+ end
11
+
12
+ def connection_added(wrapper)
13
+ id = wrapper._makara_shard_id
14
+ shard_strategy(id).connection_added(wrapper)
15
+ end
16
+
17
+ def shard_strategy(shard_id)
18
+ id = shard_id
19
+ shard_strategy = @shards[id]
20
+ unless shard_strategy
21
+ shard_strategy = pool.shard_strategy_class.new(pool)
22
+ @shards[id] = shard_strategy
23
+ end
24
+ shard_strategy
25
+ end
26
+
27
+ def current
28
+ id = shard_id
29
+ raise Makara::Errors::InvalidShard.new(pool.role, id) unless id && @shards[id]
30
+
31
+ @shards[id].current
32
+ end
33
+
34
+ def next
35
+ id = shard_id
36
+ raise Makara::Errors::InvalidShard.new(pool.role, id) unless id && @shards[id]
37
+
38
+ @shards[id].next
39
+ end
40
+
41
+ def shard_id
42
+ Thread.current['makara_shard_id'] || pool.default_shard
43
+ end
44
+
45
+ end
46
+ end
47
+ end
@@ -2,8 +2,8 @@ module Makara
2
2
  module VERSION
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 3
6
- PATCH = 8
5
+ MINOR = 5
6
+ PATCH = 0
7
7
  PRE = nil
8
8
 
9
9
  def self.to_s
@@ -6,7 +6,11 @@ Gem::Specification.new do |gem|
6
6
  gem.email = ["mike@mikeonrails.com"]
7
7
  gem.description = %q{Read-write split your DB yo}
8
8
  gem.summary = %q{Read-write split your DB yo}
9
- gem.homepage = ""
9
+ gem.homepage = "https://github.com/taskrabbit/makara"
10
+ gem.licenses = ['MIT']
11
+ gem.metadata = {
12
+ "source_code_uri" => 'https://github.com/taskrabbit/makara'
13
+ }
10
14
 
11
15
  gem.files = `git ls-files`.split($\)
12
16
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -27,7 +27,16 @@ describe ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter do
27
27
  ' select * from users for update' => true,
28
28
  'select * from users lock in share mode' => true,
29
29
  'select * from users where name = "for update"' => false,
30
- 'select * from users where name = "lock in share mode"' => false
30
+ 'select * from users where name = "lock in share mode"' => false,
31
+ 'select nextval(\'users_id_seq\')' => true,
32
+ 'select currval(\'users_id_seq\')' => true,
33
+ 'select lastval()' => true,
34
+ 'with fence as (select * from users) select * from fence' => false,
35
+ 'with fence as (select * from felines) insert to cats' => true,
36
+ 'select get_lock(\'foo\', 0)' => true,
37
+ 'select release_lock(\'foo\')' => true,
38
+ 'select pg_advisory_lock(12345)' => true,
39
+ 'select pg_advisory_unlock(12345)' => true
31
40
  }.each do |sql, should_go_to_master|
32
41
 
33
42
  it "determines that \"#{sql}\" #{should_go_to_master ? 'requires' : 'does not require'} master" do
@@ -65,10 +74,6 @@ describe ActiveRecord::ConnectionAdapters::MakaraAbstractAdapter do
65
74
  end
66
75
 
67
76
  proxy.execute(sql)
68
-
69
- if should_send_to_all_connections
70
- expect(proxy.master_context).to be_nil
71
- end
72
77
  end
73
78
 
74
79
  end