active_record_shards 2.0.0.beta2 → 2.0.0.beta5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +98 -0
- data/lib/active_record_shards/configuration_parser.rb +1 -0
- data/lib/active_record_shards/connection_switcher.rb +45 -20
- data/lib/active_record_shards/shard_selection.rb +20 -13
- data/lib/active_record_shards/tasks.rb +52 -0
- data/test/configuration_parser_test.rb +40 -35
- data/test/connection_switching_test.rb +95 -1
- data/test/database.yml +18 -0
- data/test/helper.rb +1 -1
- data/test/test.log +43513 -0
- metadata +7 -6
- data/README.rdoc +0 -33
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# ActiveRecord Shards
|
2
|
+
|
3
|
+
ActiveRecord Shards is an extension for ActiveRecord that provides support for sharded database and slaves. Basically it is just a nice way to
|
4
|
+
switch between database connection. We've made the implementation very small, and have tried not to reinvent any wheels already present in ActiveRecord.
|
5
|
+
|
6
|
+
ActiveRecord Shards has only been used and tested on Rails 2.3.x, and has in some form or another been used on in production on a large rails app for
|
7
|
+
more than a year.
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
$ gem install active_record_shards
|
12
|
+
|
13
|
+
and make sure to require 'active\_record\_shards' in some way.
|
14
|
+
|
15
|
+
## Configuration
|
16
|
+
|
17
|
+
Add the slave and shard configuration to config/database.yml:
|
18
|
+
|
19
|
+
production:
|
20
|
+
adapter: mysql
|
21
|
+
encoding: utf8
|
22
|
+
database: my_app_main
|
23
|
+
pool: 5
|
24
|
+
host: db1
|
25
|
+
username: root
|
26
|
+
password:
|
27
|
+
slave:
|
28
|
+
host: db1_slave
|
29
|
+
shards:
|
30
|
+
1:
|
31
|
+
host: db_shard1
|
32
|
+
database: my_app_shard
|
33
|
+
slave:
|
34
|
+
host: db_shard1_slave
|
35
|
+
2:
|
36
|
+
host: db_shard2
|
37
|
+
database: my_app_shard
|
38
|
+
slave:
|
39
|
+
host: db_shard2_slave
|
40
|
+
|
41
|
+
basically connections inherit configuration from the parent the configuration file.
|
42
|
+
|
43
|
+
## Usage
|
44
|
+
|
45
|
+
Normally you have some models that live on a shared database, and you might need to query this data in order to know what shard to switch to.
|
46
|
+
All the model that live on the shared database must be marked as not\_sharded:
|
47
|
+
|
48
|
+
class Account < ActiveRecord::Base
|
49
|
+
not_sharded
|
50
|
+
|
51
|
+
has_many :projects
|
52
|
+
end
|
53
|
+
|
54
|
+
class Project < ActiveRecord::Base
|
55
|
+
belongs_to :account
|
56
|
+
end
|
57
|
+
|
58
|
+
So in this setup the accounts live on the shared database, but the projects are sharded. If accounts had a shard\_id column, you could lookup the account
|
59
|
+
in a rack middleware and switch to the right shard:
|
60
|
+
|
61
|
+
class AccountMiddleware
|
62
|
+
def initialize(app)
|
63
|
+
@app = app
|
64
|
+
end
|
65
|
+
|
66
|
+
def call(env)
|
67
|
+
account = lookup_account(env)
|
68
|
+
|
69
|
+
if account
|
70
|
+
ActiveRecord::Base.on_shard(account.shard_id) do
|
71
|
+
@app.call(env)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
@app.call(env)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def lookup_account(env)
|
79
|
+
...
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
Any where in your app, you can switch to the slave databases, by wrapping you code an on\_slave block:
|
84
|
+
|
85
|
+
ActiveRecord::Base.on_slave do
|
86
|
+
Account.find_by_big_expensive_query
|
87
|
+
end
|
88
|
+
|
89
|
+
This will perform the query on the slave, and mark the returned instances as read only. There is also a shortcut for this:
|
90
|
+
|
91
|
+
Account.on_slave.find_by_big_expensive_query
|
92
|
+
|
93
|
+
## Copyright
|
94
|
+
|
95
|
+
Copyright (c) 2011 Zendesk. See LICENSE for details.
|
96
|
+
|
97
|
+
## Authors
|
98
|
+
Mick Staugaard, Eric Chapweske
|
@@ -6,6 +6,7 @@ module ActiveRecordShards
|
|
6
6
|
conf.keys.each do |env_name|
|
7
7
|
env_config = conf[env_name]
|
8
8
|
if shards = env_config.delete('shards')
|
9
|
+
env_config['shard_names'] = shards.keys
|
9
10
|
shards.each do |shard_name, shard_conf|
|
10
11
|
expand_child!(env_config, shard_conf)
|
11
12
|
conf["#{env_name}_shard_#{shard_name}"] = shard_conf
|
@@ -1,12 +1,30 @@
|
|
1
1
|
module ActiveRecordShards
|
2
2
|
module ConnectionSwitcher
|
3
|
+
def default_shard=(new_default_shard)
|
4
|
+
ActiveRecordShards::ShardSelection.default_shard = new_default_shard
|
5
|
+
switch_connection(:shard => new_default_shard)
|
6
|
+
end
|
3
7
|
|
4
8
|
def on_shard(shard, &block)
|
5
|
-
|
6
|
-
switch_connection(:shard => shard)
|
9
|
+
old_options = current_shard_selection.options
|
10
|
+
switch_connection(:shard => shard) if supports_sharding?
|
7
11
|
yield
|
8
12
|
ensure
|
9
|
-
switch_connection(
|
13
|
+
switch_connection(old_options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_all_shards(&block)
|
17
|
+
old_options = current_shard_selection.options
|
18
|
+
if supports_sharding?
|
19
|
+
shard_names.each do |shard|
|
20
|
+
switch_connection(:shard => shard)
|
21
|
+
yield
|
22
|
+
end
|
23
|
+
else
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
ensure
|
27
|
+
switch_connection(old_options)
|
10
28
|
end
|
11
29
|
|
12
30
|
def on_slave_if(condition, &block)
|
@@ -40,28 +58,26 @@ module ActiveRecordShards
|
|
40
58
|
alias_method :with_slave_unless, :on_slave_unless
|
41
59
|
|
42
60
|
def on_slave_block(&block)
|
43
|
-
|
44
|
-
|
45
|
-
switch_connection(:slave => true)
|
46
|
-
rescue ActiveRecord::AdapterNotSpecified => e
|
47
|
-
switch_connection(:slave => old_slave)
|
48
|
-
logger.warn("Failed to establish shard connection: #{e.message} - defaulting to master")
|
49
|
-
end
|
61
|
+
old_options = current_shard_selection.options
|
62
|
+
switch_connection(:slave => true)
|
50
63
|
with_scope({:find => {:readonly => current_shard_selection.on_slave?}}, :merge, &block)
|
51
64
|
ensure
|
52
|
-
switch_connection(
|
65
|
+
switch_connection(old_options)
|
53
66
|
end
|
54
67
|
|
55
68
|
# Name of the connection pool. Used by ConnectionHandler to retrieve the current connection pool.
|
56
69
|
def connection_pool_name # :nodoc:
|
57
|
-
current_shard_selection.shard_name(self)
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
70
|
+
name = current_shard_selection.shard_name(self)
|
71
|
+
|
72
|
+
if configurations[name].nil? && current_shard_selection.on_slave?
|
73
|
+
current_shard_selection.shard_name(self, false)
|
74
|
+
else
|
75
|
+
name
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def supports_sharding?
|
80
|
+
shard_names.any?
|
65
81
|
end
|
66
82
|
|
67
83
|
private
|
@@ -84,9 +100,18 @@ module ActiveRecordShards
|
|
84
100
|
end
|
85
101
|
end
|
86
102
|
|
103
|
+
def shard_names
|
104
|
+
configurations[shard_env]['shard_names'] || []
|
105
|
+
end
|
106
|
+
|
107
|
+
def shard_env
|
108
|
+
@shard_env = defined?(Rails.env) ? Rails.env : RAILS_ENV
|
109
|
+
end
|
110
|
+
|
87
111
|
def establish_shard_connection
|
88
|
-
name =
|
112
|
+
name = connection_pool_name
|
89
113
|
spec = configurations[name]
|
114
|
+
|
90
115
|
raise ActiveRecord::AdapterNotSpecified.new("No database defined by #{name} in database.yml") if spec.nil?
|
91
116
|
|
92
117
|
connection_handler.establish_connection(connection_pool_name, ActiveRecord::Base::ConnectionSpecification.new(spec, "#{spec['adapter']}_connection"))
|
@@ -1,15 +1,24 @@
|
|
1
1
|
module ActiveRecordShards
|
2
2
|
class ShardSelection
|
3
|
+
NO_SHARD = :_no_shard
|
4
|
+
cattr_accessor :default_shard
|
5
|
+
|
3
6
|
def initialize
|
4
7
|
@on_slave = false
|
5
8
|
end
|
6
9
|
|
7
|
-
def shard
|
8
|
-
@shard
|
10
|
+
def shard(klass = nil)
|
11
|
+
if (@shard || self.class.default_shard) && (klass.nil? || klass.is_sharded?)
|
12
|
+
if @shard == NO_SHARD
|
13
|
+
nil
|
14
|
+
else
|
15
|
+
(@shard || self.class.default_shard).to_s
|
16
|
+
end
|
17
|
+
end
|
9
18
|
end
|
10
19
|
|
11
20
|
def shard=(new_shard)
|
12
|
-
@shard = new_shard
|
21
|
+
@shard = (new_shard || NO_SHARD)
|
13
22
|
end
|
14
23
|
|
15
24
|
def on_slave?
|
@@ -20,22 +29,20 @@ module ActiveRecordShards
|
|
20
29
|
@on_slave = (new_slave == true)
|
21
30
|
end
|
22
31
|
|
23
|
-
def
|
24
|
-
shard_name#(RAILS_ENV)
|
25
|
-
end
|
26
|
-
|
27
|
-
def shard_name(klass = nil)
|
32
|
+
def shard_name(klass = nil, try_slave = true)
|
28
33
|
s = "#{RAILS_ENV}"
|
29
|
-
if
|
34
|
+
if the_shard = shard(klass)
|
30
35
|
s << '_shard_'
|
31
|
-
s <<
|
36
|
+
s << the_shard
|
37
|
+
end
|
38
|
+
if @on_slave && try_slave
|
39
|
+
s << "_slave" if @on_slave
|
32
40
|
end
|
33
|
-
s << "_slave" if @on_slave
|
34
41
|
s
|
35
42
|
end
|
36
43
|
|
37
|
-
def
|
38
|
-
@
|
44
|
+
def options
|
45
|
+
{:shard => @shard, :slave => @on_slave}
|
39
46
|
end
|
40
47
|
end
|
41
48
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_record_shards'
|
2
|
+
|
3
|
+
Rake::TaskManager.class_eval do
|
4
|
+
def remove_task(task_name)
|
5
|
+
@tasks.delete(task_name.to_s)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
def remove_task(task_name)
|
10
|
+
Rake.application.remove_task(task_name)
|
11
|
+
end
|
12
|
+
|
13
|
+
remove_task 'db:drop'
|
14
|
+
remove_task 'db:create'
|
15
|
+
remove_task 'db:abort_if_pending_migrations'
|
16
|
+
|
17
|
+
namespace :db do
|
18
|
+
desc 'Drops the database for the current RAILS_ENV including shards and slaves'
|
19
|
+
task :drop => :load_config do
|
20
|
+
env_name = defined?(Rails.env) ? Rails.env : RAILS_ENV || 'development'
|
21
|
+
ActiveRecord::Base.configurations.each do |key, conf|
|
22
|
+
if key.starts_with?(env_name) && !key.ends_with?("_slave")
|
23
|
+
drop_database(conf)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Create the database defined in config/database.yml for the current RAILS_ENV including shards and slaves"
|
29
|
+
task :create => :load_config do
|
30
|
+
env_name = defined?(Rails.env) ? Rails.env : RAILS_ENV || 'development'
|
31
|
+
ActiveRecord::Base.configurations.each do |key, conf|
|
32
|
+
if key.starts_with?(env_name) && !key.ends_with?("_slave")
|
33
|
+
create_database(conf)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
desc "Raises an error if there are pending migrations"
|
39
|
+
task :abort_if_pending_migrations => :environment do
|
40
|
+
if defined? ActiveRecord
|
41
|
+
pending_migrations = ActiveRecord::Base.on_shard(nil) { ActiveRecord::Migrator.new(:up, 'db/migrate').pending_migrations }
|
42
|
+
|
43
|
+
if pending_migrations.any?
|
44
|
+
puts "You have #{pending_migrations.size} pending migrations:"
|
45
|
+
pending_migrations.each do |pending_migration|
|
46
|
+
puts ' %4d %s' % [pending_migration.version, pending_migration.name]
|
47
|
+
end
|
48
|
+
abort %{Run "rake db:migrate" to update your database then try again.}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -10,13 +10,14 @@ class ConfigurationParserTest < ActiveSupport::TestCase
|
|
10
10
|
setup { @conf = @exploded_conf['test_slave'] }
|
11
11
|
should "be exploded" do
|
12
12
|
assert_equal({
|
13
|
-
"adapter"
|
14
|
-
"encoding"
|
15
|
-
"database"
|
16
|
-
"port"
|
17
|
-
"username"
|
18
|
-
"password"
|
19
|
-
"host"
|
13
|
+
"adapter" => "mysql",
|
14
|
+
"encoding" => "utf8",
|
15
|
+
"database" => "ars_test",
|
16
|
+
"port" => 123,
|
17
|
+
"username" => "root",
|
18
|
+
"password" => nil,
|
19
|
+
"host" => "main_slave_host",
|
20
|
+
"shard_names" => ["a", "b"]
|
20
21
|
}, @conf)
|
21
22
|
end
|
22
23
|
end
|
@@ -26,13 +27,14 @@ class ConfigurationParserTest < ActiveSupport::TestCase
|
|
26
27
|
setup { @conf = @exploded_conf['test_shard_a'] }
|
27
28
|
should "be exploded" do
|
28
29
|
assert_equal({
|
29
|
-
"adapter"
|
30
|
-
"encoding"
|
31
|
-
"database"
|
32
|
-
"port"
|
33
|
-
"username"
|
34
|
-
"password"
|
35
|
-
"host"
|
30
|
+
"adapter" => "mysql",
|
31
|
+
"encoding" => "utf8",
|
32
|
+
"database" => "ars_test_shard_a",
|
33
|
+
"port" => 123,
|
34
|
+
"username" => "root",
|
35
|
+
"password" => nil,
|
36
|
+
"host" => "shard_a_host",
|
37
|
+
"shard_names" => ["a", "b"]
|
36
38
|
}, @conf)
|
37
39
|
end
|
38
40
|
end
|
@@ -41,13 +43,14 @@ class ConfigurationParserTest < ActiveSupport::TestCase
|
|
41
43
|
setup { @conf = @exploded_conf['test_shard_a_slave'] }
|
42
44
|
should "be exploded" do
|
43
45
|
assert_equal({
|
44
|
-
"adapter"
|
45
|
-
"encoding"
|
46
|
-
"database"
|
47
|
-
"port"
|
48
|
-
"username"
|
49
|
-
"password"
|
50
|
-
"host"
|
46
|
+
"adapter" => "mysql",
|
47
|
+
"encoding" => "utf8",
|
48
|
+
"database" => "ars_test_shard_a",
|
49
|
+
"port" => 123,
|
50
|
+
"username" => "root",
|
51
|
+
"password" => nil,
|
52
|
+
"host" => "shard_a_slave_host",
|
53
|
+
"shard_names" => ["a", "b"]
|
51
54
|
}, @conf)
|
52
55
|
end
|
53
56
|
end
|
@@ -58,13 +61,14 @@ class ConfigurationParserTest < ActiveSupport::TestCase
|
|
58
61
|
setup { @conf = @exploded_conf['test_shard_b'] }
|
59
62
|
should "be exploded" do
|
60
63
|
assert_equal({
|
61
|
-
"adapter"
|
62
|
-
"encoding"
|
63
|
-
"database"
|
64
|
-
"port"
|
65
|
-
"username"
|
66
|
-
"password"
|
67
|
-
"host"
|
64
|
+
"adapter" => "mysql",
|
65
|
+
"encoding" => "utf8",
|
66
|
+
"database" => "ars_test_shard_b",
|
67
|
+
"port" => 123,
|
68
|
+
"username" => "root",
|
69
|
+
"password" => nil,
|
70
|
+
"host" => "shard_b_host",
|
71
|
+
"shard_names" => ["a", "b"]
|
68
72
|
}, @conf)
|
69
73
|
end
|
70
74
|
end
|
@@ -73,13 +77,14 @@ class ConfigurationParserTest < ActiveSupport::TestCase
|
|
73
77
|
setup { @conf = @exploded_conf['test_shard_b_slave'] }
|
74
78
|
should "be exploded" do
|
75
79
|
assert_equal({
|
76
|
-
"adapter"
|
77
|
-
"encoding"
|
78
|
-
"database"
|
79
|
-
"port"
|
80
|
-
"username"
|
81
|
-
"password"
|
82
|
-
"host"
|
80
|
+
"adapter" => "mysql",
|
81
|
+
"encoding" => "utf8",
|
82
|
+
"database" => "ars_test_shard_b_slave",
|
83
|
+
"port" => 123,
|
84
|
+
"username" => "root",
|
85
|
+
"password" => nil,
|
86
|
+
"host" => "shard_b_host",
|
87
|
+
"shard_names" => ["a", "b"]
|
83
88
|
}, @conf)
|
84
89
|
end
|
85
90
|
end
|
@@ -32,8 +32,102 @@ class ConnectionSwitchenTest < ActiveSupport::TestCase
|
|
32
32
|
assert_using_database('ars_test')
|
33
33
|
ActiveRecord::Base.on_slave { assert_using_database('ars_test_slave') }
|
34
34
|
end
|
35
|
+
|
36
|
+
context "on_all_shards" do
|
37
|
+
setup do
|
38
|
+
@shard_0_master = ActiveRecord::Base.on_shard(0) {ActiveRecord::Base.connection}
|
39
|
+
@shard_1_master = ActiveRecord::Base.on_shard(1) {ActiveRecord::Base.connection}
|
40
|
+
assert_not_equal(@shard_0_master.select_value("SELECT DATABASE()"), @shard_1_master.select_value("SELECT DATABASE()"))
|
41
|
+
|
42
|
+
@database_names = []
|
43
|
+
ActiveRecord::Base.on_all_shards do
|
44
|
+
@database_names << ActiveRecord::Base.connection.select_value("SELECT DATABASE()")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
should "execute the block on all shard masters" do
|
49
|
+
assert_equal(2, @database_names.size)
|
50
|
+
assert_contains(@database_names, @shard_0_master.select_value("SELECT DATABASE()"))
|
51
|
+
assert_contains(@database_names, @shard_1_master.select_value("SELECT DATABASE()"))
|
52
|
+
end
|
53
|
+
end
|
35
54
|
end
|
36
|
-
|
55
|
+
|
56
|
+
context "default shard selection" do
|
57
|
+
context "of nil" do
|
58
|
+
setup do
|
59
|
+
ActiveRecord::Base.default_shard = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
should "use unsharded db for sharded models" do
|
63
|
+
assert_using_database('ars_test', Ticket)
|
64
|
+
assert_using_database('ars_test', Account)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
context "value" do
|
69
|
+
setup do
|
70
|
+
ActiveRecord::Base.default_shard = 0
|
71
|
+
end
|
72
|
+
|
73
|
+
teardown do
|
74
|
+
ActiveRecord::Base.default_shard = nil
|
75
|
+
end
|
76
|
+
|
77
|
+
should "use default shard db for sharded models" do
|
78
|
+
assert_using_database('ars_test_shard0', Ticket)
|
79
|
+
assert_using_database('ars_test', Account)
|
80
|
+
end
|
81
|
+
|
82
|
+
should "still be able to switch to shard nil" do
|
83
|
+
ActiveRecord::Base.on_shard(nil) do
|
84
|
+
assert_using_database('ars_test', Ticket)
|
85
|
+
assert_using_database('ars_test', Account)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
context "in an unsharded environment" do
|
92
|
+
setup do
|
93
|
+
silence_warnings { ::RAILS_ENV = 'test2' }
|
94
|
+
ActiveRecord::Base.establish_connection(::RAILS_ENV)
|
95
|
+
assert_using_database('ars_test2', Ticket)
|
96
|
+
end
|
97
|
+
|
98
|
+
teardown do
|
99
|
+
silence_warnings { ::RAILS_ENV = 'test' }
|
100
|
+
ActiveRecord::Base.establish_connection(::RAILS_ENV)
|
101
|
+
assert_using_database('ars_test', Ticket)
|
102
|
+
end
|
103
|
+
|
104
|
+
context "shard switching" do
|
105
|
+
should "just stay on the main db" do
|
106
|
+
assert_using_database('ars_test2', Ticket)
|
107
|
+
assert_using_database('ars_test2', Account)
|
108
|
+
|
109
|
+
ActiveRecord::Base.on_shard(0) do
|
110
|
+
assert_using_database('ars_test2', Ticket)
|
111
|
+
assert_using_database('ars_test2', Account)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context "on_all_shards" do
|
117
|
+
setup do
|
118
|
+
@database_names = []
|
119
|
+
ActiveRecord::Base.on_all_shards do
|
120
|
+
@database_names << ActiveRecord::Base.connection.select_value("SELECT DATABASE()")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
should "execute the block on all shard masters" do
|
125
|
+
@database_names
|
126
|
+
assert_equal([ActiveRecord::Base.connection.select_value("SELECT DATABASE()")], @database_names)
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
37
131
|
context "slave driving" do
|
38
132
|
context "without slave configuration" do
|
39
133
|
|