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.
- checksums.yaml +5 -5
- data/.github/workflows/gem-publish-public.yml +36 -0
- data/.travis.yml +71 -9
- data/CHANGELOG.md +84 -25
- data/Gemfile +4 -3
- data/README.md +37 -34
- data/gemfiles/ar-head.gemfile +9 -0
- data/gemfiles/ar30.gemfile +7 -1
- data/gemfiles/ar31.gemfile +8 -1
- data/gemfiles/ar32.gemfile +8 -1
- data/gemfiles/ar40.gemfile +10 -1
- data/gemfiles/ar41.gemfile +10 -1
- data/gemfiles/ar42.gemfile +10 -1
- data/gemfiles/ar50.gemfile +11 -2
- data/gemfiles/ar51.gemfile +11 -2
- data/gemfiles/ar52.gemfile +24 -0
- data/gemfiles/ar60.gemfile +24 -0
- data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +109 -3
- data/lib/active_record/connection_adapters/makara_postgis_adapter.rb +41 -0
- data/lib/makara.rb +15 -4
- data/lib/makara/cache.rb +4 -40
- data/lib/makara/config_parser.rb +14 -3
- data/lib/makara/connection_wrapper.rb +26 -2
- data/lib/makara/context.rb +108 -38
- data/lib/makara/cookie.rb +52 -0
- data/lib/makara/error_handler.rb +2 -2
- data/lib/makara/errors/blacklisted_while_in_transaction.rb +14 -0
- data/lib/makara/errors/invalid_shard.rb +16 -0
- data/lib/makara/logging/logger.rb +1 -1
- data/lib/makara/middleware.rb +12 -75
- data/lib/makara/pool.rb +53 -40
- data/lib/makara/proxy.rb +52 -30
- data/lib/makara/railtie.rb +0 -6
- data/lib/makara/strategies/round_robin.rb +6 -0
- data/lib/makara/strategies/shard_aware.rb +47 -0
- data/lib/makara/version.rb +2 -2
- data/makara.gemspec +5 -1
- data/spec/active_record/connection_adapters/makara_abstract_adapter_spec.rb +10 -5
- data/spec/active_record/connection_adapters/makara_mysql2_adapter_spec.rb +17 -2
- data/spec/active_record/connection_adapters/makara_postgis_adapter_spec.rb +155 -0
- data/spec/active_record/connection_adapters/makara_postgresql_adapter_spec.rb +76 -3
- data/spec/cache_spec.rb +2 -52
- data/spec/config_parser_spec.rb +27 -13
- data/spec/connection_wrapper_spec.rb +5 -2
- data/spec/context_spec.rb +163 -100
- data/spec/cookie_spec.rb +72 -0
- data/spec/middleware_spec.rb +26 -55
- data/spec/pool_spec.rb +24 -0
- data/spec/proxy_spec.rb +51 -36
- data/spec/spec_helper.rb +5 -9
- data/spec/strategies/shard_aware_spec.rb +219 -0
- data/spec/support/helpers.rb +6 -2
- data/spec/support/mock_objects.rb +5 -1
- data/spec/support/mysql2_database.yml +1 -0
- data/spec/support/mysql2_database_with_custom_errors.yml +5 -0
- data/spec/support/postgis_database.yml +15 -0
- data/spec/support/postgis_schema.rb +11 -0
- data/spec/support/postgresql_database.yml +2 -0
- data/spec/support/proxy_extensions.rb +1 -1
- data/spec/support/schema.rb +5 -5
- data/spec/support/user.rb +5 -0
- metadata +28 -9
- data/lib/makara/cache/memory_store.rb +0 -28
- data/lib/makara/cache/noop_store.rb +0 -15
data/lib/makara/pool.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
68
|
-
|
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
|
-
|
103
|
+
attempt = 0
|
104
|
+
begin
|
105
|
+
provided_connection = self.next
|
100
106
|
|
101
|
-
|
102
|
-
|
107
|
+
# nil implies that it's blacklisted
|
108
|
+
if provided_connection
|
103
109
|
|
104
|
-
|
105
|
-
|
106
|
-
|
110
|
+
value = @proxy.error_handler.handle(provided_connection) do
|
111
|
+
yield provided_connection
|
112
|
+
end
|
107
113
|
|
108
|
-
|
114
|
+
@blacklist_errors = []
|
109
115
|
|
110
|
-
|
116
|
+
value
|
111
117
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
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 &&
|
144
|
-
|
145
|
-
|
146
|
-
|
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
|
data/lib/makara/proxy.rb
CHANGED
@@ -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
|
-
|
76
|
-
|
77
|
-
|
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.
|
153
|
-
|
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
|
-
|
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
|
212
|
-
#
|
213
|
-
|
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
|
252
|
-
|
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!
|
281
|
+
stick_to_master!
|
265
282
|
end
|
266
283
|
|
267
|
-
|
268
|
-
#
|
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
|
-
|
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
|
-
|
316
|
+
# this means slave connections are good.
|
317
|
+
return
|
296
318
|
end
|
297
319
|
@slave_pool.disabled = true
|
298
320
|
yield
|
data/lib/makara/railtie.rb
CHANGED
@@ -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
|
data/lib/makara/version.rb
CHANGED
data/makara.gemspec
CHANGED
@@ -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
|