makara 0.4.1 → 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 +36 -10
- data/CHANGELOG.md +61 -48
- data/Gemfile +1 -1
- data/README.md +6 -8
- data/gemfiles/ar-head.gemfile +1 -1
- data/gemfiles/ar52.gemfile +24 -0
- data/gemfiles/ar60.gemfile +24 -0
- data/lib/active_record/connection_adapters/makara_abstract_adapter.rb +105 -0
- data/lib/makara.rb +7 -4
- data/lib/makara/config_parser.rb +2 -1
- data/lib/makara/connection_wrapper.rb +24 -0
- data/lib/makara/errors/blacklisted_while_in_transaction.rb +14 -0
- data/lib/makara/errors/invalid_shard.rb +16 -0
- data/lib/makara/pool.rb +47 -24
- data/lib/makara/proxy.rb +32 -4
- 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_postgresql_adapter_spec.rb +57 -0
- data/spec/cookie_spec.rb +3 -3
- data/spec/middleware_spec.rb +1 -1
- data/spec/pool_spec.rb +24 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/strategies/shard_aware_spec.rb +219 -0
- data/spec/support/mock_objects.rb +4 -0
- metadata +16 -7
data/lib/makara.rb
CHANGED
@@ -14,10 +14,12 @@ module Makara
|
|
14
14
|
autoload :Proxy, 'makara/proxy'
|
15
15
|
|
16
16
|
module Errors
|
17
|
-
autoload :MakaraError,
|
18
|
-
autoload :AllConnectionsBlacklisted,
|
19
|
-
autoload :BlacklistConnection,
|
20
|
-
autoload :NoConnectionsAvailable,
|
17
|
+
autoload :MakaraError, 'makara/errors/makara_error'
|
18
|
+
autoload :AllConnectionsBlacklisted, 'makara/errors/all_connections_blacklisted'
|
19
|
+
autoload :BlacklistConnection, 'makara/errors/blacklist_connection'
|
20
|
+
autoload :NoConnectionsAvailable, 'makara/errors/no_connections_available'
|
21
|
+
autoload :BlacklistedWhileInTransaction, 'makara/errors/blacklisted_while_in_transaction'
|
22
|
+
autoload :InvalidShard, 'makara/errors/invalid_shard'
|
21
23
|
end
|
22
24
|
|
23
25
|
module Logging
|
@@ -29,6 +31,7 @@ module Makara
|
|
29
31
|
autoload :Abstract, 'makara/strategies/abstract'
|
30
32
|
autoload :RoundRobin, 'makara/strategies/round_robin'
|
31
33
|
autoload :PriorityFailover, 'makara/strategies/priority_failover'
|
34
|
+
autoload :ShardAware, 'makara/strategies/shard_aware'
|
32
35
|
end
|
33
36
|
|
34
37
|
end
|
data/lib/makara/config_parser.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'digest/md5'
|
2
2
|
require 'active_support/core_ext/hash/keys'
|
3
3
|
require 'active_support/core_ext/hash/except'
|
4
|
+
require 'cgi'
|
4
5
|
|
5
6
|
# Convenience methods to grab subconfigs out of the primary configuration.
|
6
7
|
# Provides a way to generate a consistent ID based on a unique config.
|
@@ -63,7 +64,7 @@ module Makara
|
|
63
64
|
# Converts the given URL to a full connection hash.
|
64
65
|
def to_hash
|
65
66
|
config = raw_config.reject { |_,value| value.blank? }
|
66
|
-
config.map { |key,value| config[key] =
|
67
|
+
config.map { |key,value| config[key] = CGI.unescape(value) if value.is_a? String }
|
67
68
|
config
|
68
69
|
end
|
69
70
|
|
@@ -35,11 +35,19 @@ module Makara
|
|
35
35
|
@config[:name]
|
36
36
|
end
|
37
37
|
|
38
|
+
def _makara_shard_id
|
39
|
+
@config[:shard_id]
|
40
|
+
end
|
41
|
+
|
38
42
|
# has this node been blacklisted?
|
39
43
|
def _makara_blacklisted?
|
40
44
|
@blacklisted_until.present? && @blacklisted_until.to_i > Time.now.to_i
|
41
45
|
end
|
42
46
|
|
47
|
+
def _makara_in_transaction?
|
48
|
+
@connection && @connection.open_transactions > 0 ? true : false
|
49
|
+
end
|
50
|
+
|
43
51
|
# blacklist this node for @config[:blacklist_duration] seconds
|
44
52
|
def _makara_blacklist!
|
45
53
|
@connection.disconnect! if @connection
|
@@ -158,6 +166,22 @@ module Makara
|
|
158
166
|
}
|
159
167
|
end
|
160
168
|
|
169
|
+
# Control methods must always be passed to the
|
170
|
+
# Makara::Proxy control object for handling (typically
|
171
|
+
# related to ActiveRecord connection pool management)
|
172
|
+
@proxy.class.control_methods.each do |meth|
|
173
|
+
extension << %Q{
|
174
|
+
def #{meth}(*args, &block)
|
175
|
+
proxy = _makara
|
176
|
+
if proxy
|
177
|
+
proxy.control.#{meth}(*args=args, block)
|
178
|
+
else
|
179
|
+
super # Only if we are not wrapped any longer
|
180
|
+
end
|
181
|
+
end
|
182
|
+
}
|
183
|
+
end
|
184
|
+
|
161
185
|
# extend the instance
|
162
186
|
con.instance_eval(extension)
|
163
187
|
# set the makara context
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Makara
|
2
|
+
module Errors
|
3
|
+
class InvalidShard < MakaraError
|
4
|
+
|
5
|
+
attr_reader :role
|
6
|
+
attr_reader :shard_id
|
7
|
+
|
8
|
+
def initialize(role, shard_id)
|
9
|
+
@role = role
|
10
|
+
@shard_id = shard_id
|
11
|
+
super "[Makara] Invalid shard_id #{shard_id} for the #{role} pool"
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/makara/pool.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
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
5
|
# Provides convenience methods for accessing underlying connections
|
@@ -13,6 +14,8 @@ module Makara
|
|
13
14
|
attr_reader :role
|
14
15
|
attr_reader :connections
|
15
16
|
attr_reader :strategy
|
17
|
+
attr_reader :shard_strategy_class
|
18
|
+
attr_reader :default_shard
|
16
19
|
|
17
20
|
def initialize(role, proxy)
|
18
21
|
@role = role
|
@@ -20,7 +23,13 @@ module Makara
|
|
20
23
|
@connections = []
|
21
24
|
@blacklist_errors = []
|
22
25
|
@disabled = false
|
23
|
-
|
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
|
24
33
|
end
|
25
34
|
|
26
35
|
|
@@ -91,33 +100,47 @@ module Makara
|
|
91
100
|
# Provide a connection that is not blacklisted and connected. Handle any errors
|
92
101
|
# that may occur within the block.
|
93
102
|
def provide
|
94
|
-
|
103
|
+
attempt = 0
|
104
|
+
begin
|
105
|
+
provided_connection = self.next
|
95
106
|
|
96
|
-
|
97
|
-
|
107
|
+
# nil implies that it's blacklisted
|
108
|
+
if provided_connection
|
98
109
|
|
99
|
-
|
100
|
-
|
101
|
-
|
110
|
+
value = @proxy.error_handler.handle(provided_connection) do
|
111
|
+
yield provided_connection
|
112
|
+
end
|
102
113
|
|
103
|
-
|
114
|
+
@blacklist_errors = []
|
104
115
|
|
105
|
-
|
116
|
+
value
|
106
117
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
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
|
115
126
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
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
|
121
144
|
end
|
122
145
|
|
123
146
|
|
@@ -135,8 +158,8 @@ module Makara
|
|
135
158
|
# to be sticky, provide back the current connection assuming it is
|
136
159
|
# not blacklisted.
|
137
160
|
def next
|
138
|
-
if @proxy.sticky && @strategy.current
|
139
|
-
|
161
|
+
if @proxy.sticky && (curr = @strategy.current)
|
162
|
+
curr
|
140
163
|
else
|
141
164
|
@strategy.next
|
142
165
|
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
|
@@ -84,6 +97,14 @@ module Makara
|
|
84
97
|
@config_parser.makara_config["#{role}_strategy".to_sym]
|
85
98
|
end
|
86
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
|
+
|
87
108
|
def strategy_class_for(strategy_name)
|
88
109
|
case strategy_name
|
89
110
|
when 'round_robin', 'roundrobin', nil, ''
|
@@ -148,8 +169,14 @@ module Makara
|
|
148
169
|
end
|
149
170
|
|
150
171
|
def any_connection
|
151
|
-
@master_pool.
|
152
|
-
|
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
|
153
180
|
end
|
154
181
|
rescue ::Makara::Errors::AllConnectionsBlacklisted, ::Makara::Errors::NoConnectionsAvailable
|
155
182
|
begin
|
@@ -286,7 +313,8 @@ module Makara
|
|
286
313
|
yield
|
287
314
|
rescue ::Makara::Errors::NoConnectionsAvailable => e
|
288
315
|
if e.role == 'master'
|
289
|
-
|
316
|
+
# this means slave connections are good.
|
317
|
+
return
|
290
318
|
end
|
291
319
|
@slave_pool.disabled = true
|
292
320
|
yield
|
@@ -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) }
|
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
require 'active_record/connection_adapters/postgresql_adapter'
|
3
|
+
require 'active_record/errors'
|
3
4
|
|
4
5
|
describe 'MakaraPostgreSQLAdapter' do
|
5
6
|
|
@@ -191,4 +192,60 @@ describe 'MakaraPostgreSQLAdapter' do
|
|
191
192
|
it_behaves_like 'a transaction supporter'
|
192
193
|
end
|
193
194
|
end
|
195
|
+
|
196
|
+
context 'with two activerecord connection pools' do
|
197
|
+
|
198
|
+
before :each do
|
199
|
+
class Model1 < ActiveRecord::Base
|
200
|
+
end
|
201
|
+
|
202
|
+
class Model2 < ActiveRecord::Base
|
203
|
+
end
|
204
|
+
|
205
|
+
Model1.establish_connection(config)
|
206
|
+
Model2.establish_connection(config)
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'should not leak raw connection into activerecord pool' do
|
211
|
+
# checkout a connection from Model1 pool and remove from the pool
|
212
|
+
conn = Model1.connection_pool.checkout
|
213
|
+
Model1.connection_pool.remove(conn)
|
214
|
+
|
215
|
+
# assign the connection to Model2 pool
|
216
|
+
conn.pool=Model2.connection_pool
|
217
|
+
|
218
|
+
# now close the connection to return it back to the pool
|
219
|
+
conn.close
|
220
|
+
|
221
|
+
# checkout the connection and make sure it is still a makara proxy
|
222
|
+
expect(Model2.connection).to eq(conn)
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'should be able to steal the connection from a different thread' do
|
226
|
+
conn = Model1.connection_pool.checkout
|
227
|
+
conn.steal!
|
228
|
+
expect(conn.owner).to eq(Thread.current)
|
229
|
+
# steal! is not thread safe. it should be done while holding connection pool's mutex
|
230
|
+
t = Thread.new { conn.steal! }
|
231
|
+
t.join
|
232
|
+
expect(conn.owner).to eq(t)
|
233
|
+
end
|
234
|
+
|
235
|
+
it 'should not be able to expire the connection from same thread' do
|
236
|
+
conn = Model2.connection_pool.checkout
|
237
|
+
# expire is not thread safe. it should be done while holding connection pool's mutex
|
238
|
+
expect {
|
239
|
+
t = Thread.new { conn.expire }
|
240
|
+
t.join
|
241
|
+
}.to raise_error(ActiveRecord::ActiveRecordError)
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'should be able to checkin connection back into activerecord pool' do
|
245
|
+
conn = Model1.connection_pool.checkout
|
246
|
+
Model1.connection_pool.checkin(conn)
|
247
|
+
# checkout the connection again and make sure it is same connection
|
248
|
+
expect(Model1.connection).to eq(conn)
|
249
|
+
end
|
250
|
+
end
|
194
251
|
end
|
data/spec/cookie_spec.rb
CHANGED
@@ -53,20 +53,20 @@ describe Makara::Cookie do
|
|
53
53
|
Makara::Cookie.store(context_data, headers)
|
54
54
|
|
55
55
|
expect(headers['Set-Cookie']).to include("#{cookie_key}=mysql%3A#{(now + 5).to_f}%7Credis%3A#{(now + 5).to_f};")
|
56
|
-
expect(headers['Set-Cookie']).to include("path=/; max-age=10; expires=#{(Time.now + 10).
|
56
|
+
expect(headers['Set-Cookie']).to include("path=/; max-age=10; expires=#{(Time.now + 10).httpdate}; HttpOnly")
|
57
57
|
end
|
58
58
|
|
59
59
|
it 'expires the cookie if the next context is empty' do
|
60
60
|
Makara::Cookie.store({}, headers)
|
61
61
|
|
62
|
-
expect(headers['Set-Cookie']).to eq("#{cookie_key}=; path=/; max-age=0; expires=#{Time.now.
|
62
|
+
expect(headers['Set-Cookie']).to eq("#{cookie_key}=; path=/; max-age=0; expires=#{Time.now.httpdate}; HttpOnly")
|
63
63
|
end
|
64
64
|
|
65
65
|
it 'allows custom cookie options to be provided' do
|
66
66
|
Makara::Cookie.store(context_data, headers, { :secure => true })
|
67
67
|
|
68
68
|
expect(headers['Set-Cookie']).to include("#{cookie_key}=mysql%3A#{(now + 5).to_f}%7Credis%3A#{(now + 5).to_f};")
|
69
|
-
expect(headers['Set-Cookie']).to include("path=/; max-age=10; expires=#{(Time.now + 10).
|
69
|
+
expect(headers['Set-Cookie']).to include("path=/; max-age=10; expires=#{(Time.now + 10).httpdate}; secure; HttpOnly")
|
70
70
|
end
|
71
71
|
end
|
72
72
|
end
|