master_slave_adapter 0.2.0 → 1.0.0.beta1
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.
- data/.travis.yml +1 -0
- data/CHANGELOG.md +11 -0
- data/TODO.txt +14 -4
- data/lib/active_record/connection_adapters/master_slave_adapter/circuit_breaker.rb +61 -0
- data/lib/active_record/connection_adapters/master_slave_adapter/clock.rb +42 -0
- data/lib/active_record/connection_adapters/master_slave_adapter/version.rb +7 -0
- data/lib/active_record/connection_adapters/master_slave_adapter.rb +501 -1
- data/lib/active_record/connection_adapters/mysql_master_slave_adapter.rb +72 -1
- data/lib/master_slave_adapter.rb +1 -624
- data/master_slave_adapter.gemspec +8 -6
- data/spec/circuit_breaker_spec.rb +52 -0
- data/spec/master_slave_adapter_spec.rb +48 -293
- data/spec/mysql_master_slave_adapter_spec.rb +320 -0
- metadata +16 -8
- data/VERSION +0 -1
data/.travis.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,14 @@
|
|
1
|
+
# 1.0.0 (not released yet)
|
2
|
+
|
3
|
+
* Add support for unavailable master connection
|
4
|
+
* Fallback to slave connection if possible
|
5
|
+
* Restrict the public interface. Removed the following methods:
|
6
|
+
* all class methods from ActiveRecord::ConnectionAdapters::MasterSlaveAdapter
|
7
|
+
* #current_connection=
|
8
|
+
* #current_clock=
|
9
|
+
* #slave_consistent?
|
10
|
+
* Fix 1.8.7 compliance
|
11
|
+
|
1
12
|
# 0.2.0 (April 2, 2012)
|
2
13
|
|
3
14
|
* Add support for ActiveRecord's query cache
|
data/TODO.txt
CHANGED
@@ -1,8 +1,18 @@
|
|
1
1
|
Read only mode
|
2
2
|
--------------
|
3
3
|
|
4
|
-
-
|
4
|
+
- Write tests
|
5
|
+
- Clock.parse
|
6
|
+
- connection_error (integration test)
|
7
|
+
- integration tests
|
8
|
+
- with_consistency accepts string clock
|
9
|
+
- raise MasterUnavailable in all cases
|
10
|
+
- connection stack usage
|
5
11
|
|
6
|
-
-
|
7
|
-
-
|
8
|
-
-
|
12
|
+
- Check
|
13
|
+
- thread safety
|
14
|
+
- AR reconnect behavior / connection check
|
15
|
+
- replication in other databases
|
16
|
+
|
17
|
+
- restructure MasterSlaveAdapter with proper namespaces
|
18
|
+
- make clock private
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module MasterSlaveAdapter
|
4
|
+
class CircuitBreaker
|
5
|
+
def initialize(logger = nil, failure_threshold = 5, timeout = 30)
|
6
|
+
@logger = logger
|
7
|
+
@failure_count = 0
|
8
|
+
@failure_threshold = failure_threshold
|
9
|
+
@timeout = timeout
|
10
|
+
@state = :closed
|
11
|
+
end
|
12
|
+
|
13
|
+
def tripped?
|
14
|
+
if open? && timeout_exceeded?
|
15
|
+
change_state_to :half_open
|
16
|
+
end
|
17
|
+
|
18
|
+
open?
|
19
|
+
end
|
20
|
+
|
21
|
+
def success!
|
22
|
+
if !closed?
|
23
|
+
@failure_count = 0
|
24
|
+
change_state_to :closed
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def fail!
|
29
|
+
@failure_count += 1
|
30
|
+
if !open? && @failure_count >= @failure_threshold
|
31
|
+
@opened_at = Time.now
|
32
|
+
change_state_to :open
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def open?
|
39
|
+
:open == @state
|
40
|
+
end
|
41
|
+
|
42
|
+
def half_open?
|
43
|
+
:half_open == @state
|
44
|
+
end
|
45
|
+
|
46
|
+
def closed?
|
47
|
+
:closed == @state
|
48
|
+
end
|
49
|
+
|
50
|
+
def timeout_exceeded?
|
51
|
+
(Time.now - @opened_at) >= @timeout
|
52
|
+
end
|
53
|
+
|
54
|
+
def change_state_to(state)
|
55
|
+
@state = state
|
56
|
+
@logger && @logger.warn("circuit is now #{state}")
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module MasterSlaveAdapter
|
4
|
+
class Clock
|
5
|
+
include Comparable
|
6
|
+
attr_reader :file, :position
|
7
|
+
|
8
|
+
def initialize(file, position)
|
9
|
+
raise ArgumentError, "file and postion may not be nil" if file.nil? || position.nil?
|
10
|
+
@file, @position = file, position.to_i
|
11
|
+
end
|
12
|
+
|
13
|
+
def <=>(other)
|
14
|
+
@file == other.file ? @position <=> other.position : @file <=> other.file
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
[ @file, @position ].join('@')
|
19
|
+
end
|
20
|
+
|
21
|
+
def infinity?
|
22
|
+
self == self.class.infinity
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.zero
|
26
|
+
@zero ||= Clock.new('', 0)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.infinity
|
30
|
+
@infinity ||= Clock.new('', Float::MAX.to_i)
|
31
|
+
end
|
32
|
+
|
33
|
+
# TODO: tests
|
34
|
+
def self.parse(string)
|
35
|
+
new(*string.split('@'))
|
36
|
+
rescue
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -1 +1,501 @@
|
|
1
|
-
require '
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/connection_adapters/master_slave_adapter/circuit_breaker'
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
class MasterUnavailable < ConnectionNotEstablished; end
|
6
|
+
|
7
|
+
class Base
|
8
|
+
class << self
|
9
|
+
def with_consistency(clock, &blk)
|
10
|
+
if connection.respond_to? :with_consistency
|
11
|
+
connection.with_consistency(clock, &blk)
|
12
|
+
else
|
13
|
+
yield
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def with_master(&blk)
|
19
|
+
if connection.respond_to? :with_master
|
20
|
+
connection.with_master(&blk)
|
21
|
+
else
|
22
|
+
yield
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_slave(&blk)
|
27
|
+
if connection.respond_to? :with_slave
|
28
|
+
connection.with_slave(&blk)
|
29
|
+
else
|
30
|
+
yield
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def on_commit(&blk)
|
35
|
+
connection.on_commit(&blk) if connection.respond_to? :on_commit
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_rollback(&blk)
|
39
|
+
connection.on_rollback(&blk) if connection.respond_to? :on_rollback
|
40
|
+
end
|
41
|
+
|
42
|
+
def master_slave_connection(config)
|
43
|
+
config = massage(config)
|
44
|
+
name = config.fetch(:connection_adapter)
|
45
|
+
adapter = "#{name.classify}MasterSlaveAdapter"
|
46
|
+
|
47
|
+
load_adapter("#{name}_master_slave")
|
48
|
+
ConnectionAdapters.const_get(adapter).new(config, logger)
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
def massage(config)
|
54
|
+
config = config.symbolize_keys
|
55
|
+
skip = [ :adapter, :connection_adapter, :master, :slaves ]
|
56
|
+
defaults = config.
|
57
|
+
reject { |k,_| skip.include?(k) }.
|
58
|
+
merge(:adapter => config.fetch(:connection_adapter))
|
59
|
+
([config.fetch(:master)] + config.fetch(:slaves, [])).map do |cfg|
|
60
|
+
cfg.symbolize_keys!.reverse_merge!(defaults)
|
61
|
+
end
|
62
|
+
config
|
63
|
+
end
|
64
|
+
|
65
|
+
def load_adapter(adapter_name)
|
66
|
+
unless respond_to?("#{adapter_name}_connection")
|
67
|
+
begin
|
68
|
+
require "active_record/connection_adapters/#{adapter_name}_adapter"
|
69
|
+
rescue LoadError
|
70
|
+
begin
|
71
|
+
require 'rubygems'
|
72
|
+
gem "activerecord-#{adapter_name}-adapter"
|
73
|
+
require "active_record/connection_adapters/#{adapter_name}_adapter"
|
74
|
+
rescue LoadError
|
75
|
+
raise %Q{Please install the #{adapter_name} adapter:
|
76
|
+
`gem install activerecord-#{adapter_name}-adapter` (#{$!})"}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
module ConnectionAdapters
|
85
|
+
class AbstractAdapter
|
86
|
+
alias_method :orig_log_info, :log_info
|
87
|
+
def log_info(sql, name, ms)
|
88
|
+
connection_name =
|
89
|
+
[ @config[:name], @config[:host], @config[:port] ].compact.join(":")
|
90
|
+
orig_log_info sql, "[#{connection_name}] #{name || 'SQL'}", ms
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
module MasterSlaveAdapter
|
95
|
+
class Base < AbstractAdapter
|
96
|
+
def initialize(config, logger)
|
97
|
+
super(nil, logger)
|
98
|
+
|
99
|
+
@config = config
|
100
|
+
|
101
|
+
@connections = {}
|
102
|
+
@connections[:master] = connect_to_master
|
103
|
+
@connections[:slaves] = @config.fetch(:slaves).map { |cfg| connect(cfg, :slave) }
|
104
|
+
|
105
|
+
@disable_connection_test = @config[:disable_connection_test] == 'true'
|
106
|
+
@circuit = CircuitBreaker.new(logger)
|
107
|
+
|
108
|
+
self.current_connection = slave_connection!
|
109
|
+
end
|
110
|
+
|
111
|
+
# MASTER SLAVE ADAPTER INTERFACE ========================================
|
112
|
+
|
113
|
+
def with_master
|
114
|
+
with(master_connection) { yield }
|
115
|
+
end
|
116
|
+
|
117
|
+
def with_slave
|
118
|
+
with(slave_connection!) { yield }
|
119
|
+
end
|
120
|
+
|
121
|
+
def with_consistency(clock)
|
122
|
+
if clock.nil?
|
123
|
+
raise ArgumentError, "consistency must be a valid comparable value"
|
124
|
+
end
|
125
|
+
|
126
|
+
# try random slave, else fall back to master
|
127
|
+
slave = slave_connection!
|
128
|
+
conn =
|
129
|
+
if !open_transaction? && slave_consistent?(slave, clock)
|
130
|
+
slave
|
131
|
+
else
|
132
|
+
master_connection
|
133
|
+
end
|
134
|
+
|
135
|
+
with(conn) { yield }
|
136
|
+
|
137
|
+
current_clock || clock
|
138
|
+
end
|
139
|
+
|
140
|
+
def on_commit(&blk)
|
141
|
+
on_commit_callbacks.push blk
|
142
|
+
end
|
143
|
+
|
144
|
+
def on_rollback(&blk)
|
145
|
+
on_rollback_callbacks.push blk
|
146
|
+
end
|
147
|
+
|
148
|
+
# ADAPTER INTERFACE OVERRIDES ===========================================
|
149
|
+
|
150
|
+
def insert(*args)
|
151
|
+
on_write { |conn| conn.insert(*args) }
|
152
|
+
end
|
153
|
+
|
154
|
+
def update(*args)
|
155
|
+
on_write { |conn| conn.update(*args) }
|
156
|
+
end
|
157
|
+
|
158
|
+
def delete(*args)
|
159
|
+
on_write { |conn| conn.delete(*args) }
|
160
|
+
end
|
161
|
+
|
162
|
+
def commit_db_transaction
|
163
|
+
on_write { |conn| conn.commit_db_transaction }
|
164
|
+
on_commit_callbacks.shift.call(current_clock) until on_commit_callbacks.blank?
|
165
|
+
end
|
166
|
+
|
167
|
+
def rollback_db_transaction
|
168
|
+
on_commit_callbacks.clear
|
169
|
+
with(master_connection) { |conn| conn.rollback_db_transaction }
|
170
|
+
on_rollback_callbacks.shift.call until on_rollback_callbacks.blank?
|
171
|
+
end
|
172
|
+
|
173
|
+
def active?
|
174
|
+
return true if @disable_connection_test
|
175
|
+
connections.map { |c| c.active? }.all?
|
176
|
+
end
|
177
|
+
|
178
|
+
def reconnect!
|
179
|
+
connections.each { |c| c.reconnect! }
|
180
|
+
end
|
181
|
+
|
182
|
+
def disconnect!
|
183
|
+
connections.each { |c| c.disconnect! }
|
184
|
+
end
|
185
|
+
|
186
|
+
def reset!
|
187
|
+
connections.each { |c| c.reset! }
|
188
|
+
end
|
189
|
+
|
190
|
+
def cache(&blk)
|
191
|
+
connections.inject(blk) do |block, connection|
|
192
|
+
lambda { connection.cache(&block) }
|
193
|
+
end.call
|
194
|
+
end
|
195
|
+
|
196
|
+
def uncached(&blk)
|
197
|
+
connections.inject(blk) do |block, connection|
|
198
|
+
lambda { connection.uncached(&block) }
|
199
|
+
end.call
|
200
|
+
end
|
201
|
+
|
202
|
+
def clear_query_cache
|
203
|
+
connections.each { |connection| connection.clear_query_cache }
|
204
|
+
end
|
205
|
+
|
206
|
+
# Someone calling execute directly on the connection is likely to be a
|
207
|
+
# write, respectively some DDL statement. People really shouldn't do that,
|
208
|
+
# but let's delegate this to master, just to be sure.
|
209
|
+
def execute(*args)
|
210
|
+
on_write { |conn| conn.execute(*args) }
|
211
|
+
end
|
212
|
+
|
213
|
+
# ADAPTER INTERFACE DELEGATES ===========================================
|
214
|
+
|
215
|
+
def self.rescued_delegate(*methods)
|
216
|
+
options = methods.pop
|
217
|
+
to = options[:to]
|
218
|
+
|
219
|
+
file, line = caller.first.split(':', 2)
|
220
|
+
line = line.to_i
|
221
|
+
|
222
|
+
methods.each do |method|
|
223
|
+
module_eval(<<-EOS, file, line)
|
224
|
+
def #{method}(*args, &block)
|
225
|
+
begin
|
226
|
+
#{to}.__send__(:#{method}, *args, &block)
|
227
|
+
rescue => exception
|
228
|
+
if master_connection?(#{to}) && connection_error?(exception)
|
229
|
+
reset_master_connection
|
230
|
+
raise MasterUnavailable
|
231
|
+
else
|
232
|
+
raise
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
EOS
|
237
|
+
end
|
238
|
+
end
|
239
|
+
class << self; private :rescued_delegate; end
|
240
|
+
|
241
|
+
# === must go to master
|
242
|
+
rescued_delegate :adapter_name,
|
243
|
+
:supports_migrations?,
|
244
|
+
:supports_primary_key?,
|
245
|
+
:supports_savepoints?,
|
246
|
+
:native_database_types,
|
247
|
+
:raw_connection,
|
248
|
+
:open_transactions,
|
249
|
+
:increment_open_transactions,
|
250
|
+
:decrement_open_transactions,
|
251
|
+
:transaction_joinable=,
|
252
|
+
:create_savepoint,
|
253
|
+
:rollback_to_savepoint,
|
254
|
+
:release_savepoint,
|
255
|
+
:current_savepoint_name,
|
256
|
+
:begin_db_transaction,
|
257
|
+
:outside_transaction?,
|
258
|
+
:add_limit!,
|
259
|
+
:default_sequence_name,
|
260
|
+
:reset_sequence!,
|
261
|
+
:insert_fixture,
|
262
|
+
:empty_insert_statement,
|
263
|
+
:case_sensitive_equality_operator,
|
264
|
+
:limited_update_conditions,
|
265
|
+
:insert_sql,
|
266
|
+
:update_sql,
|
267
|
+
:delete_sql,
|
268
|
+
:to => :master_connection
|
269
|
+
# schema statements
|
270
|
+
rescued_delegate :table_exists?,
|
271
|
+
:create_table,
|
272
|
+
:change_table,
|
273
|
+
:rename_table,
|
274
|
+
:drop_table,
|
275
|
+
:add_column,
|
276
|
+
:remove_column,
|
277
|
+
:remove_columns,
|
278
|
+
:change_column,
|
279
|
+
:change_column_default,
|
280
|
+
:rename_column,
|
281
|
+
:add_index,
|
282
|
+
:remove_index,
|
283
|
+
:remove_index!,
|
284
|
+
:rename_index,
|
285
|
+
:index_name,
|
286
|
+
:index_exists?,
|
287
|
+
:structure_dump,
|
288
|
+
:dump_schema_information,
|
289
|
+
:initialize_schema_migrations_table,
|
290
|
+
:assume_migrated_upto_version,
|
291
|
+
:type_to_sql,
|
292
|
+
:add_column_options!,
|
293
|
+
:distinct,
|
294
|
+
:add_order_by_for_association_limiting!,
|
295
|
+
:add_timestamps,
|
296
|
+
:remove_timestamps,
|
297
|
+
:to => :master_connection
|
298
|
+
# ActiveRecord 3.0
|
299
|
+
rescued_delegate :visitor,
|
300
|
+
:to => :master_connection
|
301
|
+
# no clear interface contract:
|
302
|
+
rescued_delegate :tables, # commented in SchemaStatements
|
303
|
+
:truncate_table, # monkeypatching database_cleaner gem
|
304
|
+
:primary_key, # is Base#primary_key meant to be the contract?
|
305
|
+
:to => :master_connection
|
306
|
+
# No need to be so picky about these methods
|
307
|
+
rescued_delegate :add_limit_offset!, # DatabaseStatements
|
308
|
+
:add_lock!, #DatabaseStatements
|
309
|
+
:columns,
|
310
|
+
:table_alias_for,
|
311
|
+
:to => :prefer_master_connection
|
312
|
+
|
313
|
+
# ok, we might have missed more
|
314
|
+
def method_missing(name, *args, &blk)
|
315
|
+
master_connection.send(name.to_sym, *args, &blk).tap do
|
316
|
+
@logger.try(:warn, %Q{
|
317
|
+
You called the unsupported method '#{name}' on #{self.class.name}.
|
318
|
+
In order to help us improve master_slave_adapter, please report this
|
319
|
+
to: https://github.com/soundcloud/master_slave_adapter/issues
|
320
|
+
|
321
|
+
Thank you.
|
322
|
+
})
|
323
|
+
end
|
324
|
+
rescue => exception
|
325
|
+
if connection_error?(exception)
|
326
|
+
reset_master_connection
|
327
|
+
raise MasterUnavailable
|
328
|
+
else
|
329
|
+
raise
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
# === determine read connection
|
334
|
+
rescued_delegate :select_all,
|
335
|
+
:select_one,
|
336
|
+
:select_rows,
|
337
|
+
:select_value,
|
338
|
+
:select_values,
|
339
|
+
:to => :connection_for_read
|
340
|
+
|
341
|
+
def connection_for_read
|
342
|
+
open_transaction? ? master_connection : current_connection
|
343
|
+
end
|
344
|
+
private :connection_for_read
|
345
|
+
|
346
|
+
# === doesn't really matter, but must be handled by underlying adapter
|
347
|
+
rescued_delegate *(ActiveRecord::ConnectionAdapters::Quoting.instance_methods + [{
|
348
|
+
:to => :current_connection }])
|
349
|
+
# issue #4: current_database is not supported by all adapters, though
|
350
|
+
rescued_delegate :current_database, :to => :current_connection
|
351
|
+
|
352
|
+
# UTIL ==================================================================
|
353
|
+
|
354
|
+
def master_connection
|
355
|
+
if circuit.tripped?
|
356
|
+
raise MasterUnavailable
|
357
|
+
end
|
358
|
+
|
359
|
+
@connections[:master] ||= connect_to_master
|
360
|
+
if @connections[:master]
|
361
|
+
circuit.success!
|
362
|
+
@connections[:master]
|
363
|
+
else
|
364
|
+
circuit.fail!
|
365
|
+
raise MasterUnavailable
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
def master_available?
|
370
|
+
!@connections[:master].nil?
|
371
|
+
end
|
372
|
+
|
373
|
+
# Returns a random slave connection
|
374
|
+
# Note: the method is not referentially transparent, hence the bang
|
375
|
+
def slave_connection!
|
376
|
+
@connections[:slaves].sample
|
377
|
+
end
|
378
|
+
|
379
|
+
def connections
|
380
|
+
@connections.values.flatten.compact
|
381
|
+
end
|
382
|
+
|
383
|
+
def current_connection
|
384
|
+
connection_stack.first
|
385
|
+
end
|
386
|
+
|
387
|
+
def current_clock
|
388
|
+
@master_slave_clock
|
389
|
+
end
|
390
|
+
|
391
|
+
def master_clock
|
392
|
+
raise NotImplementedError
|
393
|
+
end
|
394
|
+
|
395
|
+
def slave_clock(conn)
|
396
|
+
raise NotImplementedError
|
397
|
+
end
|
398
|
+
|
399
|
+
protected
|
400
|
+
|
401
|
+
def prefer_master_connection
|
402
|
+
master_available? ? master_connection : slave_connection!
|
403
|
+
end
|
404
|
+
|
405
|
+
def master_connection?(connection)
|
406
|
+
@connections[:master] == connection
|
407
|
+
end
|
408
|
+
|
409
|
+
def reset_master_connection
|
410
|
+
@connections[:master] = nil
|
411
|
+
end
|
412
|
+
|
413
|
+
def current_clock=(clock)
|
414
|
+
@master_slave_clock = clock
|
415
|
+
end
|
416
|
+
|
417
|
+
def slave_consistent?(conn, clock)
|
418
|
+
if (last_seen_clock = get_last_seen_slave_clock(conn))
|
419
|
+
last_seen_clock >= clock
|
420
|
+
elsif (slave_clk = slave_clock(conn))
|
421
|
+
set_last_seen_slave_clock(conn, slave_clk)
|
422
|
+
slave_clk >= clock
|
423
|
+
else
|
424
|
+
false
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def on_write
|
429
|
+
with(master_connection) do |conn|
|
430
|
+
yield(conn).tap do
|
431
|
+
unless open_transaction?
|
432
|
+
if mc = master_clock
|
433
|
+
self.current_clock = mc unless current_clock.try(:>=, mc)
|
434
|
+
end
|
435
|
+
# keep using master after write
|
436
|
+
connection_stack.replace([ conn ])
|
437
|
+
end
|
438
|
+
end
|
439
|
+
end
|
440
|
+
end
|
441
|
+
|
442
|
+
def with(conn)
|
443
|
+
self.current_connection = conn
|
444
|
+
yield(conn).tap { connection_stack.shift if connection_stack.size > 1 }
|
445
|
+
end
|
446
|
+
|
447
|
+
private
|
448
|
+
|
449
|
+
def connect(cfg, name)
|
450
|
+
adapter_method = "#{cfg.fetch(:adapter)}_connection".to_sym
|
451
|
+
ActiveRecord::Base.send(adapter_method, { :name => name }.merge(cfg))
|
452
|
+
end
|
453
|
+
|
454
|
+
def open_transaction?
|
455
|
+
master_available? ? (master_connection.open_transactions > 0) : false
|
456
|
+
end
|
457
|
+
|
458
|
+
def connection_stack
|
459
|
+
@master_slave_connection ||= []
|
460
|
+
end
|
461
|
+
|
462
|
+
def current_connection=(conn)
|
463
|
+
connection_stack.unshift(conn)
|
464
|
+
end
|
465
|
+
|
466
|
+
def on_commit_callbacks
|
467
|
+
Thread.current[:on_commit_callbacks] ||= []
|
468
|
+
end
|
469
|
+
|
470
|
+
def on_rollback_callbacks
|
471
|
+
Thread.current[:on_rollback_callbacks] ||= []
|
472
|
+
end
|
473
|
+
|
474
|
+
def get_last_seen_slave_clock(conn)
|
475
|
+
conn.instance_variable_get(:@last_seen_slave_clock)
|
476
|
+
end
|
477
|
+
|
478
|
+
def set_last_seen_slave_clock(conn, clock)
|
479
|
+
last_seen = get_last_seen_slave_clock(conn)
|
480
|
+
if last_seen.nil? || last_seen < clock
|
481
|
+
conn.instance_variable_set(:@last_seen_slave_clock, clock)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def connect_to_master
|
486
|
+
connect(@config.fetch(:master), :master)
|
487
|
+
rescue => exception
|
488
|
+
connection_error?(exception) ? nil : raise
|
489
|
+
end
|
490
|
+
|
491
|
+
def connection_error?(exception)
|
492
|
+
raise NotImplementedError
|
493
|
+
end
|
494
|
+
|
495
|
+
def circuit
|
496
|
+
@circuit
|
497
|
+
end
|
498
|
+
end
|
499
|
+
end
|
500
|
+
end
|
501
|
+
end
|