seamless_database_pool 1.0.12 → 1.0.13

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,81 +1,83 @@
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
- You must use compatible database adapters for both the master and the read connections. For example, you cannot use an Oracle server as your master and PostgreSQL servers as you read slaves.
70
-
71
- = Using the read pool
72
-
73
- 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.
74
-
75
- This is done with static methods on SeamlessDatabasePool.
76
-
77
- = Controller Filters
78
-
79
- 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.
80
-
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.
25
+
26
+ One limitation on failover is when database servers are down when the pool is being initialized during startup. In this case, the connections cannot be initialized and are not added to the pool. If this happens, you will need to restart your processes once the database servers are back online.
27
+
28
+ = Configuration
29
+
30
+ == The pool configuration
31
+
32
+ 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.
33
+
34
+ == The read pool
35
+
36
+ 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.
37
+
38
+ 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.
39
+
40
+ 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.
41
+
42
+ == The master connection
43
+
44
+ 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.
45
+
46
+ 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.
47
+
48
+ == Example configuration
49
+
50
+ development:
51
+ adapter: seamless_database_pool
52
+ database: mydb_development
53
+ username: read_user
54
+ password: abc123
55
+ pool_adapter: mysql2
56
+ port: 3306
57
+ master:
58
+ host: master-db.example.com
59
+ port: 6000
60
+ username: master_user
61
+ password: 567pass
62
+ read_pool:
63
+ - host: read-db-1.example.com
64
+ pool_weight: 2
65
+ - host: read-db-2.example.com
66
+
67
+ 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.
68
+
69
+ 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.
70
+
71
+ You must use compatible database adapters for both the master and the read connections. For example, you cannot use an Oracle server as your master and PostgreSQL servers as you read slaves.
72
+
73
+ = Using the read pool
74
+
75
+ 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.
76
+
77
+ This is done with static methods on SeamlessDatabasePool.
78
+
79
+ = Controller Filters
80
+
81
+ 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.
82
+
81
83
  See SeamlessDatabasePool::ControllerFilter for more details.
data/Rakefile CHANGED
@@ -65,7 +65,7 @@ begin
65
65
  Jeweler::Tasks.new do |gem|
66
66
  gem.name = "seamless_database_pool"
67
67
  gem.summary = "Add support for master/slave database clusters in ActiveRecord to improve performance."
68
- gem.email = "brian@embellishedvisions.com"
68
+ gem.email = "bbdurand@gmail.com"
69
69
  gem.homepage = "http://github.com/bdurand/seamless_database_pool"
70
70
  gem.authors = ["Brian Durand"]
71
71
  gem.files = FileList["lib/**/*", "spec/**/*", "README.rdoc", "Rakefile", "MIT-LICENSE"].to_a
@@ -38,14 +38,8 @@ module ActiveRecord
38
38
  end
39
39
  end if config[:read_pool]
40
40
 
41
- @seamless_database_pool_classes ||= {}
42
- klass = @seamless_database_pool_classes[master_connection.class]
43
- unless klass
44
- klass = ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.adapter_class(master_connection)
45
- @seamless_database_pool_classes[master_connection.class] = klass
46
- end
47
-
48
- return klass.new(nil, logger, master_connection, read_connections, pool_weights)
41
+ klass = ::ActiveRecord::ConnectionAdapters::SeamlessDatabasePoolAdapter.adapter_class(master_connection)
42
+ klass.new(nil, logger, master_connection, read_connections, pool_weights)
49
43
  end
50
44
 
51
45
  def establish_adapter(adapter)
@@ -95,6 +89,9 @@ module ActiveRecord
95
89
  class << self
96
90
  # Create an anonymous class that extends this one and proxies methods to the pool connections.
97
91
  def adapter_class(master_connection)
92
+ adapter_class_name = master_connection.adapter_name.classify
93
+ return const_get(adapter_class_name) if const_defined?(adapter_class_name, false)
94
+
98
95
  # Define methods to proxy to the appropriate pool
99
96
  read_only_methods = [:select_one, :select_all, :select_value, :select_values, :select, :select_rows, :execute, :tables, :columns]
100
97
  master_methods = []
@@ -134,7 +131,9 @@ module ActiveRecord
134
131
  EOS
135
132
  end
136
133
  klass.send :protected, :select
137
-
134
+
135
+ const_set(adapter_class_name, klass)
136
+
138
137
  return klass
139
138
  end
140
139
 
@@ -149,11 +148,11 @@ module ActiveRecord
149
148
  end
150
149
 
151
150
  def initialize(connection, logger, master_connection, read_connections, pool_weights)
152
- super(connection, logger)
153
-
154
151
  @master_connection = master_connection
155
152
  @read_connections = read_connections.dup.freeze
156
153
 
