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