mysql_replication_adapter 0.2.0 → 0.4.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.
- data/History.txt +5 -0
- data/Manifest.txt +1 -0
- data/lib/mysql_replication_adapter.rb +107 -416
- data/lib/mysql_replication_adapter/ar_base_ext.rb +69 -0
- data/lib/mysql_replication_adapter/version.rb +1 -1
- data/test/test_helper.rb +1 -0
- data/test/test_mysql_replication_adapter.rb +52 -2
- data/website/index.html +1 -1
- metadata +3 -2
data/History.txt
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
== 0.4.0 2007-11-02
|
2
|
+
* 1 major enhancement
|
3
|
+
* Fixed a bug in the slave selection algorithm that was causing the last item in the list to always be omitted.
|
4
|
+
* A bit of internal restructuring to make the code simpler.
|
5
|
+
|
1
6
|
== 0.0.1 2007-07-23
|
2
7
|
|
3
8
|
* 1 major enhancement:
|
data/Manifest.txt
CHANGED
@@ -8,172 +8,22 @@ unless defined?(RAILS_CONNECTION_ADAPTERS) && RAILS_CONNECTION_ADAPTERS.include?
|
|
8
8
|
RAILS_CONNECTION_ADAPTERS << "mysql_replication"
|
9
9
|
end
|
10
10
|
|
11
|
-
require 'active_record/connection_adapters/abstract_adapter'
|
12
11
|
require 'set'
|
13
|
-
|
14
|
-
module MysqlCompat #:nodoc:
|
15
|
-
# add all_hashes method to standard mysql-c bindings or pure ruby version
|
16
|
-
def self.define_all_hashes_method!
|
17
|
-
raise 'Mysql not loaded' unless defined?(::Mysql)
|
18
|
-
|
19
|
-
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
|
20
|
-
return if target.instance_methods.include?('all_hashes')
|
21
|
-
|
22
|
-
# Ruby driver has a version string and returns null values in each_hash
|
23
|
-
# C driver >= 2.7 returns null values in each_hash
|
24
|
-
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
|
25
|
-
target.class_eval <<-'end_eval'
|
26
|
-
def all_hashes
|
27
|
-
rows = []
|
28
|
-
each_hash { |row| rows << row }
|
29
|
-
rows
|
30
|
-
end
|
31
|
-
end_eval
|
32
|
-
|
33
|
-
# adapters before 2.7 don't have a version constant
|
34
|
-
# and don't return null values in each_hash
|
35
|
-
else
|
36
|
-
target.class_eval <<-'end_eval'
|
37
|
-
def all_hashes
|
38
|
-
rows = []
|
39
|
-
all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
|
40
|
-
each_hash { |row| rows << all_fields.dup.update(row) }
|
41
|
-
rows
|
42
|
-
end
|
43
|
-
end_eval
|
44
|
-
end
|
45
|
-
|
46
|
-
unless target.instance_methods.include?('all_hashes')
|
47
|
-
raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
12
|
+
require "mysql_replication_adapter/ar_base_ext"
|
51
13
|
|
52
14
|
module ActiveRecord
|
53
|
-
class Base
|
54
|
-
def self.require_mysql
|
55
|
-
# Include the MySQL driver if one hasn't already been loaded
|
56
|
-
unless defined? Mysql
|
57
|
-
begin
|
58
|
-
require_library_or_gem 'mysql'
|
59
|
-
rescue LoadError => cannot_require_mysql
|
60
|
-
# Use the bundled Ruby/MySQL driver if no driver is already in place
|
61
|
-
begin
|
62
|
-
require 'active_record/vendor/mysql'
|
63
|
-
rescue LoadError
|
64
|
-
raise cannot_require_mysql
|
65
|
-
end
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
# Define Mysql::Result.all_hashes
|
70
|
-
MysqlCompat.define_all_hashes_method!
|
71
|
-
end
|
72
|
-
|
73
|
-
# Establishes a connection to the database that's used by all Active Record objects.
|
74
|
-
def self.mysql_replication_connection(config) # :nodoc:
|
75
|
-
config = config.symbolize_keys
|
76
|
-
host = config[:host]
|
77
|
-
port = config[:port]
|
78
|
-
socket = config[:socket]
|
79
|
-
username = config[:username] ? config[:username].to_s : 'root'
|
80
|
-
password = config[:password].to_s
|
81
|
-
|
82
|
-
if config.has_key?(:database)
|
83
|
-
database = config[:database]
|
84
|
-
else
|
85
|
-
raise ArgumentError, "No database specified. Missing argument: database."
|
86
|
-
end
|
87
|
-
|
88
|
-
require_mysql
|
89
|
-
mysql = Mysql.init
|
90
|
-
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
|
91
|
-
|
92
|
-
ConnectionAdapters::MysqlReplicationAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
|
93
|
-
end
|
94
|
-
|
95
|
-
class << self
|
96
|
-
alias_method :old_find_every, :find_every
|
97
|
-
|
98
|
-
VALID_FIND_OPTIONS << :use_slave
|
99
|
-
|
100
|
-
# Override the standard find to check for the :use_slave option. When specified, the
|
101
|
-
# resulting query will be sent to a slave machine.
|
102
|
-
def find_every(options)
|
103
|
-
result = if options[:use_slave] && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
104
|
-
connection.load_balance_query do
|
105
|
-
old_find_every(options)
|
106
|
-
end
|
107
|
-
else
|
108
|
-
old_find_every(options)
|
109
|
-
end
|
110
|
-
result
|
111
|
-
end
|
112
|
-
|
113
|
-
alias_method :old_find_by_sql, :find_by_sql
|
114
|
-
# Override find_by_sql so that you can tell it to selectively use a slave machine
|
115
|
-
def find_by_sql(sql, use_slave = false)
|
116
|
-
if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
117
|
-
connection.load_balance_query {old_find_by_sql sql}
|
118
|
-
else
|
119
|
-
old_find_by_sql sql
|
120
|
-
end
|
121
|
-
end
|
122
|
-
|
123
|
-
alias_method :old_calculate, :calculate
|
124
|
-
|
125
|
-
def calculate(operation, column_name, options ={})
|
126
|
-
use_slave = options.delete(:use_slave)
|
127
|
-
if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
128
|
-
connection.load_balance_query {old_calculate(operation, column_name, options)}
|
129
|
-
else
|
130
|
-
old_calculate(operation, column_name, options)
|
131
|
-
end
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
end
|
136
|
-
|
137
15
|
module ConnectionAdapters
|
138
16
|
class CannotWriteToSlave < Exception
|
139
17
|
end
|
140
18
|
|
141
|
-
|
19
|
+
class AbstractAdapter
|
142
20
|
# Adding this method allows non-mysql-replication adapter applications to function without changing
|
143
21
|
# code. Useful in development and test.
|
144
|
-
|
145
|
-
|
146
|
-
#end
|
147
|
-
#end
|
148
|
-
|
149
|
-
class MysqlColumn < Column #:nodoc:
|
150
|
-
#TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text])
|
151
|
-
|
152
|
-
def initialize(name, default, sql_type = nil, null = true)
|
153
|
-
@original_default = default
|
154
|
-
super
|
155
|
-
@default = nil if missing_default_forged_as_empty_string?
|
22
|
+
def load_balance_query
|
23
|
+
yield
|
156
24
|
end
|
157
|
-
|
158
|
-
private
|
159
|
-
def simplified_type(field_type)
|
160
|
-
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
|
161
|
-
return :string if field_type =~ /enum/i
|
162
|
-
super
|
163
|
-
end
|
164
|
-
|
165
|
-
# MySQL misreports NOT NULL column default when none is given.
|
166
|
-
# We can't detect this for columns which may have a legitimate ''
|
167
|
-
# default (string, text, binary) but we can for others (integer,
|
168
|
-
# datetime, boolean, and the rest).
|
169
|
-
#
|
170
|
-
# Test whether the column has default '', is not null, and is not
|
171
|
-
# a type allowing default ''.
|
172
|
-
def missing_default_forged_as_empty_string?
|
173
|
-
!null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type)
|
174
|
-
end
|
175
25
|
end
|
176
|
-
|
26
|
+
|
177
27
|
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
|
178
28
|
# the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
|
179
29
|
#
|
@@ -196,51 +46,27 @@ module ActiveRecord
|
|
196
46
|
# to your environment.rb file:
|
197
47
|
#
|
198
48
|
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
|
199
|
-
class MysqlReplicationAdapter <
|
200
|
-
@@emulate_booleans = true
|
201
|
-
cattr_accessor :emulate_booleans
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
"MySQL server has gone away"
|
210
|
-
]
|
49
|
+
class MysqlReplicationAdapter < MysqlAdapter
|
50
|
+
# @@emulate_booleans = true
|
51
|
+
# cattr_accessor :emulate_booleans
|
52
|
+
#
|
53
|
+
# LOST_CONNECTION_ERROR_MESSAGES = [
|
54
|
+
# "Server shutdown in progress",
|
55
|
+
# "Broken pipe",
|
56
|
+
# "Lost connection to MySQL server during query",
|
57
|
+
# "MySQL server has gone away"
|
58
|
+
# ]
|
211
59
|
|
212
60
|
def initialize(connection, logger, connection_options, config)
|
213
|
-
|
214
|
-
@
|
215
|
-
|
216
|
-
connect
|
61
|
+
@master = @clones = nil
|
62
|
+
@retries = config[:retries]
|
63
|
+
super(connection, logger, connection_options, config)
|
217
64
|
end
|
218
65
|
|
219
66
|
def adapter_name #:nodoc:
|
220
67
|
'MySQLReplication'
|
221
68
|
end
|
222
69
|
|
223
|
-
def supports_migrations? #:nodoc:
|
224
|
-
true
|
225
|
-
end
|
226
|
-
|
227
|
-
def native_database_types #:nodoc:
|
228
|
-
{
|
229
|
-
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
|
230
|
-
:string => { :name => "varchar", :limit => 255 },
|
231
|
-
:text => { :name => "text" },
|
232
|
-
:integer => { :name => "int", :limit => 11 },
|
233
|
-
:float => { :name => "float" },
|
234
|
-
:decimal => { :name => "decimal" },
|
235
|
-
:datetime => { :name => "datetime" },
|
236
|
-
:timestamp => { :name => "datetime" },
|
237
|
-
:time => { :name => "time" },
|
238
|
-
:date => { :name => "date" },
|
239
|
-
:binary => { :name => "blob" },
|
240
|
-
:boolean => { :name => "tinyint", :limit => 1 }
|
241
|
-
}
|
242
|
-
end
|
243
|
-
|
244
70
|
# the magic load_balance method
|
245
71
|
def load_balance_query
|
246
72
|
old_connection = @connection
|
@@ -253,9 +79,9 @@ module ActiveRecord
|
|
253
79
|
# choose a random clone to use for the moment
|
254
80
|
def select_clone
|
255
81
|
# if we happen not to be connected to any clones, just use the master
|
256
|
-
return @master if @clones.empty?
|
82
|
+
return @master if @clones.nil? || @clones.empty?
|
257
83
|
# return a random clone
|
258
|
-
return @clones[rand(@clones.size
|
84
|
+
return @clones[rand(@clones.size)]
|
259
85
|
end
|
260
86
|
|
261
87
|
# This method raises an exception if the current connection is a clone. It is called inside
|
@@ -264,65 +90,13 @@ module ActiveRecord
|
|
264
90
|
def ensure_master
|
265
91
|
raise CannotWriteToSlave, "You attempted to perform a write operation inside a slave-balanced read block." unless @connection == @master
|
266
92
|
end
|
267
|
-
|
268
|
-
# QUOTING ==================================================
|
269
|
-
|
270
|
-
def quote(value, column = nil)
|
271
|
-
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
272
|
-
s = column.class.string_to_binary(value).unpack("H*")[0]
|
273
|
-
"x'#{s}'"
|
274
|
-
elsif value.kind_of?(BigDecimal)
|
275
|
-
"'#{value.to_s("F")}'"
|
276
|
-
else
|
277
|
-
super
|
278
|
-
end
|
279
|
-
end
|
280
|
-
|
281
|
-
def quote_column_name(name) #:nodoc:
|
282
|
-
"`#{name}`"
|
283
|
-
end
|
284
|
-
|
285
|
-
def quote_string(string) #:nodoc:
|
286
|
-
@connection.quote(string)
|
287
|
-
end
|
288
|
-
|
289
|
-
def quoted_true
|
290
|
-
"1"
|
291
|
-
end
|
292
|
-
|
293
|
-
def quoted_false
|
294
|
-
"0"
|
295
|
-
end
|
296
|
-
|
297
|
-
|
298
|
-
# CONNECTION MANAGEMENT ====================================
|
299
|
-
|
300
|
-
def active?
|
301
|
-
if @connection.respond_to?(:stat)
|
302
|
-
@connection.stat
|
303
|
-
else
|
304
|
-
@connection.query 'select 1'
|
305
|
-
end
|
306
|
-
|
307
|
-
# mysql-ruby doesn't raise an exception when stat fails.
|
308
|
-
if @connection.respond_to?(:errno)
|
309
|
-
@connection.errno.zero?
|
310
|
-
else
|
311
|
-
true
|
312
|
-
end
|
313
|
-
rescue Mysql::Error
|
314
|
-
false
|
315
|
-
end
|
316
|
-
|
317
|
-
def reconnect!
|
318
|
-
disconnect!
|
319
|
-
connect
|
320
|
-
end
|
321
|
-
|
93
|
+
|
322
94
|
def disconnect!
|
323
|
-
@
|
324
|
-
@clones
|
325
|
-
|
95
|
+
@master.close rescue nil
|
96
|
+
if @clones
|
97
|
+
@clones.each do |clone|
|
98
|
+
clone.close rescue nil
|
99
|
+
end
|
326
100
|
end
|
327
101
|
end
|
328
102
|
|
@@ -330,9 +104,23 @@ module ActiveRecord
|
|
330
104
|
# DATABASE STATEMENTS ======================================
|
331
105
|
|
332
106
|
def execute(sql, name = nil) #:nodoc:
|
107
|
+
retries = 0
|
333
108
|
log(sql, "#{name} against #{@connection.host_info}") do
|
334
109
|
@connection.query(sql)
|
335
110
|
end
|
111
|
+
rescue Mysql::Error => ex
|
112
|
+
if ex.message =~ /MySQL server has gone away/
|
113
|
+
if @retries && retries < @retries
|
114
|
+
retries += 1
|
115
|
+
disconnect!
|
116
|
+
connect
|
117
|
+
retry
|
118
|
+
else
|
119
|
+
raise
|
120
|
+
end
|
121
|
+
else
|
122
|
+
raise
|
123
|
+
end
|
336
124
|
rescue ActiveRecord::StatementInvalid => exception
|
337
125
|
if exception.message.split(":").first =~ /Packets out of order/
|
338
126
|
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
|
@@ -353,179 +141,82 @@ module ActiveRecord
|
|
353
141
|
@connection.affected_rows
|
354
142
|
end
|
355
143
|
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
def commit_db_transaction #:nodoc:
|
363
|
-
execute "COMMIT"
|
364
|
-
rescue Exception
|
365
|
-
# Transactions aren't supported
|
366
|
-
end
|
367
|
-
|
368
|
-
def rollback_db_transaction #:nodoc:
|
369
|
-
execute "ROLLBACK"
|
370
|
-
rescue Exception
|
371
|
-
# Transactions aren't supported
|
372
|
-
end
|
373
|
-
|
374
|
-
|
375
|
-
def add_limit_offset!(sql, options) #:nodoc:
|
376
|
-
if limit = options[:limit]
|
377
|
-
unless offset = options[:offset]
|
378
|
-
sql << " LIMIT #{limit}"
|
379
|
-
else
|
380
|
-
sql << " LIMIT #{offset}, #{limit}"
|
381
|
-
end
|
382
|
-
end
|
144
|
+
private
|
145
|
+
# Create the array of clone Mysql instances. Note that the instances
|
146
|
+
# actually don't correspond to the clone specs at this point. We're
|
147
|
+
# just getting Mysql object instances we can connect with later.
|
148
|
+
def init_clones
|
149
|
+
@clones = (@config[:clones] || @config[:slaves]).map{Mysql.init}
|
383
150
|
end
|
384
|
-
|
385
|
-
|
386
|
-
#
|
387
|
-
|
388
|
-
|
389
|
-
if
|
390
|
-
|
391
|
-
|
392
|
-
|
151
|
+
|
152
|
+
# Given a Mysql object and connection options, call #real_connect
|
153
|
+
# on the connection.
|
154
|
+
def setup_connection(conn, conn_opts)
|
155
|
+
# figure out if we're going to be doing any different
|
156
|
+
# encoding. if so, set it.
|
157
|
+
encoding = @config[:encoding]
|
158
|
+
if encoding
|
159
|
+
conn.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
393
160
|
end
|
394
161
|
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
end
|
413
|
-
|
414
|
-
def current_database
|
415
|
-
select_one("SELECT DATABASE() as db")["db"]
|
416
|
-
end
|
417
|
-
|
418
|
-
def tables(name = nil) #:nodoc:
|
419
|
-
tables = []
|
420
|
-
execute("SHOW TABLES", name).each { |field| tables << field[0] }
|
421
|
-
tables
|
422
|
-
end
|
423
|
-
|
424
|
-
def indexes(table_name, name = nil)#:nodoc:
|
425
|
-
indexes = []
|
426
|
-
current_index = nil
|
427
|
-
execute("SHOW KEYS FROM #{table_name}", name).each do |row|
|
428
|
-
if current_index != row[2]
|
429
|
-
next if row[2] == "PRIMARY" # skip the primary key
|
430
|
-
current_index = row[2]
|
431
|
-
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
|
432
|
-
end
|
433
|
-
|
434
|
-
indexes.last.columns << row[4]
|
435
|
-
end
|
436
|
-
indexes
|
437
|
-
end
|
438
|
-
|
439
|
-
def columns(table_name, name = nil)#:nodoc:
|
440
|
-
sql = "SHOW FIELDS FROM #{table_name}"
|
441
|
-
columns = []
|
442
|
-
execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
|
443
|
-
columns
|
444
|
-
end
|
445
|
-
|
446
|
-
def create_table(name, options = {}) #:nodoc:
|
447
|
-
super(name, {:options => "ENGINE=InnoDB"}.merge(options))
|
448
|
-
end
|
449
|
-
|
450
|
-
def rename_table(name, new_name)
|
451
|
-
execute "RENAME TABLE #{name} TO #{new_name}"
|
452
|
-
end
|
453
|
-
|
454
|
-
def change_column_default(table_name, column_name, default) #:nodoc:
|
455
|
-
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
|
456
|
-
|
457
|
-
execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
|
458
|
-
end
|
459
|
-
|
460
|
-
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
461
|
-
unless options_include_default?(options)
|
462
|
-
options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
|
463
|
-
end
|
464
|
-
|
465
|
-
change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
466
|
-
add_column_options!(change_column_sql, options)
|
467
|
-
execute(change_column_sql)
|
468
|
-
end
|
469
|
-
|
470
|
-
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
471
|
-
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
|
472
|
-
execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
|
162
|
+
# set the ssl options
|
163
|
+
conn.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
|
164
|
+
|
165
|
+
# do the actual connect
|
166
|
+
conn.real_connect(*conn_opts)
|
167
|
+
|
168
|
+
# swap the current connection for the connection we just set up
|
169
|
+
old_conn, @connection = @connection, conn
|
170
|
+
|
171
|
+
# set these options!
|
172
|
+
execute("SET NAMES '#{encoding}'") if encoding
|
173
|
+
# By default, MySQL 'where id is null' selects the last inserted id.
|
174
|
+
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
175
|
+
execute("SET SQL_AUTO_IS_NULL=0")
|
176
|
+
|
177
|
+
# swap the old current connection back into place
|
178
|
+
@connection = old_conn
|
473
179
|
end
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
#
|
488
|
-
|
489
|
-
|
490
|
-
# save the master in a separate instance variable so we always know what it is
|
491
|
-
@master = @connection
|
180
|
+
|
181
|
+
def connect
|
182
|
+
# if this is our first time in this method, then master will be
|
183
|
+
# nil, and should get set.
|
184
|
+
@master = @connection unless @master
|
185
|
+
|
186
|
+
# set up the master connection
|
187
|
+
setup_connection(@master, @connection_options)
|
188
|
+
|
189
|
+
clone_config = @config[:clones] || @config[:slaves]
|
190
|
+
|
191
|
+
# if clones are specified, then set up those connections
|
192
|
+
if clone_config
|
193
|
+
# create the clone connections if they don't already exist
|
194
|
+
init_clones unless @clones
|
492
195
|
|
493
|
-
#
|
494
|
-
|
495
|
-
clone_config = @config[:clones] || @config[:slaves]
|
196
|
+
# produce a pairing of connection options with an existing clone
|
197
|
+
clones_with_configs = @clones.zip(clone_config)
|
496
198
|
|
497
|
-
|
498
|
-
|
499
|
-
conn = Mysql.init
|
500
|
-
if encoding
|
501
|
-
conn.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
502
|
-
end
|
503
|
-
conn.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
|
199
|
+
clones_with_configs.each do |clone_and_config|
|
200
|
+
clone, config = clone_and_config
|
504
201
|
|
505
|
-
|
506
|
-
|
507
|
-
end
|
508
|
-
|
509
|
-
|
510
|
-
|
202
|
+
# Cause the individual clone Mysql instances to (re)connect
|
203
|
+
# Note - the instances aren't being replaced. This is critical,
|
204
|
+
# as otherwise the current connection could end up pointed at a
|
205
|
+
# bad connection object in the case of a failure.
|
206
|
+
setup_connection(clone,
|
207
|
+
[
|
208
|
+
config["host"],
|
209
|
+
config["username"], config["password"],
|
210
|
+
config["database"], config["port"], config["socket"]
|
211
|
+
]
|
212
|
+
)
|
511
213
|
end
|
214
|
+
else
|
215
|
+
# warning, no slaves specified.
|
216
|
+
warn "Warning: MysqlReplicationAdapter in use, but no slave database connections specified."
|
512
217
|
end
|
513
|
-
|
514
|
-
|
515
|
-
@connection.query_with_result = true
|
516
|
-
result = execute(sql, name)
|
517
|
-
rows = result.all_hashes
|
518
|
-
result.free
|
519
|
-
rows
|
520
|
-
end
|
521
|
-
|
522
|
-
def supports_views?
|
523
|
-
version[0] >= 5
|
524
|
-
end
|
525
|
-
|
526
|
-
def version
|
527
|
-
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
528
|
-
end
|
218
|
+
end
|
219
|
+
|
529
220
|
end
|
530
221
|
end
|
531
222
|
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
class Base
|
4
|
+
class << self
|
5
|
+
|
6
|
+
# Establishes a connection to the database that's used by all Active Record objects.
|
7
|
+
def mysql_replication_connection(config) # :nodoc:
|
8
|
+
config = config.symbolize_keys
|
9
|
+
host = config[:host]
|
10
|
+
port = config[:port]
|
11
|
+
socket = config[:socket]
|
12
|
+
username = config[:username] ? config[:username].to_s : 'root'
|
13
|
+
password = config[:password].to_s
|
14
|
+
|
15
|
+
if config.has_key?(:database)
|
16
|
+
database = config[:database]
|
17
|
+
else
|
18
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
19
|
+
end
|
20
|
+
|
21
|
+
require_mysql
|
22
|
+
mysql = Mysql.init
|
23
|
+
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
|
24
|
+
|
25
|
+
ConnectionAdapters::MysqlReplicationAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
|
26
|
+
end
|
27
|
+
|
28
|
+
alias_method :old_find_every, :find_every
|
29
|
+
|
30
|
+
VALID_FIND_OPTIONS << :use_slave
|
31
|
+
|
32
|
+
# Override the standard find to check for the :use_slave option. When specified, the
|
33
|
+
# resulting query will be sent to a slave machine.
|
34
|
+
def find_every(options)
|
35
|
+
result = if options[:use_slave] && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
36
|
+
connection.load_balance_query do
|
37
|
+
old_find_every(options)
|
38
|
+
end
|
39
|
+
else
|
40
|
+
old_find_every(options)
|
41
|
+
end
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
alias_method :old_find_by_sql, :find_by_sql
|
46
|
+
# Override find_by_sql so that you can tell it to selectively use a slave machine
|
47
|
+
def find_by_sql(sql, use_slave = false)
|
48
|
+
if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
49
|
+
connection.load_balance_query {old_find_by_sql sql}
|
50
|
+
else
|
51
|
+
old_find_by_sql sql
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
alias_method :old_calculate, :calculate
|
56
|
+
|
57
|
+
def calculate(operation, column_name, options ={})
|
58
|
+
use_slave = options.delete(:use_slave)
|
59
|
+
if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
60
|
+
connection.load_balance_query {old_calculate(operation, column_name, options)}
|
61
|
+
else
|
62
|
+
old_calculate(operation, column_name, options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
69
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -1,11 +1,61 @@
|
|
1
1
|
require File.dirname(__FILE__) + '/test_helper.rb'
|
2
2
|
|
3
|
+
class Person < ActiveRecord::Base
|
4
|
+
|
5
|
+
end
|
6
|
+
|
3
7
|
class TestMysqlReplicationAdapter < Test::Unit::TestCase
|
4
8
|
|
5
9
|
def setup
|
10
|
+
@adapter = get_adapter
|
11
|
+
ActiveRecord::Base.connection = @adapter
|
12
|
+
|
13
|
+
@adapter.drop_table :people rescue nil
|
14
|
+
|
15
|
+
@adapter.create_table :people do |t|
|
16
|
+
t.column :name, :string
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
def teardown
|
22
|
+
@adapter.drop_table :people
|
23
|
+
end
|
24
|
+
|
25
|
+
def get_adapter
|
26
|
+
ActiveRecord::Base.mysql_replication_connection(
|
27
|
+
{"host" => "localhost",
|
28
|
+
"username" => "root",
|
29
|
+
"password" => "",
|
30
|
+
"database" => "mysql_repl_test_master",
|
31
|
+
"retries" => 2,
|
32
|
+
"slaves" => [
|
33
|
+
{"host" => "localhost",
|
34
|
+
"username" => "root",
|
35
|
+
"password" => "",
|
36
|
+
"database" => "mysql_repl_test_slave_1"
|
37
|
+
},
|
38
|
+
{"host" => "localhost",
|
39
|
+
"username" => "root",
|
40
|
+
"password" => "",
|
41
|
+
"database" => "mysql_repl_test_slave_2"
|
42
|
+
}
|
43
|
+
]
|
44
|
+
}
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_insert
|
49
|
+
assert_not_nil Person.create(:name => "blah")
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_lookup
|
53
|
+
Person.create(:name => "blah")
|
54
|
+
assert_equal Person.find(1).id, 1
|
6
55
|
end
|
7
56
|
|
8
|
-
def
|
9
|
-
|
57
|
+
def test_slave_lookup
|
58
|
+
Person.create(:name => "blah")
|
59
|
+
assert_nil Person.find_by_id(1, :use_slave => true)
|
10
60
|
end
|
11
61
|
end
|
data/website/index.html
CHANGED
@@ -33,7 +33,7 @@
|
|
33
33
|
<h1>mysql_replication_adapter</h1>
|
34
34
|
<div id="version" class="clickable" onclick='document.location = "http://rubyforge.org/projects/mysql_replication_adapter"; return false'>
|
35
35
|
<p>Get Version</p>
|
36
|
-
<a href="http://rubyforge.org/projects/mysql_replication_adapter" class="numbers">0.
|
36
|
+
<a href="http://rubyforge.org/projects/mysql_replication_adapter" class="numbers">0.4.0</a>
|
37
37
|
</div>
|
38
38
|
<h1>→ ‘mysql_replication_adapter’</h1>
|
39
39
|
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
|
|
3
3
|
specification_version: 1
|
4
4
|
name: mysql_replication_adapter
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2007-
|
6
|
+
version: 0.4.0
|
7
|
+
date: 2007-11-02 00:00:00 -07:00
|
8
8
|
summary: An ActiveRecord database adapter that allows you to specify a single write master and multiple read-only slaves.
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- Rakefile
|
37
37
|
- lib/mysql_replication_adapter.rb
|
38
38
|
- lib/mysql_replication_adapter/version.rb
|
39
|
+
- lib/mysql_replication_adapter/ar_base_ext.rb
|
39
40
|
- scripts/txt2html
|
40
41
|
- setup.rb
|
41
42
|
- test/test_helper.rb
|