seamless_database_pool 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 Brian Durand
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 ADDED
@@ -0,0 +1,79 @@
1
+ Seamless Database Pool provides a simple way in which to add support for a master/slave database cluster to ActiveRecord to allow massive scalability and automatic failover. The guiding design principle behind this code is to make it absolutely trivial to add to an existing, complex application. That way when you have a big, nasty application which needs to scale the database you won't have to stop all feature development just to refactor your database connection code. Let's face it, when the database is having scaling problems, you are in for a world of hurt and the faster you can fix the problem the better.
2
+
3
+ This code is available as both a Rails plugin and a gem so it will work with any ActiveRecord application.
4
+
5
+ = Database Clusters
6
+
7
+ In a master/slave cluster you have one master database server which uses replication to feed all changes to one or more slave databases which are set up to only handle reads. Since most applications put most of the load on the server with reads, this setup can scale out an application quite well. You'll need to work with your database of choice to get replication set up. This plugin has an connection adapter which will handle proxying database requests to the right server.
8
+
9
+ = Simple Integration
10
+
11
+ You can convert a standard Rails application (i.e. one that follows the scaffold conventions) to use a database cluster with three simple steps:
12
+
13
+ 1. Set up the database cluster (OK maybe this one isn't simple)
14
+ 2. Update database.yml settings to point to the servers in the cluster
15
+ 3. Add this code to ApplicationController:
16
+
17
+ include SeamlessDatabasePool::ControllerFilter
18
+ use_database_pool :all => :persistent, [:create, :update, :destroy] => :master
19
+
20
+ If needed you can control how the connection pool is utilized by wrapping your code in some simple blocks.
21
+
22
+ = Failover
23
+
24
+ One of the other main advantages of using any sort of cluster is that one node can fail without bringing down your application. This plugin automatically handles failing over dead database connections in the read pool. That is if it tries to use a read connection and it is found to be inactive, the connector will try to reconnect. If that fails, it will try another connection in the read pool. After thirty seconds it will try to reconnect the dead connection again. One limitation on failover is that a server with an unreachable IP can't failover on startup. If you have a server completely die and it can't be restarted, you should update the pool configuration immediately to remove that entry.
25
+
26
+ = Configuration
27
+
28
+ == The pool configuration
29
+
30
+ The cluster connections are configured in database.yml using the seamless_database_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.
31
+
32
+ == The read pool
33
+
34
+ The read pool is specified with a read_pool property in the pool connection definition in database.yml. This should be specified as an array of hashes where each hash is the configuration for each read connection you'd like to use (see below for an example). As noted above, the configuration for the entire pool will be merged in with the options for each connection.
35
+
36
+ Each connection can be assigned an additional option of pool_weight. This value should be number which indicates the relative weight that the connection should be given in the pool. If no value is specified, it will default to one. Setting the value to zero will keep the connection out of the pool.
37
+
38
+ If possible, you should set the permissions on the database user for the read connections to one that only has select permission. This can be especially useful in development and testing to ensure that the read connection never have writes sent to them.
39
+
40
+ == The master connection
41
+
42
+ The master connection is specified with a master_connection property in the pool connection definition in database.yml (see below for an example). The master connection will be used for all non-select statements against the database (i.e. insert, update, delete, etc.). It will also be used for all statements inside a transaction or any reload commands.
43
+
44
+ By default, the master connection will be included in the read pool. If you would like to dedicate this connection only for write operations, you should set the pool weight to zero. Do not duplicate the master connection in the read pool as this will result in the additional overhead of two connections to the database.
45
+
46
+ == Example configuration
47
+
48
+ development:
49
+ adapter: seamless_database_pool
50
+ database: mydb_development
51
+ username: read_user
52
+ password: abc123
53
+ pool_adapter: mysql
54
+ port: 3306
55
+ master:
56
+ host: master-db.example.com
57
+ port: 6000
58
+ username: master_user
59
+ password: 567pass
60
+ read_pool:
61
+ - host: read-db-1.example.com
62
+ pool_weight: 2
63
+ - host: read-db-2.example.com
64
+
65
+ In this configuration, the master connection will be a mysql connection to master-db.example.com:6000 using the username master_user and the password 567pass.
66
+
67
+ The read pool will use three mysql connections to master-db, read-db-1, and read-db-2. The master connection will use a different port, username, password for the connection. The read connections will use the same values. Further, the connection read-db-1 will get half the traffic as the other two connections, so presumably it's on a more powerful box.
68
+
69
+ = Using the read pool
70
+
71
+ By default, the master connection will be used for everything. This is not terribly useful, so you should really specify a method of using the read pool for the actions that need it. Read connections will only be used for select statements against the database.
72
+
73
+ This is done with static methods on SeamlessDatabasePool.
74
+
75
+ = Controller Filters
76
+
77
+ To ease integration into a Ruby on Rails application, several controller filters are provided to invoke the above connection methods in a block. These are not implemented as standard controller filters so that the connection methods can be in effect for other filters.
78
+
79
+ See SeamlessDatabasePool::ControllerFilter for more details.
@@ -0,0 +1,43 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/rdoctask'
4
+ require 'rake/gempackagetask'
5
+ require 'spec/rake/spectask'
6
+
7
+ desc 'Default: run unit tests.'
8
+ task :default => :test
9
+
10
+ desc 'Test seamless_database_pool.'
11
+ Spec::Rake::SpecTask.new(:test) do |t|
12
+ t.spec_files = 'spec/**/*_spec.rb'
13
+ end
14
+
15
+ desc 'Generate documentation for seamless_database_pool.'
16
+ Rake::RDocTask.new(:rdoc) do |rdoc|
17
+ rdoc.rdoc_dir = 'rdoc'
18
+ rdoc.options << '--title' << 'Seamless Database Pool' << '--line-numbers' << '--inline-source' << '--main' << 'README'
19
+ rdoc.rdoc_files.include('README')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ spec = Gem::Specification.new do |s|
24
+ s.name = "seamless_database_pool"
25
+ s.version = "1.0.0"
26
+ s.author = "Brian Durand"
27
+ s.platform = Gem::Platform::RUBY
28
+ s.summary = "Support for master/slave database clusters in ActiveRecord"
29
+ s.files = FileList["lib/**/*", "init.rb", "MIT-LICENSE", 'Rakefile'].to_a
30
+ s.require_path = "lib"
31
+ s.test_files = FileList["{spec}/**/*_spec.rb"].to_a
32
+ s.has_rdoc = true
33
+ s.rdoc_options << '--title' << 'Seamless Database Pool' << '--line-numbers' << '--inline-source' << '--main' << 'README'
34
+ s.extra_rdoc_files = ["README"]
35
+ s.homepage = "http://seamlessdbpool.rubyforge.org"
36
+ s.rubyforge_project = "seamlessdbpool"
37
+ s.email = 'brian@embellishedvisions.com'
38
+ s.requirements = 'rspec 1.0.8 or higher is needed to run the tests'
39
+ end
40
+
41
+ Rake::GemPackageTask.new(spec) do |pkg|
42
+ pkg.need_tar = true
43
+ end
data/init.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'seamless_database_pool'
2
+
@@ -0,0 +1,302 @@
1
+ require 'seamless_database_pool/connect_timeout'
2
+
3
+ module ActiveRecord
4
+ class Base
5
+ def self.seamless_database_pool_connection (config)
6
+ pool_weights = {}
7
+
8
+ default_config = {:pool_weight => 1}.merge(config.merge(:adapter => config[:pool_adapter]))
9
+ default_config.delete(:master)
10
+ default_config.delete(:read_pool)
11
+ default_config.delete(:pool_adapter)
12
+
13
+ master_config = default_config.merge(config[:master].symbolize_keys)
14
+ establish_adapter(master_config[:adapter])
15
+ master_connection = send("#{master_config[:adapter]}_connection".to_sym, master_config)
16
+ master_connection.class.send(:include, SeamlessDatabasePool::ConnectTimeout) unless master_connection.class.include?(SeamlessDatabasePool::ConnectTimeout)
17
+ master_connection.connect_timeout = master_config[:connect_timeout]
18
+ pool_weights[master_connection] = master_config[:pool_weight].to_i if master_config[:pool_weight].to_i > 0
19
+
20
+ read_connections = []
21
+ config[:read_pool].each do |read_config|
22
+ read_config = default_config.merge(read_config.symbolize_keys)
23
+ read_config[:pool_weight] = read_config[:pool_weight].to_i
24
+ if read_config[:pool_weight] > 0
25
+ establish_adapter(read_config[:adapter])
26
+ conn = send("#{read_config[:adapter]}_connection".to_sym, read_config)
27
+ conn.class.send(:include, SeamlessDatabasePool::ConnectTimeout) unless conn.class.include?(SeamlessDatabasePool::ConnectTimeout)
28
+ conn.connect_timeout = read_config[:connect_timeout]
29
+ read_connections << conn
30
+ pool_weights[conn] = read_config[:pool_weight]
31
+ end
32
+ end if config[:read_pool]
33
+
34
+ ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.new(nil, logger, master_connection, read_connections, pool_weights)
35
+ end
36
+
37
+ def self.establish_adapter (adapter)
38
+ unless adapter then raise AdapterNotSpecified, "database configuration does not specify adapter" end
39
+ raise AdapterNotFound, "database pool must specify adapters" if adapter == 'seamless_database_pool'
40
+
41
+ begin
42
+ require 'rubygems'
43
+ gem "activerecord-#{adapter}-adapter"
44
+ require "active_record/connection_adapters/#{adapter}_adapter"
45
+ rescue LoadError
46
+ begin
47
+ require "active_record/connection_adapters/#{adapter}_adapter"
48
+ rescue LoadError
49
+ raise "Please install the #{adapter} adapter: `gem install activerecord-#{adapter}-adapter` (#{$!})"
50
+ end
51
+ end
52
+
53
+ adapter_method = "#{adapter}_connection"
54
+ if !respond_to?(adapter_method)
55
+ raise AdapterNotFound, "database configuration specifies nonexistent #{adapter} adapter"
56
+ end
57
+ end
58
+
59
+ # Force reload to use the master connection since it's probably being called for a reason.
60
+ def reload_with_seamless_database_pool (options = nil)
61
+ SeamlessDatabasePool.use_master_connection do
62
+ reload_without_seamless_database_pool(options)
63
+ end
64
+ end
65
+ alias_method_chain(:reload, :seamless_database_pool)
66
+ end
67
+
68
+ module Associations
69
+ class AssociationProxy
70
+ # Force reload to use the master connection since it's probably being called for a reason.
71
+ def reload_with_seamless_database_pool
72
+ SeamlessDatabasePool.use_master_connection do
73
+ reload_without_seamless_database_pool
74
+ end
75
+ end
76
+ alias_method_chain(:reload, :seamless_database_pool)
77
+ end
78
+ end
79
+
80
+ module ConnectionAdapters
81
+ class SeamlessDatabasePoolAdapter < AbstractAdapter
82
+
83
+ READ_ONLY_METHODS = [:select_one, :select_all, :select_value, :select_values, :select_rows, :select]
84
+ CONNECTION_METHODS = [:adapter_name, :active?, :reconnect!, :disconnect!, :verify!, :reset_runtime]
85
+
86
+ attr_reader :read_connections, :master_connection
87
+
88
+ def initialize (connection, logger, master_connection, read_connections, pool_weights)
89
+ super(connection, logger)
90
+
91
+ @master_connection = master_connection
92
+ @read_connections = read_connections.dup.freeze
93
+
94
+ @weighted_read_connections = []
95
+ pool_weights.each_pair do |conn, weight|
96
+ weight.times{@weighted_read_connections << conn}
97
+ end
98
+ @available_read_connections = [AvailableConnections.new(@weighted_read_connections)]
99
+
100
+ define_proxy_methods
101
+ end
102
+
103
+ def adapter_name #:nodoc:
104
+ 'Seamless Database Pool'
105
+ end
106
+
107
+ # Returns an array of the master connection and the read pool connections
108
+ def all_connections
109
+ [@master_connection] + @read_connections
110
+ end
111
+
112
+ # Get the pool weight of a connection
113
+ def pool_weight (connection)
114
+ return @weighted_read_connections.select{|conn| conn == connection}.size
115
+ end
116
+
117
+ def active?
118
+ active = true
119
+ all_connections.each{|conn| active &= conn.active?}
120
+ return active
121
+ end
122
+
123
+ def reconnect!
124
+ all_connections.each{|conn| conn.reconnect!}
125
+ end
126
+
127
+ def disconnect!
128
+ all_connections.each{|conn| conn.disconnect!}
129
+ end
130
+
131
+ def verify!(timeout)
132
+ all_connections.each{|conn| conn.verify!(timeout)}
133
+ end
134
+
135
+ def reset_runtime
136
+ all_connections.inject(0.0){|total, conn| total += conn.reset_runtime}
137
+ end
138
+
139
+ # Get a random read connection from the pool. If the connection is not active, it will attempt to reconnect
140
+ # to the database. If that fails, it will be removed from the pool for one minute.
141
+ def random_read_connection
142
+ weighted_read_connections = available_read_connections
143
+ if @use_master or weighted_read_connections.empty?
144
+ return master_connection
145
+ else
146
+ weighted_read_connections[rand(weighted_read_connections.length)]
147
+ end
148
+ end
149
+
150
+ # Get the current read connection
151
+ def current_read_connection
152
+ return SeamlessDatabasePool.read_only_connection(self)
153
+ end
154
+
155
+ def transaction(start_db_transaction = true)
156
+ use_master_connection do
157
+ master_connection.transaction(start_db_transaction) do
158
+ yield if block_given?
159
+ end
160
+ end
161
+ end
162
+
163
+ def using_master_connection?
164
+ !!@use_master
165
+ end
166
+
167
+ # Force using the master connection in a block.
168
+ def use_master_connection
169
+ save_val = @use_master
170
+ begin
171
+ @use_master = true
172
+ yield if block_given?
173
+ ensure
174
+ @use_master = save_val
175
+ end
176
+ end
177
+
178
+ class DatabaseConnectionError < StandardError
179
+ attr_accessor :wrapped_exception
180
+ end
181
+
182
+ # This simple class puts an expire time on an array of connections. It is used so the a connection
183
+ # to a down database won't try to reconnect over and over.
184
+ class AvailableConnections
185
+ attr_reader :connections, :failed_connection
186
+ attr_writer :expires
187
+
188
+ def initialize (connections, failed_connection = nil, expires = nil)
189
+ @connections = connections
190
+ @failed_connection = failed_connection
191
+ @expires = expires
192
+ end
193
+
194
+ def expired?
195
+ @expires < Time.now if @expires
196
+ end
197
+
198
+ def reconnect!
199
+ failed_connection.reconnect!
200
+ raise DatabaseConnectionError.new unless failed_connection.active?
201
+ end
202
+ end
203
+
204
+ # Get the available weighted connections. When a connection is dead and cannot be reconnected, it will
205
+ # be temporarily removed from the read pool so we don't keep trying to reconnect to a database that isn't
206
+ # listening.
207
+ def available_read_connections
208
+ available = @available_read_connections.last
209
+ if available.expired?
210
+ begin
211
+ available.reconnect!
212
+ rescue
213
+ # Couldn't reconnect so try again in a little bit
214
+ available.expires = 30.seconds.from_now
215
+ return available.connections
216
+ end
217
+ @available_read_connections.pop
218
+ return available_read_connections
219
+ else
220
+ return available.connections
221
+ end
222
+ end
223
+
224
+ # Temporarily remove a connection from the read pool.
225
+ def suppress_read_connection (conn, expire)
226
+ available = available_read_connections
227
+ connections = available.reject{|c| c == conn}
228
+
229
+ # This wasn't a read connection so don't suppress it
230
+ return if connections.length == available.length
231
+
232
+ if connections.empty?
233
+ # No connections available so we might as well try them all again
234
+ @available_read_connections.slice!(1, @available_read_connections.length)
235
+ else
236
+ # Available connections will now not include the suppressed connection for a while
237
+ @available_read_connections.push(AvailableConnections.new(connections, conn, expire.seconds.from_now))
238
+ end
239
+ end
240
+
241
+ private
242
+
243
+ def proxy_connection_method (connection, method, proxy_type, *args, &block)
244
+ begin
245
+ connection.send(method, *args, &block)
246
+ rescue => e
247
+ unless proxy_type == :retry or connection.active?
248
+ connection.reconnect! rescue nil
249
+ connection_error = DatabaseConnectionError.new
250
+ connection_error.wrapped_exception = e
251
+ raise connection_error
252
+ end
253
+ raise e
254
+ end
255
+ end
256
+
257
+ # Define proxy methods to handle actual database work.
258
+ def define_proxy_methods
259
+ master_methods = master_connection.public_methods(false) + master_connection.protected_methods(false) + master_connection.private_methods(false)
260
+ master_methods -= READ_ONLY_METHODS
261
+ master_methods -= CONNECTION_METHODS
262
+ master_methods.delete(:transaction)
263
+
264
+ master_methods.each do |method_name|
265
+ instance_eval(%Q(
266
+ def #{method_name}(*args, &block)
267
+ begin
268
+ proxy_connection_method(master_connection, :#{method_name}, :master, *args, &block)
269
+ rescue DatabaseConnectionError => e
270
+ raise e.wrapped_exception
271
+ end
272
+ end
273
+ ))
274
+ end
275
+
276
+ READ_ONLY_METHODS.each do |method_name|
277
+ instance_eval(%Q(
278
+ def #{method_name}(*args, &block)
279
+ connection = current_read_connection
280
+ begin
281
+ proxy_connection_method(connection, :#{method_name}, :read, *args, &block)
282
+ rescue DatabaseConnectionError => e
283
+ unless block or using_master_connection?
284
+ # Try again with a different connection if needed unless it could have a side effect
285
+ unless connection.active?
286
+ suppress_read_connection(connection, 30)
287
+ connection = current_read_connection
288
+ SeamlessDatabasePool.set_persistent_read_connection(self, connection)
289
+ end
290
+ proxy_connection_method(connection, :#{method_name}, :retry, *args, &block)
291
+ else
292
+ raise e.wrapped_exception
293
+ end
294
+ end
295
+ end
296
+ ))
297
+ end
298
+ end
299
+
300
+ end
301
+ end
302
+ end