makara 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -14,10 +14,12 @@ module Makara
14
14
  autoload :Proxy, 'makara/proxy'
15
15
 
16
16
  module Errors
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'
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
@@ -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] = URI.unescape(value) if value.is_a? String }
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,14 @@
1
+ module Makara
2
+ module Errors
3
+ class BlacklistedWhileInTransaction < MakaraError
4
+
5
+ attr_reader :role
6
+
7
+ def initialize(role)
8
+ @role = role
9
+ super "[Makara] Blacklisted while in transaction in the #{role} pool"
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -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
@@ -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
- @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
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
- provided_connection = self.next
103
+ attempt = 0
104
+ begin
105
+ provided_connection = self.next
95
106
 
96
- # nil implies that it's blacklisted
97
- if provided_connection
107
+ # nil implies that it's blacklisted
108
+ if provided_connection
98
109
 
99
- value = @proxy.error_handler.handle(provided_connection) do
100
- yield provided_connection
101
- end
110
+ value = @proxy.error_handler.handle(provided_connection) do
111
+ yield provided_connection
112
+ end
102
113
 
103
- @blacklist_errors = []
114
+ @blacklist_errors = []
104
115
 
105
- value
116
+ value
106
117
 
107
- # if we've made any connections within this pool, we should report the blackout.
108
- elsif connection_made?
109
- err = Makara::Errors::AllConnectionsBlacklisted.new(self, @blacklist_errors)
110
- @blacklist_errors = []
111
- raise err
112
- else
113
- raise Makara::Errors::NoConnectionsAvailable.new(@role) unless @disabled
114
- 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
115
126
 
116
- # when a connection causes a blacklist error within the provided block, we blacklist it then retry
117
- rescue Makara::Errors::BlacklistConnection => e
118
- @blacklist_errors.insert(0, e)
119
- provided_connection._makara_blacklist!
120
- 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
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
- @strategy.current
161
+ if @proxy.sticky && (curr = @strategy.current)
162
+ curr
140
163
  else
141
164
  @strategy.next
142
165
  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
@@ -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.provide do |con|
152
- 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
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
- Kernel.raise ::Makara::Errors::NoConnectionsAvailable.new('master and slave')
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
@@ -2,8 +2,8 @@ module Makara
2
2
  module VERSION
3
3
 
4
4
  MAJOR = 0
5
- MINOR = 4
6
- PATCH = 1
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) }
@@ -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
@@ -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).gmtime.rfc2822}; HttpOnly")
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.gmtime.rfc2822}; HttpOnly")
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).gmtime.rfc2822}; secure; HttpOnly")
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