154
+ super(connection, logger)
155
+
157
156
  @weighted_read_connections = []
158
157
  pool_weights.each_pair do |conn, weight|
159
158
  weight.times{@weighted_read_connections << conn}
@@ -214,6 +213,23 @@ module ActiveRecord
214
213
  do_to_connections {|conn| total += conn.reset_runtime}
215
214
  total
216
215
  end
216
+
217
+ def lease
218
+ synchronize do
219
+ unless @in_use
220
+ @in_use = true
221
+ @last_use = Time.now
222
+ end
223
+ end
224
+ end
225
+
226
+ def expire
227
+ @in_use = false
228
+ end
229
+
230
+ def close
231
+ pool.checkin self
232
+ end
217
233
 
218
234
  # Get a random read connection from the pool. If the connection is not active, it will attempt to reconnect
219
235
  # to the database. If that fails, it will be removed from the pool for one minute.
@@ -2,6 +2,8 @@ require File.join(File.dirname(__FILE__), 'seamless_database_pool', 'connect_tim
2
2
  require File.join(File.dirname(__FILE__), 'seamless_database_pool', 'connection_statistics.rb')
3
3
  require File.join(File.dirname(__FILE__), 'seamless_database_pool', 'controller_filter.rb')
4
4
  require File.join(File.dirname(__FILE__), 'active_record', 'connection_adapters', 'seamless_database_pool_adapter.rb')
5
+ require File.join(File.dirname(__FILE__), 'seamless_database_pool', 'railtie.rb') if defined?(Rails::Railtie)
6
+
5
7
  $LOAD_PATH << File.dirname(__FILE__) unless $LOAD_PATH.include?(File.dirname(__FILE__))
6
8
 
7
9
  # This module allows setting the read pool connection type. Generally you will use one of
@@ -126,6 +128,22 @@ module SeamlessDatabasePool
126
128
  class_name = ADAPTER_TO_CLASS_NAME_MAP[name] || name.camelize
127
129
  "ActiveRecord::ConnectionAdapters::#{class_name}Adapter".constantize
128
130
  end
131
+
132
+ # Pull out the master configuration for compatibility with such things as the Rails' rake db:*
133
+ # tasks which only support known adapters.
134
+ def master_database_configuration(database_configs)
135
+ configs = {}
136
+ database_configs.each do |key, values|
137
+ if values['adapter'] == 'seamless_database_pool'
138
+ values['adapter'] = values.delete('pool_adapter')
139
+ values = values.merge(values['master']) if values['master'].is_a?(Hash)
140
+ values.delete('pool_weight')
141
+ values.delete('master')
142
+ values.delete('read_pool')
143
+ end
144
+ configs[key] = values
145
+ end
146
+ configs
147
+ end
129
148
  end
130
-
131
149
  end
@@ -0,0 +1,13 @@
1
+ module SeamlessDatabasePool
2
+ class Railtie < ::Rails::Railtie
3
+ rake_tasks do
4
+ namespace :db do
5
+ task :load_config do
6
+ # Override seamless_database_pool configuration so db:* rake tasks work as expected.
7
+ original_config = Rails.application.config.database_configuration
8
+ ActiveRecord::Base.configurations = SeamlessDatabasePool.master_database_configuration(original_config)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -14,12 +14,12 @@ describe "Test connection adapters" do
14
14
 
15
15
  before(:all) do
16
16
  ActiveRecord::Base.establish_connection('adapter' => "sqlite3", 'database' => ":memory:")
17
- model.create_tables
17
+ SeamlessDatabasePool::TestModel.db_model(adapter).create_tables
18
18
  end
19
19
 
20
20
  after(:all) do
21
- model.drop_tables
22
- model.cleanup_database!
21
+ SeamlessDatabasePool::TestModel.db_model(adapter).drop_tables
22
+ SeamlessDatabasePool::TestModel.db_model(adapter).cleanup_database!
23
23
  end
24
24
 
25
25
  before(:each) do
@@ -30,8 +30,8 @@ mysql:
30
30
  username: root
31
31
  password:
32
32
  master:
33
- adapter: mysql
33
+ adapter: mysql2
34
34
  pool_weight: 0
35
35
  read_pool:
36
36
  - adapter: read_only
37
- real_adapter: mysql
37
+ real_adapter: mysql2
@@ -131,4 +131,38 @@ describe "SeamlessDatabasePool" do
131
131
  SeamlessDatabasePool.read_only_connection_type(nil).should == nil
132
132
  end
133
133
 
134
+ it "should pull out the master configurations for compatibility with rake db:* tasks" do
135
+ config = {
136
+ 'development' => {
137
+ 'adapter' => 'seamless_database_pool',
138
+ 'pool_adapter' => 'mysql2',
139
+ 'database' => 'development',
140
+ 'username' => 'root',
141
+ 'master' => {
142
+ 'host' => 'localhost',
143
+ 'pool_weight' => 2
144
+ },
145
+ 'read_pool' => {
146
+ 'host' => 'slavehost',
147
+ 'pool_weight' => 5
148
+ }
149
+ },
150
+ 'test' => {
151
+ 'adapter' => 'mysql2',
152
+ 'database' => 'test'
153
+ }
154
+ }
155
+ SeamlessDatabasePool.master_database_configuration(config).should == {
156
+ 'development' => {
157
+ 'adapter' => 'mysql2',
158
+ 'database' => 'development',
159
+ 'username' => 'root',
160
+ 'host' => 'localhost'
161
+ },
162
+ 'test' => {
163
+ 'adapter' => 'mysql2',
164
+ 'database' => 'test'
165
+ }
166
+ }
167
+ end
134
168
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: seamless_database_pool
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.12
4
+ version: 1.0.13
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-11-08 00:00:00.000000000 Z
12
+ date: 2013-06-11 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
16
- requirement: &70158253976860 !ruby/object:Gem::Requirement
16
+ requirement: !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,15 @@ dependencies:
21
21
  version: 2.2.2
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70158253976860
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 2.2.2
25
30
  - !ruby/object:Gem::Dependency
26
31
  name: rspec
27
- requirement: &70158253974440 !ruby/object:Gem::Requirement
32
+ requirement: !ruby/object:Gem::Requirement
28
33
  none: false
29
34
  requirements:
30
35
  - - ! '>='
@@ -32,10 +37,15 @@ dependencies:
32
37
  version: '2.0'
33
38
  type: :development
34
39
  prerelease: false
35
- version_requirements: *70158253974440
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '2.0'
36
46
  - !ruby/object:Gem::Dependency
37
47
  name: jeweler
38
- requirement: &70158253971880 !ruby/object:Gem::Requirement
48
+ requirement: !ruby/object:Gem::Requirement
39
49
  none: false
40
50
  requirements:
41
51
  - - ! '>='
@@ -43,10 +53,15 @@ dependencies:
43
53
  version: '0'
44
54
  type: :development
45
55
  prerelease: false
46
- version_requirements: *70158253971880
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
47
62
  - !ruby/object:Gem::Dependency
48
63
  name: sqlite3
49
- requirement: &70158253970680 !ruby/object:Gem::Requirement
64
+ requirement: !ruby/object:Gem::Requirement
50
65
  none: false
51
66
  requirements:
52
67
  - - ! '>='
@@ -54,10 +69,15 @@ dependencies:
54
69
  version: '0'
55
70
  type: :development
56
71
  prerelease: false
57
- version_requirements: *70158253970680
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
58
78
  - !ruby/object:Gem::Dependency
59
79
  name: mysql
60
- requirement: &70158253960160 !ruby/object:Gem::Requirement
80
+ requirement: !ruby/object:Gem::Requirement
61
81
  none: false
62
82
  requirements:
63
83
  - - ! '>='
@@ -65,10 +85,15 @@ dependencies:
65
85
  version: '0'
66
86
  type: :development
67
87
  prerelease: false
68
- version_requirements: *70158253960160
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
69
94
  - !ruby/object:Gem::Dependency
70
95
  name: pg
71
- requirement: &70158253958100 !ruby/object:Gem::Requirement
96
+ requirement: !ruby/object:Gem::Requirement
72
97
  none: false
73
98
  requirements:
74
99
  - - ! '>='
@@ -76,9 +101,14 @@ dependencies:
76
101
  version: '0'
77
102
  type: :development
78
103
  prerelease: false
79
- version_requirements: *70158253958100
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
80
110
  description:
81
- email: brian@embellishedvisions.com
111
+ email: bbdurand@gmail.com
82
112
  executables: []
83
113
  extensions: []
84
114
  extra_rdoc_files:
@@ -94,6 +124,7 @@ files:
94
124
  - lib/seamless_database_pool/connect_timeout.rb
95
125
  - lib/seamless_database_pool/connection_statistics.rb
96
126
  - lib/seamless_database_pool/controller_filter.rb
127
+ - lib/seamless_database_pool/railtie.rb
97
128
  - spec/connection_adapters_spec.rb
98
129
  - spec/connection_statistics_spec.rb
99
130
  - spec/controller_filter_spec.rb
@@ -123,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
123
154
  version: '0'
124
155
  requirements: []
125
156
  rubyforge_project:
126
- rubygems_version: 1.8.11
157
+ rubygems_version: 1.8.25
127
158
  signing_key:
128
159
  specification_version: 3
129
160
  summary: Add support for master/slave database clusters in ActiveRecord to improve