maria_db_cluster_pool 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/MIT-LICENSE +20 -0
- data/README.rdoc +36 -0
- data/Rakefile +83 -0
- data/lib/active_record/connection_adapters/maria_db_cluster_pool_adapter.rb +324 -0
- data/lib/maria_db_cluster_pool/arel_compiler.rb +27 -0
- data/lib/maria_db_cluster_pool/connect_timeout.rb +24 -0
- data/lib/maria_db_cluster_pool.rb +131 -0
- metadata +207 -0
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Microting A/S
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
== Database Cluster
|
2
|
+
|
3
|
+
MariaDB Cluster Pool gem is designed for usage with Maria DB Galera Cluster, so this gem will only support a master/master setup
|
4
|
+
|
5
|
+
= Configuration
|
6
|
+
|
7
|
+
== The pool configuration
|
8
|
+
|
9
|
+
The cluster connections are configured in database.yml using the maria_db_cluster_pool adapter. Any properties you configure for the connection will be inherited by all connections in the pool. In this way, you can configure ports, usernames, etc. once instead of for each connection. One exception is that you can set the pool_adapter property which each connection will inherit as the adapter property. Each connection in the pool uses all the same configuration properties as normal for the adapters.
|
10
|
+
|
11
|
+
== Example configuration
|
12
|
+
|
13
|
+
development:
|
14
|
+
adapter: maria_db_cluster_pool
|
15
|
+
database: mydb_development
|
16
|
+
username: read_user
|
17
|
+
password: abc123
|
18
|
+
pool_adapter: mysql
|
19
|
+
port: 3306
|
20
|
+
server_pool:
|
21
|
+
- host: read-db-1.example.com
|
22
|
+
pool_weight: 1
|
23
|
+
- host: read-db-2.example.com
|
24
|
+
pool_weight: 2
|
25
|
+
|
26
|
+
== License
|
27
|
+
|
28
|
+
This software is a derived work of https://github.com/bdurand/seamless_database_pool the parts which derives from that codes is copyrighted by Brian Durand
|
29
|
+
|
30
|
+
Copyright (C) 2013 Microting A/S
|
31
|
+
|
32
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
33
|
+
|
34
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
35
|
+
|
36
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'yaml'
|
4
|
+
|
5
|
+
desc 'Default: run unit tests.'
|
6
|
+
task :default => :test
|
7
|
+
|
8
|
+
begin
|
9
|
+
require 'rspec'
|
10
|
+
require 'rspec/core/rake_task'
|
11
|
+
desc 'Run the unit tests'
|
12
|
+
RSpec::Core::RakeTask.new(:test)
|
13
|
+
|
14
|
+
#namespace :test do
|
15
|
+
# desc "Run all tests including for all database adapters"
|
16
|
+
# task :all do
|
17
|
+
# save_val = ENV['TEST_ADAPTERS']
|
18
|
+
# begin
|
19
|
+
# ENV['TEST_ADAPTERS'] = YAML.load_file(File.expand_path("../spec/database.yml", __FILE__)).keys.join(' ')
|
20
|
+
# Rake::Task["test"].execute
|
21
|
+
# ensure
|
22
|
+
# ENV['TEST_ADAPTERS'] = save_val
|
23
|
+
# end
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# desc "Test all database adapters defined in database.yml or just the one specified in TEST_ADAPTERS"
|
27
|
+
# task :adapters do
|
28
|
+
# save_val = ENV['TEST_ADAPTERS']
|
29
|
+
# begin
|
30
|
+
# ENV['TEST_ADAPTERS'] ||= YAML.load_file(File.expand_path("../spec/database.yml", __FILE__)).keys.join(' ')
|
31
|
+
# Rake::Task["test:adapters:specified"].execute
|
32
|
+
# ensure
|
33
|
+
# ENV['TEST_ADAPTERS'] = save_val
|
34
|
+
# end
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# namespace :adapters do
|
38
|
+
# desc "Internal task to run database adapter tests"
|
39
|
+
# RSpec::Core::RakeTask.new(:specified) do |t|
|
40
|
+
# t.pattern = FileList.new('spec/connection_adapters_spec.rb')
|
41
|
+
# end
|
42
|
+
#
|
43
|
+
# YAML.load_file(File.expand_path("../spec/database.yml", __FILE__)).keys.each do |adapter_name|
|
44
|
+
# desc "Test the #{adapter_name} database adapter"
|
45
|
+
# task adapter_name do
|
46
|
+
# save_val = ENV['TEST_ADAPTERS']
|
47
|
+
# begin
|
48
|
+
# ENV['TEST_ADAPTERS'] = adapter_name
|
49
|
+
# Rake::Task["test:adapters:specified"].execute
|
50
|
+
# ensure
|
51
|
+
# ENV['TEST_ADAPTERS'] = save_val
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#end
|
57
|
+
rescue LoadError
|
58
|
+
task :test do
|
59
|
+
STDERR.puts "You must have rspec >= 2.0 to run the tests"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
begin
|
64
|
+
require 'jeweler'
|
65
|
+
Jeweler::Tasks.new do |gem|
|
66
|
+
gem.name = "maria_db_cluster_pool"
|
67
|
+
gem.summary = "Add support for master/master database clusters in ActiveRecord to improve performance."
|
68
|
+
gem.email = "rm@microting.com"
|
69
|
+
gem.homepage = "https://github.com/renemadsen/maria_db_cluster_pool"
|
70
|
+
gem.authors = ["René Schultz Madsen"]
|
71
|
+
gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "MIT-LICENSE"].to_a
|
72
|
+
gem.has_rdoc = true
|
73
|
+
gem.extra_rdoc_files = ["README.rdoc", "MIT-LICENSE"]
|
74
|
+
|
75
|
+
gem.add_dependency('activerecord', '>= 2.2.2')
|
76
|
+
gem.add_development_dependency('rspec', '>= 2.0')
|
77
|
+
gem.add_development_dependency('jeweler')
|
78
|
+
gem.add_development_dependency('mysql')
|
79
|
+
end
|
80
|
+
|
81
|
+
Jeweler::GemcutterTasks.new
|
82
|
+
rescue LoadError
|
83
|
+
end
|
@@ -0,0 +1,324 @@
|
|
1
|
+
require 'maria_db_cluster_pool/connect_timeout'
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Base
|
5
|
+
class << self
|
6
|
+
def maria_db_cluster_pool_connection(config)
|
7
|
+
pool_weights = {}
|
8
|
+
|
9
|
+
config = config.with_indifferent_access
|
10
|
+
default_config = {:pool_weight => 1}.merge(config.merge(:adapter => config[:pool_adapter])).with_indifferent_access
|
11
|
+
default_config.delete(:server_pool)
|
12
|
+
default_config.delete(:pool_adapter)
|
13
|
+
|
14
|
+
pool_connections = []
|
15
|
+
config[:server_pool].each do |server_config|
|
16
|
+
server_config = default_config.merge(server_config).with_indifferent_access
|
17
|
+
server_config[:pool_weight] = server_config[:pool_weight].to_i
|
18
|
+
begin
|
19
|
+
establish_adapter(server_config[:adapter])
|
20
|
+
conn = send("#{server_config[:adapter]}_connection".to_sym, server_config)
|
21
|
+
conn.class.send(:include, MariaDBClusterPool::ConnectTimeout) unless conn.class.include?(MariaDBClusterPool::ConnectTimeout)
|
22
|
+
conn.connect_timeout = server_config[:connect_timeout]
|
23
|
+
pool_connections << conn
|
24
|
+
pool_weights[conn] = server_config[:pool_weight]
|
25
|
+
rescue Exception => e
|
26
|
+
if logger
|
27
|
+
logger.error("Error connecting to read connection #{server_config.inspect}")
|
28
|
+
logger.error(e)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end if config[:server_pool]
|
32
|
+
|
33
|
+
@maria_db_cluster_pool_classes ||= {}
|
34
|
+
klass = @maria_db_cluster_pool_classes[pool_connections[0].class]
|
35
|
+
unless klass
|
36
|
+
klass = ActiveRecord::ConnectionAdapters::MariaDBClusterPoolAdapter.adapter_class(pool_connections[0])
|
37
|
+
@maria_db_cluster_pool_classes[pool_connections[0].class] = klass
|
38
|
+
end
|
39
|
+
|
40
|
+
return klass.new(pool_connections[0], logger, pool_connections, pool_weights)
|
41
|
+
end
|
42
|
+
|
43
|
+
def establish_adapter(adapter)
|
44
|
+
raise AdapterNotSpecified.new("database configuration does not specify adapter") unless adapter
|
45
|
+
raise AdapterNotFound.new("database pool must specify adapters") if adapter == 'MariaDB_Cluster_Pool'
|
46
|
+
|
47
|
+
begin
|
48
|
+
require 'rubygems'
|
49
|
+
gem "activerecord-#{adapter}-adapter"
|
50
|
+
require "active_record/connection_adapters/#{adapter}_adapter"
|
51
|
+
rescue LoadError
|
52
|
+
begin
|
53
|
+
require "active_record/connection_adapters/#{adapter}_adapter"
|
54
|
+
rescue LoadError
|
55
|
+
raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})"
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
adapter_method = "#{adapter}_connection"
|
60
|
+
if !respond_to?(adapter_method)
|
61
|
+
raise AdapterNotFound, "database configuration specifies nonexistent #{adapter} adapter"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
module MariaDBClusterPoolBehavior
|
67
|
+
def self.included(base)
|
68
|
+
base.alias_method_chain(:reload, :maria_db_cluster_pool)
|
69
|
+
end
|
70
|
+
|
71
|
+
# Force reload to use the master connection since it's probably being called for a reason.
|
72
|
+
def reload_with_maria_db_cluster_pool(*args)
|
73
|
+
reload_without_maria_db_Cluster_Pool(*args)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
include(MariaDBClusterPoolBehavior) unless include?(MariaDBClusterPoolBehavior)
|
78
|
+
end
|
79
|
+
|
80
|
+
module ConnectionAdapters
|
81
|
+
class MariaDBClusterPoolAdapter < AbstractAdapter
|
82
|
+
|
83
|
+
attr_reader :connections # The total sum of connections
|
84
|
+
attr_reader :master_connection # The current connection in use
|
85
|
+
attr_reader :available_connections # The list of connections usable to the class
|
86
|
+
|
87
|
+
class << self
|
88
|
+
# Create an anonymous class that extends this one and proxies methods to the pool connections.
|
89
|
+
def adapter_class(master_connection)
|
90
|
+
# Define methods to proxy to the appropriate pool
|
91
|
+
master_methods = []
|
92
|
+
master_connection_classes = [AbstractAdapter, Quoting, DatabaseStatements, SchemaStatements]
|
93
|
+
master_connection_classes << DatabaseLimits if const_defined?(:DatabaseLimits)
|
94
|
+
master_connection_class = master_connection.class
|
95
|
+
while ![Object, AbstractAdapter].include?(master_connection_class) do
|
96
|
+
master_connection_classes << master_connection_class
|
97
|
+
master_connection_class = master_connection_class.superclass
|
98
|
+
end
|
99
|
+
master_connection_classes.each do |connection_class|
|
100
|
+
master_methods.concat(connection_class.public_instance_methods(false))
|
101
|
+
master_methods.concat(connection_class.protected_instance_methods(false))
|
102
|
+
end
|
103
|
+
master_methods.uniq!
|
104
|
+
master_methods -= public_instance_methods(false) + protected_instance_methods(false) + private_instance_methods(false)
|
105
|
+
master_methods = master_methods.collect{|m| m.to_sym}
|
106
|
+
|
107
|
+
klass = Class.new(self)
|
108
|
+
master_methods.each do |method_name|
|
109
|
+
klass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
110
|
+
def #{method_name}(*args, &block)
|
111
|
+
return proxy_connection_method(master_connection, :#{method_name}, *args, &block)
|
112
|
+
end
|
113
|
+
EOS
|
114
|
+
end
|
115
|
+
|
116
|
+
return klass
|
117
|
+
end
|
118
|
+
|
119
|
+
# Set the arel visitor on the connections.
|
120
|
+
def visitor_for(pool)
|
121
|
+
# This is ugly, but then again, so is the code in ActiveRecord for setting the arel
|
122
|
+
# visitor. There is a note in the code indicating the method signatures should be updated.
|
123
|
+
config = pool.spec.config.with_indifferent_access
|
124
|
+
adapter = config[:master][:adapter] || config[:pool_adapter]
|
125
|
+
MariaDBClusterPool.adapter_class_for(adapter).visitor_for(pool)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def initialize(connection, logger, connections, pool_weights)
|
130
|
+
super(connection, logger)
|
131
|
+
|
132
|
+
@available_connections = []
|
133
|
+
@master_connection = connection
|
134
|
+
@connections = connections.dup.freeze
|
135
|
+
|
136
|
+
pool_weights.each_pair do |conn, weight|
|
137
|
+
@available_connections[weight] = AvailableConnection.new(conn)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def adapter_name #:nodoc:
|
142
|
+
'MariaDB_Cluster_Pool'
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns an array of the master connection and the read pool connections
|
146
|
+
def all_connections
|
147
|
+
@connections
|
148
|
+
end
|
149
|
+
|
150
|
+
def requires_reloading?
|
151
|
+
false
|
152
|
+
end
|
153
|
+
|
154
|
+
def visitor=(visitor)
|
155
|
+
all_connections.each{|conn| conn.visitor = visitor}
|
156
|
+
end
|
157
|
+
|
158
|
+
def visitor
|
159
|
+
connection.visitor
|
160
|
+
end
|
161
|
+
|
162
|
+
def active?
|
163
|
+
active = true
|
164
|
+
do_to_connections {|conn| active &= conn.active?}
|
165
|
+
return active
|
166
|
+
end
|
167
|
+
|
168
|
+
def reconnect!
|
169
|
+
do_to_connections {|conn| conn.reconnect!}
|
170
|
+
end
|
171
|
+
|
172
|
+
def disconnect!
|
173
|
+
do_to_connections {|conn| conn.disconnect!}
|
174
|
+
end
|
175
|
+
|
176
|
+
def reset!
|
177
|
+
do_to_connections {|conn| conn.reset!}
|
178
|
+
end
|
179
|
+
|
180
|
+
def verify!(*ignored)
|
181
|
+
do_to_connections {|conn| conn.verify!(*ignored)}
|
182
|
+
end
|
183
|
+
|
184
|
+
def reset_runtime
|
185
|
+
total = 0.0
|
186
|
+
do_to_connections {|conn| total += conn.reset_runtime}
|
187
|
+
total
|
188
|
+
end
|
189
|
+
|
190
|
+
class DatabaseConnectionError < StandardError
|
191
|
+
end
|
192
|
+
|
193
|
+
# This simple class puts an expire time on an array of connections. It is used so the a connection
|
194
|
+
# to a down database won't try to reconnect over and over.
|
195
|
+
class AvailableConnection
|
196
|
+
attr_reader :connection
|
197
|
+
attr_writer :failed_connection
|
198
|
+
attr_writer :expires
|
199
|
+
|
200
|
+
def initialize(connection, failed_connection = false, expires = nil)
|
201
|
+
@connection = connection
|
202
|
+
@failed_connection = failed_connection
|
203
|
+
@expires = expires
|
204
|
+
end
|
205
|
+
|
206
|
+
def expired?
|
207
|
+
@expires ? @expires <= Time.now : false
|
208
|
+
end
|
209
|
+
|
210
|
+
def failed?
|
211
|
+
@failed_connection
|
212
|
+
end
|
213
|
+
|
214
|
+
def reconnect!
|
215
|
+
@connection.reconnect!
|
216
|
+
if @connection.active?
|
217
|
+
@failed_connection = false
|
218
|
+
@expires = nil
|
219
|
+
else
|
220
|
+
raise DatabaseConnectionError.new
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
# Get the available weighted connections. When a connection is dead and cannot be reconnected, it will
|
226
|
+
# be temporarily removed from the read pool so we don't keep trying to reconnect to a database that isn't
|
227
|
+
# listening.
|
228
|
+
def available_connections
|
229
|
+
@available_connections.each do |a|
|
230
|
+
if a != nil
|
231
|
+
if a.expired?
|
232
|
+
begin
|
233
|
+
@logger.info("Adding dead database connection back to the pool : #{a.connection.inspect}") if @logger
|
234
|
+
a.reconnect!
|
235
|
+
rescue => e
|
236
|
+
a.expires = 30.seconds.from_now
|
237
|
+
@logger.warn("Failed to reconnect to database when adding connection back to the pool")
|
238
|
+
@logger.warn(e)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
244
|
+
@available_connections
|
245
|
+
|
246
|
+
end
|
247
|
+
|
248
|
+
def reset_available_connections
|
249
|
+
@available_connections.each do |a|
|
250
|
+
if a != nil
|
251
|
+
a.reconnect! rescue nil
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
# Temporarily remove a connection from the read pool.
|
257
|
+
def suppress_connection(conn, expire)
|
258
|
+
available = available_connections
|
259
|
+
available.each do |a|
|
260
|
+
if a != nil
|
261
|
+
if a.connection == conn
|
262
|
+
a.failed_connection = true
|
263
|
+
a.expires = expire.seconds.from_now
|
264
|
+
@logger.info("Supressing database connection from the pool : #{a.connection.inspect}") if @logger
|
265
|
+
end
|
266
|
+
end
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def next_usable_connection
|
271
|
+
available = available_connections
|
272
|
+
available.each do |a|
|
273
|
+
if a != nil
|
274
|
+
unless a.failed?
|
275
|
+
if a.connection.active?
|
276
|
+
@logger.info("New master connection is now : #{a.connection.inspect}") if @logger
|
277
|
+
@master_connection = a.connection
|
278
|
+
break
|
279
|
+
end
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
private
|
286
|
+
|
287
|
+
def proxy_connection_method(connection, method, *args, &block)
|
288
|
+
available_connections
|
289
|
+
begin
|
290
|
+
connection.send(method, *args, &block)
|
291
|
+
rescue ArgumentError
|
292
|
+
begin
|
293
|
+
connection.send(method, *args)
|
294
|
+
rescue ArgumentError
|
295
|
+
connection.send(method)
|
296
|
+
end
|
297
|
+
rescue
|
298
|
+
# If the statement was a read statement and it wasn't forced against the master connection
|
299
|
+
# try to reconnect if the connection is dead and then re-run the statement.
|
300
|
+
unless connection.active?
|
301
|
+
suppress_connection(@master_connection, 30)
|
302
|
+
next_usable_connection
|
303
|
+
end
|
304
|
+
proxy_connection_method(@master_connection, method, *args, &block)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
|
308
|
+
# Yield a block to each connection in the pool. If the connection is dead, ignore the error
|
309
|
+
def do_to_connections
|
310
|
+
all_connections.each do |conn|
|
311
|
+
begin
|
312
|
+
yield(conn)
|
313
|
+
rescue => e
|
314
|
+
if @logger
|
315
|
+
@logger.warn("Error in do_to_connections")
|
316
|
+
@logger.warn(e.message)
|
317
|
+
@logger.warn(e.backtrace.inspect)
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
322
|
+
end
|
323
|
+
end
|
324
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module Arel
|
2
|
+
module SqlCompiler
|
3
|
+
# Hook into arel to use the compiler used by the master connection.
|
4
|
+
class MariaDBClusterPoolCompiler < GenericCompiler
|
5
|
+
def self.new(relation)
|
6
|
+
@compiler_classes ||= {}
|
7
|
+
master_adapter = relation.engine.connection.master_connection.adapter_name
|
8
|
+
compiler_class = @compiler_classes[master_adapter]
|
9
|
+
unless compiler_class
|
10
|
+
begin
|
11
|
+
require "arel/engines/sql/compilers/#{master_adapter.downcase}_compiler"
|
12
|
+
rescue LoadError
|
13
|
+
begin
|
14
|
+
# try to load an externally defined compiler, in case this adapter has defined the compiler on its own.
|
15
|
+
require "#{master_adapter.downcase}/arel_compiler"
|
16
|
+
rescue LoadError
|
17
|
+
raise "#{master_adapter} is not supported by Arel."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
compiler_class = Arel::SqlCompiler.const_get("#{master_adapter}Compiler")
|
21
|
+
@compiler_classes[master_adapter] = compiler_class
|
22
|
+
end
|
23
|
+
compiler_class.new(relation)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'timeout'
|
2
|
+
|
3
|
+
module MariaDBClusterPool
|
4
|
+
# This module is mixed into connection adapters to allow the reconnect! method to timeout if the
|
5
|
+
# IP address becomes unreachable. The default timeout is 1 second, but you can change it by setting
|
6
|
+
# the connect_timeout parameter in the adapter configuration.
|
7
|
+
module ConnectTimeout
|
8
|
+
attr_accessor :connect_timeout
|
9
|
+
|
10
|
+
def self.included(base)
|
11
|
+
base.alias_method_chain :reconnect!, :connect_timeout
|
12
|
+
end
|
13
|
+
|
14
|
+
def reconnect_with_connect_timeout!
|
15
|
+
begin
|
16
|
+
timeout(connect_timeout || 1) do
|
17
|
+
reconnect_without_connect_timeout!
|
18
|
+
end
|
19
|
+
rescue Timeout::Error
|
20
|
+
raise ActiveRecord::ConnectionTimeoutError.new("reconnect timed out")
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'maria_db_cluster_pool', 'connect_timeout.rb')
|
2
|
+
#require File.join(File.dirname(__FILE__), 'mariadb_cluster_pool', 'connection_statistics.rb')
|
3
|
+
#require File.join(File.dirname(__FILE__), 'mariadb_cluster_pool', 'controller_filter.rb')
|
4
|
+
require File.join(File.dirname(__FILE__), 'active_record', 'connection_adapters', 'maria_db_cluster_pool_adapter.rb')
|
5
|
+
$LOAD_PATH << File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__))
|
6
|
+
|
7
|
+
# This module allows setting the read pool connection type. Generally you will use one of
|
8
|
+
#
|
9
|
+
# - use_random_connection
|
10
|
+
# - use_persistent_read_connection
|
11
|
+
# - use_master_connection
|
12
|
+
#
|
13
|
+
# Each of these methods can take an optional block. If they are called with a block, they
|
14
|
+
# will set the read connection type only within the block. Otherwise they will set the default
|
15
|
+
# read connection type. If none is ever called, the read connection type will be :master.
|
16
|
+
|
17
|
+
module MariaDBClusterPool
|
18
|
+
|
19
|
+
# Adapter name to class name map. This exists because there isn't an obvious way to translate things like
|
20
|
+
# sqlite3 to SQLite3. The adapters that ship with ActiveRecord are defined here. If you use
|
21
|
+
# an adapter that doesn't translate directly to camel case, then add the mapping here in an initializer.
|
22
|
+
ADAPTER_TO_CLASS_NAME_MAP = {"sqlite" => "SQLite", "sqlite3" => "SQLite3", "postgresql" => "PostgreSQL"}
|
23
|
+
|
24
|
+
#READ_CONNECTION_METHODS = [:master, :persistent, :random]
|
25
|
+
|
26
|
+
class << self
|
27
|
+
# Call this method to use a random connection from the read pool for every select statement.
|
28
|
+
# This method is good if your replication is very fast. Otherwise there is a chance you could
|
29
|
+
# get inconsistent results from one request to the next. This can result in mysterious failures
|
30
|
+
# if your code selects a value in one statement and then uses in another statement. You can wind
|
31
|
+
# up trying to use a value from one server that hasn't been replicated to another one yet.
|
32
|
+
# This method is best if you have few processes which generate a lot of queries and you have
|
33
|
+
# fast replication.
|
34
|
+
#def use_random_connection
|
35
|
+
# if block_given?
|
36
|
+
# set_read_only_connection_type(:random){yield}
|
37
|
+
# else
|
38
|
+
# Thread.current[:read_only_connection] = :random
|
39
|
+
# end
|
40
|
+
#end
|
41
|
+
|
42
|
+
# Call this method to pick a random connection from the read pool and use it for all subsequent
|
43
|
+
# select statements. This provides consistency from one select statement to the next. This
|
44
|
+
# method should always be called with a block otherwise you can end up with an imbalanced read
|
45
|
+
# pool. This method is best if you have lots of processes which have a relatively few select
|
46
|
+
# statements or a slow replication mechanism. Generally this is the best method to use for web
|
47
|
+
# applications.
|
48
|
+
#def use_persistent_read_connection
|
49
|
+
# if block_given?
|
50
|
+
# set_read_only_connection_type(:persistent){yield}
|
51
|
+
# else
|
52
|
+
# Thread.current[:read_only_connection] = {}
|
53
|
+
# end
|
54
|
+
#end
|
55
|
+
|
56
|
+
# Call this method to use the master connection for all subsequent select statements. This
|
57
|
+
# method is most useful when you are doing lots of updates since it guarantees consistency
|
58
|
+
# if you do a select immediately after an update or insert.
|
59
|
+
#
|
60
|
+
# The master connection will also be used for selects inside any transaction blocks. It will
|
61
|
+
# also be used if you pass :readonly => false to any ActiveRecord.find method.
|
62
|
+
#def use_master_connection
|
63
|
+
# if block_given?
|
64
|
+
# set_read_only_connection_type(:master){yield}
|
65
|
+
# else
|
66
|
+
# Thread.current[:read_only_connection] = :master
|
67
|
+
# end
|
68
|
+
#end
|
69
|
+
|
70
|
+
# Set the read only connection type to either :master, :random, or :persistent.
|
71
|
+
#def set_read_only_connection_type(connection_type)
|
72
|
+
# saved_connection = Thread.current[:read_only_connection]
|
73
|
+
# retval = nil
|
74
|
+
# begin
|
75
|
+
# connection_type = {} if connection_type == :persistent
|
76
|
+
# Thread.current[:read_only_connection] = connection_type
|
77
|
+
# retval = yield if block_given?
|
78
|
+
# ensure
|
79
|
+
# Thread.current[:read_only_connection] = saved_connection
|
80
|
+
# end
|
81
|
+
# return retval
|
82
|
+
#end
|
83
|
+
|
84
|
+
# Get the read only connection type currently in use. Will be one of :master, :random, or :persistent.
|
85
|
+
#def read_only_connection_type(default = :master)
|
86
|
+
# connection_type = Thread.current[:read_only_connection] || default
|
87
|
+
# connection_type = :persistent if connection_type.kind_of?(Hash)
|
88
|
+
# return connection_type
|
89
|
+
#end
|
90
|
+
|
91
|
+
# Get a connection from a connection pool.
|
92
|
+
#def get_a_connection(pool_connection)
|
93
|
+
# #return pool_connection.master_connection if pool_connection.using_master_connection?
|
94
|
+
# #connection_type = Thread.current[:read_only_connection]
|
95
|
+
#
|
96
|
+
# if connection_type.kind_of?(Hash)
|
97
|
+
# connection = connection_type[pool_connection]
|
98
|
+
# unless connection
|
99
|
+
# connection = pool_connection.random_connection
|
100
|
+
# connection_type[pool_connection] = connection
|
101
|
+
# end
|
102
|
+
# return connection
|
103
|
+
# elsif connection_type == :random
|
104
|
+
# return pool_connection.random_connection
|
105
|
+
# else
|
106
|
+
# return pool_connection.master_connection
|
107
|
+
# end
|
108
|
+
#end
|
109
|
+
|
110
|
+
# This method is provided as a way to change the persistent connection when it fails and a new one is substituted.
|
111
|
+
#def set_persistent_read_connection(pool_connection, read_connection)
|
112
|
+
# connection_type = Thread.current[:read_only_connection]
|
113
|
+
# connection_type[pool_connection] = read_connection if connection_type.kind_of?(Hash)
|
114
|
+
#end
|
115
|
+
|
116
|
+
#def clear_read_only_connection
|
117
|
+
# Thread.current[:read_only_connection] = nil
|
118
|
+
#end
|
119
|
+
|
120
|
+
# Get the connection adapter class for an adapter name. The class will be loaded from
|
121
|
+
# ActiveRecord::ConnectionAdapters::NameAdapter where Name is the camelized version of the name.
|
122
|
+
# If the adapter class does not fit this pattern (i.e. sqlite3 => SQLite3Adapter), then add
|
123
|
+
# the mapping to the +ADAPTER_TO_CLASS_NAME_MAP+ Hash.
|
124
|
+
def adapter_class_for(name)
|
125
|
+
name = name.to_s
|
126
|
+
class_name = ADAPTER_TO_CLASS_NAME_MAP[name] || name.camelize
|
127
|
+
"ActiveRecord::ConnectionAdapters::#{class_name}Adapter".constantize
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
metadata
ADDED
@@ -0,0 +1,207 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: maria_db_cluster_pool
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 25
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 3
|
10
|
+
version: 0.0.3
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- "Ren\xC3\xA9 Schultz Madsen"
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2013-04-04 00:00:00 +02:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: activerecord
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ">="
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 21
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 3
|
33
|
+
- 11
|
34
|
+
version: 2.3.11
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rake
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - "="
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 49
|
46
|
+
segments:
|
47
|
+
- 0
|
48
|
+
- 8
|
49
|
+
- 7
|
50
|
+
version: 0.8.7
|
51
|
+
type: :runtime
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: rspec
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
hash: 3
|
62
|
+
segments:
|
63
|
+
- 2
|
64
|
+
- 0
|
65
|
+
version: "2.0"
|
66
|
+
type: :runtime
|
67
|
+
version_requirements: *id003
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: jeweler
|
70
|
+
prerelease: false
|
71
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ">="
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
hash: 3
|
77
|
+
segments:
|
78
|
+
- 0
|
79
|
+
version: "0"
|
80
|
+
type: :runtime
|
81
|
+
version_requirements: *id004
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: mysql
|
84
|
+
prerelease: false
|
85
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
87
|
+
requirements:
|
88
|
+
- - ">="
|
89
|
+
- !ruby/object:Gem::Version
|
90
|
+
hash: 3
|
91
|
+
segments:
|
92
|
+
- 0
|
93
|
+
version: "0"
|
94
|
+
type: :runtime
|
95
|
+
version_requirements: *id005
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: activerecord
|
98
|
+
prerelease: false
|
99
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
100
|
+
none: false
|
101
|
+
requirements:
|
102
|
+
- - ">="
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
hash: 3
|
105
|
+
segments:
|
106
|
+
- 2
|
107
|
+
- 2
|
108
|
+
- 2
|
109
|
+
version: 2.2.2
|
110
|
+
type: :runtime
|
111
|
+
version_requirements: *id006
|
112
|
+
- !ruby/object:Gem::Dependency
|
113
|
+
name: rspec
|
114
|
+
prerelease: false
|
115
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
116
|
+
none: false
|
117
|
+
requirements:
|
118
|
+
- - ">="
|
119
|
+
- !ruby/object:Gem::Version
|
120
|
+
hash: 3
|
121
|
+
segments:
|
122
|
+
- 2
|
123
|
+
- 0
|
124
|
+
version: "2.0"
|
125
|
+
type: :development
|
126
|
+
version_requirements: *id007
|
127
|
+
- !ruby/object:Gem::Dependency
|
128
|
+
name: jeweler
|
129
|
+
prerelease: false
|
130
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
131
|
+
none: false
|
132
|
+
requirements:
|
133
|
+
- - ">="
|
134
|
+
- !ruby/object:Gem::Version
|
135
|
+
hash: 3
|
136
|
+
segments:
|
137
|
+
- 0
|
138
|
+
version: "0"
|
139
|
+
type: :development
|
140
|
+
version_requirements: *id008
|
141
|
+
- !ruby/object:Gem::Dependency
|
142
|
+
name: mysql
|
143
|
+
prerelease: false
|
144
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
145
|
+
none: false
|
146
|
+
requirements:
|
147
|
+
- - ">="
|
148
|
+
- !ruby/object:Gem::Version
|
149
|
+
hash: 3
|
150
|
+
segments:
|
151
|
+
- 0
|
152
|
+
version: "0"
|
153
|
+
type: :development
|
154
|
+
version_requirements: *id009
|
155
|
+
description:
|
156
|
+
email: rm@microting.com
|
157
|
+
executables: []
|
158
|
+
|
159
|
+
extensions: []
|
160
|
+
|
161
|
+
extra_rdoc_files:
|
162
|
+
- MIT-LICENSE
|
163
|
+
- README.rdoc
|
164
|
+
files:
|
165
|
+
- MIT-LICENSE
|
166
|
+
- README.rdoc
|
167
|
+
- Rakefile
|
168
|
+
- lib/active_record/connection_adapters/maria_db_cluster_pool_adapter.rb
|
169
|
+
- lib/maria_db_cluster_pool.rb
|
170
|
+
- lib/maria_db_cluster_pool/arel_compiler.rb
|
171
|
+
- lib/maria_db_cluster_pool/connect_timeout.rb
|
172
|
+
has_rdoc: true
|
173
|
+
homepage: https://github.com/renemadsen/maria_db_cluster_pool
|
174
|
+
licenses: []
|
175
|
+
|
176
|
+
post_install_message:
|
177
|
+
rdoc_options: []
|
178
|
+
|
179
|
+
require_paths:
|
180
|
+
- lib
|
181
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
182
|
+
none: false
|
183
|
+
requirements:
|
184
|
+
- - ">="
|
185
|
+
- !ruby/object:Gem::Version
|
186
|
+
hash: 3
|
187
|
+
segments:
|
188
|
+
- 0
|
189
|
+
version: "0"
|
190
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
191
|
+
none: false
|
192
|
+
requirements:
|
193
|
+
- - ">="
|
194
|
+
- !ruby/object:Gem::Version
|
195
|
+
hash: 3
|
196
|
+
segments:
|
197
|
+
- 0
|
198
|
+
version: "0"
|
199
|
+
requirements: []
|
200
|
+
|
201
|
+
rubyforge_project:
|
202
|
+
rubygems_version: 1.5.3
|
203
|
+
signing_key:
|
204
|
+
specification_version: 3
|
205
|
+
summary: Add support for master/master database clusters in ActiveRecord to improve performance.
|
206
|
+
test_files: []
|
207
|
+
|