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.
@@ -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