switchman 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/Rakefile +30 -0
- data/app/models/switchman/shard.rb +502 -0
- data/db/migrate/20130328212039_create_switchman_shards.rb +9 -0
- data/db/migrate/20130328224244_create_default_shard.rb +9 -0
- data/lib/switchman.rb +9 -0
- data/lib/switchman/active_record/abstract_adapter.rb +11 -0
- data/lib/switchman/active_record/association.rb +108 -0
- data/lib/switchman/active_record/attribute_methods.rb +104 -0
- data/lib/switchman/active_record/base.rb +95 -0
- data/lib/switchman/active_record/calculations.rb +63 -0
- data/lib/switchman/active_record/connection_handler.rb +147 -0
- data/lib/switchman/active_record/connection_pool.rb +117 -0
- data/lib/switchman/active_record/finder_methods.rb +25 -0
- data/lib/switchman/active_record/log_subscriber.rb +43 -0
- data/lib/switchman/active_record/postgresql_adapter.rb +13 -0
- data/lib/switchman/active_record/query_cache.rb +12 -0
- data/lib/switchman/active_record/query_methods.rb +184 -0
- data/lib/switchman/active_record/relation.rb +69 -0
- data/lib/switchman/cache_extensions.rb +12 -0
- data/lib/switchman/connection_pool_proxy.rb +62 -0
- data/lib/switchman/database_server.rb +197 -0
- data/lib/switchman/default_shard.rb +28 -0
- data/lib/switchman/engine.rb +91 -0
- data/lib/switchman/r_spec_helper.rb +124 -0
- data/lib/switchman/shackles.rb +34 -0
- data/lib/switchman/test_helper.rb +65 -0
- data/lib/switchman/version.rb +3 -0
- data/spec/dummy/Rakefile +7 -0
- data/spec/dummy/app/models/appendage.rb +24 -0
- data/spec/dummy/app/models/digit.rb +9 -0
- data/spec/dummy/app/models/feature.rb +5 -0
- data/spec/dummy/app/models/mirror_user.rb +5 -0
- data/spec/dummy/app/models/user.rb +23 -0
- data/spec/dummy/config.ru +4 -0
- data/spec/dummy/config/application.rb +59 -0
- data/spec/dummy/config/boot.rb +10 -0
- data/spec/dummy/config/database.yml +17 -0
- data/spec/dummy/config/database.yml.example +25 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +37 -0
- data/spec/dummy/config/environments/production.rb +67 -0
- data/spec/dummy/config/environments/test.rb +37 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
- data/spec/dummy/config/initializers/secret_token.rb +7 -0
- data/spec/dummy/config/initializers/session_store.rb +8 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/routes.rb +8 -0
- data/spec/dummy/db/migrate/20130403132607_create_users.rb +10 -0
- data/spec/dummy/db/migrate/20130411202442_create_appendages.rb +10 -0
- data/spec/dummy/db/migrate/20130411202551_create_mirror_users.rb +9 -0
- data/spec/dummy/db/migrate/20131022202028_create_digits.rb +10 -0
- data/spec/dummy/db/migrate/20131206172923_create_features.rb +12 -0
- data/spec/dummy/db/schema.rb +57 -0
- data/spec/dummy/log/development.log +504 -0
- data/spec/dummy/log/test.log +29907 -0
- data/spec/dummy/script/rails +6 -0
- data/spec/dummy/tmp/cache/2E2/830/shard%2F2 +0 -0
- data/spec/dummy/tmp/cache/2E3/840/shard%2F3 +0 -0
- data/spec/dummy/tmp/cache/313/970/shard%2F30 +0 -0
- data/spec/dummy/tmp/cache/314/980/shard%2F31 +0 -0
- data/spec/dummy/tmp/cache/316/980/shard%2F15 +1 -0
- data/spec/dummy/tmp/cache/316/9D0/shard%2F60 +0 -0
- data/spec/dummy/tmp/cache/317/990/shard%2F16 +0 -0
- data/spec/dummy/tmp/cache/317/9C0/shard%2F43 +1 -0
- data/spec/dummy/tmp/cache/317/9E0/shard%2F61 +0 -0
- data/spec/dummy/tmp/cache/318/9A0/shard%2F17 +0 -0
- data/spec/dummy/tmp/cache/318/9D0/shard%2F44 +0 -0
- data/spec/dummy/tmp/cache/318/9F0/shard%2F62 +1 -0
- data/spec/dummy/tmp/cache/319/9E0/shard%2F45 +0 -0
- data/spec/dummy/tmp/cache/319/9F0/shard%2F54 +1 -0
- data/spec/dummy/tmp/cache/319/A10/shard%2F72 +1 -0
- data/spec/dummy/tmp/cache/319/A30/shard%2F90 +0 -0
- data/spec/dummy/tmp/cache/31B/9E0/shard%2F29 +1 -0
- data/spec/dummy/tmp/cache/321/AA0/shard%2F89 +0 -0
- data/spec/dummy/tmp/cache/322/AC0/shard%2F99 +1 -0
- data/spec/dummy/tmp/cache/344/D70/shard%2F103 +1 -0
- data/spec/dummy/tmp/cache/345/D80/shard%2F104 +0 -0
- data/spec/dummy/tmp/cache/345/DB0/shard%2F131 +1 -0
- data/spec/dummy/tmp/cache/345/DC0/shard%2F140 +0 -0
- data/spec/dummy/tmp/cache/346/D90/shard%2F105 +0 -0
- data/spec/dummy/tmp/cache/346/DB0/shard%2F123 +0 -0
- data/spec/dummy/tmp/cache/346/DD0/shard%2F222 +1 -0
- data/spec/dummy/tmp/cache/346/DE0/shard%2F150 +0 -0
- data/spec/dummy/tmp/cache/346/DF0/shard%2F240 +1 -0
- data/spec/dummy/tmp/cache/347/DA0/shard%2F106 +1 -0
- data/spec/dummy/tmp/cache/347/DC0/shard%2F124 +0 -0
- data/spec/dummy/tmp/cache/347/DC0/shard%2F205 +1 -0
- data/spec/dummy/tmp/cache/347/E10/shard%2F250 +1 -0
- data/spec/dummy/tmp/cache/348/DF0/shard%2F143 +1 -0
- data/spec/dummy/tmp/cache/348/DF0/shard%2F224 +1 -0
- data/spec/dummy/tmp/cache/348/E10/shard%2F161 +1 -0
- data/spec/dummy/tmp/cache/349/DD0/shard%2F117 +1 -0
- data/spec/dummy/tmp/cache/349/E40/shard%2F180 +1 -0
- data/spec/dummy/tmp/cache/34A/DF0/shard%2F127 +1 -0
- data/spec/dummy/tmp/cache/34A/DF0/shard%2F208 +1 -0
- data/spec/dummy/tmp/cache/34A/E10/shard%2F145 +1 -0
- data/spec/dummy/tmp/cache/34A/E60/shard%2F190 +1 -0
- data/spec/dummy/tmp/cache/34B/E30/shard%2F155 +1 -0
- data/spec/dummy/tmp/cache/34D/E30/shard%2F139 +0 -0
- data/spec/dummy/tmp/cache/34E/E50/shard%2F149 +0 -0
- data/spec/dummy/tmp/cache/353/EF0/shard%2F199 +1 -0
- data/spec/dummy/tmp/cache/3A4/E90/shard%2F10003 +1 -0
- data/spec/dummy/tmp/cache/3A5/ED0/shard%2F10031 +1 -0
- data/spec/dummy/tmp/cache/3A9/EF0/shard%2F10017 +1 -0
- data/spec/lib/active_record/association_spec.rb +305 -0
- data/spec/lib/active_record/attribute_methods_spec.rb +108 -0
- data/spec/lib/active_record/base_spec.rb +66 -0
- data/spec/lib/active_record/calculations_spec.rb +119 -0
- data/spec/lib/active_record/connection_handler_spec.rb +45 -0
- data/spec/lib/active_record/connection_pool_spec.rb +23 -0
- data/spec/lib/active_record/finder_methods_spec.rb +29 -0
- data/spec/lib/active_record/query_cache_spec.rb +20 -0
- data/spec/lib/active_record/query_methods_spec.rb +130 -0
- data/spec/lib/active_record/relation_spec.rb +38 -0
- data/spec/lib/cache_extensions_spec.rb +27 -0
- data/spec/lib/connection_pool_proxy_spec.rb +13 -0
- data/spec/lib/database_server_spec.rb +154 -0
- data/spec/lib/shackles_spec.rb +147 -0
- data/spec/models/shard_spec.rb +382 -0
- data/spec/spec_helper.rb +32 -0
- metadata +344 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
module ActiveRecord
|
5
|
+
describe Relation do
|
6
|
+
include RSpecHelper
|
7
|
+
|
8
|
+
before do
|
9
|
+
@user1 = User.create!(:name => 'user1')
|
10
|
+
@user2 = @shard1.activate { User.create!(:name => 'user2') }
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#exec_queries" do
|
14
|
+
it "should activate the correct shard for the query" do
|
15
|
+
User.shard(@shard1).where(:id => @user2.local_id).first.should == @user2
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should activate multiple shards if necessary" do
|
19
|
+
User.where(:id => [@user1.id, @user2.id]).all.sort_by(&:id).should == [@user1, @user2].sort_by(&:id)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#update_all" do
|
24
|
+
it "should activate the correct shard for the query" do
|
25
|
+
User.shard(@shard1).where(:id => @user2.local_id).update_all(:name => 'a')
|
26
|
+
@user1.reload.name.should == 'user1'
|
27
|
+
@user2.reload.name.should == 'a'
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should activate multiple shards if necessary" do
|
31
|
+
User.where(:id => [@user1.id, @user2.id]).update_all(:name => 'a')
|
32
|
+
@user1.reload.name.should == 'a'
|
33
|
+
@user2.reload.name.should == 'a'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
describe CacheExtensions do
|
5
|
+
it "should automatically isolate cache keys from different shards" do
|
6
|
+
cache = ActiveSupport::Cache::MemoryStore.new
|
7
|
+
Rails.stubs(:cache_without_sharding).returns(cache)
|
8
|
+
db = DatabaseServer.create(:settings => { :adapter => 'sqlite3' })
|
9
|
+
s1 = db.shards.create!
|
10
|
+
s2 = db.shards.create!
|
11
|
+
|
12
|
+
s1.activate { Rails.cache }.should == s2.activate { Rails.cache }
|
13
|
+
|
14
|
+
from_1 = s1.activate { Rails.cache.fetch('key') { 1 } }
|
15
|
+
from_1.should == 1
|
16
|
+
from_2 = s2.activate do
|
17
|
+
Rails.cache.fetch('key') { 2 }
|
18
|
+
end
|
19
|
+
from_2.should == 2
|
20
|
+
|
21
|
+
from_1 = s1.activate { Rails.cache.fetch('key') }
|
22
|
+
from_1.should == 1
|
23
|
+
from_2 = s2.activate { Rails.cache.fetch('key') }
|
24
|
+
from_2.should == 2
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
describe ConnectionPoolProxy do
|
5
|
+
it "should not share connections for sqlite shards on the same db" do
|
6
|
+
@db = DatabaseServer.create(:config => { :adapter => 'sqlite3', :database => ':memory:' })
|
7
|
+
@sqlite_shard1 = @db.shards.create!
|
8
|
+
@sqlite_shard2 = @db.shards.create!
|
9
|
+
::ActiveRecord::Base.connection.should_not == @sqlite_shard2.activate { ::ActiveRecord::Base.connection }
|
10
|
+
@sqlite_shard1.activate { ::ActiveRecord::Base.connection }.should_not == @sqlite_shard2.activate { ::ActiveRecord::Base.connection }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,154 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
describe DatabaseServer do
|
5
|
+
describe "shareable?" do
|
6
|
+
it "should be false for sqlite" do
|
7
|
+
db = DatabaseServer.new
|
8
|
+
db.config = { :adapter => 'sqlite3', :database => '%{shard_name}' }
|
9
|
+
db.shareable?.should be_false
|
10
|
+
end
|
11
|
+
|
12
|
+
it "should be true for mysql" do
|
13
|
+
db = DatabaseServer.new
|
14
|
+
db.config = { :adapter => 'mysql' }
|
15
|
+
db.shareable?.should be_true
|
16
|
+
|
17
|
+
db = DatabaseServer.new
|
18
|
+
db.config = { :adapter => 'mysql2' }
|
19
|
+
db.shareable?.should be_true
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should be true for postgres with a non-variable username" do
|
23
|
+
db = DatabaseServer.new
|
24
|
+
db.config = { :adapter => 'postgresql' }
|
25
|
+
db.shareable?.should be_true
|
26
|
+
end
|
27
|
+
|
28
|
+
it "should be false for postgres with variable username" do
|
29
|
+
db = DatabaseServer.new
|
30
|
+
db.config = { :adapter => 'postgresql', :username => '%{schema_search_path}' }
|
31
|
+
db.shareable?.should be_false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should depend on the database environment" do
|
35
|
+
db = DatabaseServer.new
|
36
|
+
db.config = { :adapter => 'postgresql', :username => '%{schema_search_path}', :deploy => { :username => 'deploy' } }
|
37
|
+
db.shareable?.should be_false
|
38
|
+
::Shackles.activate(:deploy) { db.shareable? }.should be_true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "#create_new_shard" do
|
43
|
+
def maybe_activate(shard)
|
44
|
+
shard.activate { yield } if shard
|
45
|
+
yield unless shard
|
46
|
+
end
|
47
|
+
|
48
|
+
adapter = ::ActiveRecord::Base.connection.adapter_name
|
49
|
+
def create_shard(server)
|
50
|
+
new_shard = server.create_new_shard
|
51
|
+
new_shard.should_not be_new_record
|
52
|
+
new_shard.name.should match /shard_#{new_shard.id}/
|
53
|
+
# They should share a connection pool
|
54
|
+
if server == Shard.default.database_server
|
55
|
+
User.connection_pool.current_pool.should == new_shard.activate { User.connection_pool.current_pool }
|
56
|
+
User.connection_pool.current_pool.should == Shard.connection_pool.current_pool
|
57
|
+
else
|
58
|
+
User.connection_pool.current_pool.should_not == new_shard.activate { User.connection_pool.current_pool }
|
59
|
+
end
|
60
|
+
# The tables should be created, ready to use
|
61
|
+
new_shard.activate {
|
62
|
+
a = User.create!
|
63
|
+
a.should_not be_new_record
|
64
|
+
}
|
65
|
+
ensure
|
66
|
+
if new_shard
|
67
|
+
new_shard.drop_database
|
68
|
+
new_shard.destroy
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should be able to create a new sqlite shard from a given server" do
|
73
|
+
@db = DatabaseServer.create(:config => { :adapter => 'sqlite3', :database => '%{shard_name}', :shard_name => ':memory:' })
|
74
|
+
create_shard(@db)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should be able to create a new shard from the default db" do
|
78
|
+
create_shard(Shard.default.database_server)
|
79
|
+
end
|
80
|
+
|
81
|
+
it "should be able to create a new shard from a db server that doesn't have any shards" do
|
82
|
+
# otherwise it's a repeat of the sqlite spec above
|
83
|
+
pending 'A "real" database"' unless %w{MySQL Mysql2 PostgreSQL}.include?(adapter)
|
84
|
+
|
85
|
+
# So, it's really the same server, but we want separate connections
|
86
|
+
server = DatabaseServer.create(:config => Shard.default.database_server.config)
|
87
|
+
create_shard(server)
|
88
|
+
end
|
89
|
+
|
90
|
+
class MyException < Exception; end
|
91
|
+
it "should use the connection's db name as temp db name" do
|
92
|
+
db = DatabaseServer.new
|
93
|
+
db.config = { :adapter => 'postgresql' }
|
94
|
+
Shard.expects(:create!).with(:name => Shard.default.name, :database_server => db).raises(MyException.new)
|
95
|
+
lambda { db.create_new_shard }.should raise_error(MyException)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe ".server_for_new_shard" do
|
100
|
+
before(:all) do
|
101
|
+
@db1 = DatabaseServer.find(nil)
|
102
|
+
@old_open = @db1.config.delete(:open)
|
103
|
+
@old_servers = DatabaseServer.all
|
104
|
+
@old_servers.delete(@db1)
|
105
|
+
@old_servers.each do |db|
|
106
|
+
db.destroy unless db == @db1
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
before do
|
111
|
+
@db1.config.delete(:open)
|
112
|
+
end
|
113
|
+
|
114
|
+
after do
|
115
|
+
@db2.try(:destroy)
|
116
|
+
end
|
117
|
+
|
118
|
+
after(:all) do
|
119
|
+
@db1.config[:open] = @old_open
|
120
|
+
@old_servers.each do |db|
|
121
|
+
DatabaseServer.create(:id => db.id, :config => db.config)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it "should return the default server if that's the only one around" do
|
126
|
+
DatabaseServer.server_for_new_shard.should == @db1
|
127
|
+
end
|
128
|
+
|
129
|
+
it "should return on open server" do
|
130
|
+
@db1.config[:open] = true
|
131
|
+
DatabaseServer.server_for_new_shard.should == @db1
|
132
|
+
end
|
133
|
+
|
134
|
+
it "should return another server if it's the only one open" do
|
135
|
+
@db2 = DatabaseServer.create(:config => { :open => true})
|
136
|
+
4.times { DatabaseServer.server_for_new_shard.should == @db2 }
|
137
|
+
@db2.config.delete(:open)
|
138
|
+
@db1.config[:open] = true
|
139
|
+
4.times { DatabaseServer.server_for_new_shard.should == @db1 }
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should return multiple open servers" do
|
143
|
+
@db2 = DatabaseServer.create(:config => { :open => true })
|
144
|
+
@db1.config[:open] = true
|
145
|
+
dbs = []
|
146
|
+
20.times do
|
147
|
+
dbs << DatabaseServer.server_for_new_shard
|
148
|
+
end
|
149
|
+
dbs.should include(@db1)
|
150
|
+
dbs.should include(@db2)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,147 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
describe Shackles do
|
5
|
+
include RSpecHelper
|
6
|
+
|
7
|
+
before do
|
8
|
+
#!!! trick Shackles in to actually switching envs
|
9
|
+
Rails.env.stubs(:test?).returns(false)
|
10
|
+
|
11
|
+
# be sure to test bugs where the current env isn't yet included in this hash
|
12
|
+
::Shackles.connection_handlers.clear
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should call ensure_handler when switching envs" do
|
16
|
+
old_handler = ::ActiveRecord::Base.connection_handler
|
17
|
+
::Shackles.expects(:ensure_handler).returns(old_handler).twice
|
18
|
+
::Shackles.activate(:slave) {}
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should capture the correct current_pool" do
|
22
|
+
# use @shard2 cause it has its own DatabaseServer
|
23
|
+
@shard2.activate do
|
24
|
+
@current_pool = ::Shackles.activate(:slave) { ::ActiveRecord::Base.connection_pool.current_pool }
|
25
|
+
end
|
26
|
+
::Shackles.activate(:slave) do
|
27
|
+
::ActiveRecord::Base.connection_pool.default_pool.should_not == @current_pool
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should correctly set up pools for sharding categories" do
|
32
|
+
@default_pools = ::ActiveRecord::Base.connection_handler.connection_pools
|
33
|
+
::Shackles.activate(:slave_that_no_one_else_uses) do
|
34
|
+
pools = ::ActiveRecord::Base.connection_handler.connection_pools
|
35
|
+
@default_pools.keys.sort.should == pools.keys.sort
|
36
|
+
@default_pools.keys.each do |model|
|
37
|
+
@default_pools[model].should_not == pools[model]
|
38
|
+
end
|
39
|
+
# should have the same number of distinct default_pools
|
40
|
+
pools.values.map(&:default_pool).uniq.length.should == @default_pools.values.map(&:default_pool).uniq.length
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "non-transactional" do
|
45
|
+
self.use_transactional_fixtures = false
|
46
|
+
|
47
|
+
it "should really disconnect all envs" do
|
48
|
+
::ActiveRecord::Base.connection
|
49
|
+
::ActiveRecord::Base.connection_pool.should be_connected
|
50
|
+
@shard1.activate do
|
51
|
+
::ActiveRecord::Base.connection
|
52
|
+
::ActiveRecord::Base.connection_pool.should be_connected
|
53
|
+
end
|
54
|
+
@shard2.activate do
|
55
|
+
::ActiveRecord::Base.connection
|
56
|
+
::ActiveRecord::Base.connection_pool.should be_connected
|
57
|
+
end
|
58
|
+
|
59
|
+
::Shackles.activate(:slave) do
|
60
|
+
::ActiveRecord::Base.connection
|
61
|
+
::ActiveRecord::Base.connection_pool.should be_connected
|
62
|
+
@shard1.activate do
|
63
|
+
::ActiveRecord::Base.connection
|
64
|
+
::ActiveRecord::Base.connection_pool.should be_connected
|
65
|
+
end
|
66
|
+
@shard2.activate do
|
67
|
+
::ActiveRecord::Base.connection
|
68
|
+
::ActiveRecord::Base.connection_pool.should be_connected
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
::ActiveRecord::Base.clear_all_connections!
|
73
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
74
|
+
@shard1.activate do
|
75
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
76
|
+
end
|
77
|
+
@shard2.activate do
|
78
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
79
|
+
end
|
80
|
+
::Shackles.activate(:slave) do
|
81
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
82
|
+
@shard1.activate do
|
83
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
84
|
+
end
|
85
|
+
@shard2.activate do
|
86
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def actual_connection_count
|
92
|
+
::ActiveRecord::Base.connection_pool.current_pool.instance_variable_get(:@reserved_connections).length
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should really return active connections to the pool in all envs" do
|
96
|
+
::ActiveRecord::Base.connection
|
97
|
+
actual_connection_count.should_not == 0
|
98
|
+
@shard1.activate do
|
99
|
+
::ActiveRecord::Base.connection
|
100
|
+
actual_connection_count.should_not == 0
|
101
|
+
end
|
102
|
+
@shard2.activate do
|
103
|
+
::ActiveRecord::Base.connection
|
104
|
+
actual_connection_count.should_not == 0
|
105
|
+
end
|
106
|
+
|
107
|
+
::Shackles.activate(:slave) do
|
108
|
+
::ActiveRecord::Base.connection
|
109
|
+
actual_connection_count.should_not == 0
|
110
|
+
@shard1.activate do
|
111
|
+
::ActiveRecord::Base.connection
|
112
|
+
actual_connection_count.should_not == 0
|
113
|
+
end
|
114
|
+
@shard2.activate do
|
115
|
+
::ActiveRecord::Base.connection
|
116
|
+
actual_connection_count.should_not == 0
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
::ActiveRecord::Base.clear_active_connections!
|
121
|
+
actual_connection_count.should == 0
|
122
|
+
@shard1.activate do
|
123
|
+
actual_connection_count.should == 0
|
124
|
+
end
|
125
|
+
@shard2.activate do
|
126
|
+
actual_connection_count.should == 0
|
127
|
+
end
|
128
|
+
::Shackles.activate(:slave) do
|
129
|
+
actual_connection_count.should == 0
|
130
|
+
@shard1.activate do
|
131
|
+
actual_connection_count.should == 0
|
132
|
+
end
|
133
|
+
@shard2.activate do
|
134
|
+
actual_connection_count.should == 0
|
135
|
+
end
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
it "should not establish connections when switching environments" do
|
140
|
+
::ActiveRecord::Base.clear_all_connections!
|
141
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
142
|
+
::Shackles.activate(:slave) {}
|
143
|
+
::ActiveRecord::Base.connection_pool.should_not be_connected
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,382 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module Switchman
|
4
|
+
describe Shard do
|
5
|
+
include RSpecHelper
|
6
|
+
|
7
|
+
describe ".activate" do
|
8
|
+
it "should activate a hash of shard categories" do
|
9
|
+
Shard.current.should == Shard.default
|
10
|
+
Shard.current(:other).should == Shard.default
|
11
|
+
Shard.activate(:default => @shard1, :other => @shard2) do
|
12
|
+
Shard.current.should == @shard1
|
13
|
+
Shard.current(:other).should == @shard2
|
14
|
+
end
|
15
|
+
Shard.current.should == Shard.default
|
16
|
+
Shard.current(:other).should == Shard.default
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should not allow activating the unsharded category" do
|
20
|
+
Shard.current(:unsharded).should == Shard.default
|
21
|
+
Shard.activate(:unsharded => @shard1) do
|
22
|
+
Shard.current(:unsharded).should == Shard.default
|
23
|
+
end
|
24
|
+
Shard.current(:unsharded).should == Shard.default
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#activate" do
|
29
|
+
it "should activate the default category when no args are used" do
|
30
|
+
Shard.current.should == Shard.default
|
31
|
+
@shard1.activate do
|
32
|
+
Shard.current.should == @shard1
|
33
|
+
end
|
34
|
+
Shard.current.should == Shard.default
|
35
|
+
end
|
36
|
+
|
37
|
+
it "should activate other categories" do
|
38
|
+
Shard.current(:other).should == Shard.default
|
39
|
+
@shard1.activate(:other) do
|
40
|
+
Shard.current(:other).should == @shard1
|
41
|
+
Shard.current.should == Shard.default
|
42
|
+
end
|
43
|
+
Shard.current(:other).should == Shard.default
|
44
|
+
end
|
45
|
+
|
46
|
+
it "should activate multiple categories" do
|
47
|
+
Shard.current.should == Shard.default
|
48
|
+
Shard.current(:other).should == Shard.default
|
49
|
+
@shard1.activate(:default, :other) do
|
50
|
+
Shard.current.should == @shard1
|
51
|
+
Shard.current(:other).should == @shard1
|
52
|
+
end
|
53
|
+
Shard.current.should == Shard.default
|
54
|
+
Shard.current(:other).should == Shard.default
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
describe "#shard" do
|
59
|
+
it "should return the default shard if the instance variable is not set" do
|
60
|
+
# i.e. the instance var would not be set if we got this back from a cache
|
61
|
+
# that was populated pre-sharding
|
62
|
+
a = User.new
|
63
|
+
a.shard.should == Shard.default
|
64
|
+
a.instance_variable_set(:@shard, nil)
|
65
|
+
a.shard.should == Shard.default
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
describe "#drop_database" do
|
70
|
+
it "should work" do
|
71
|
+
# use a separate connection so we don't commit the transaction
|
72
|
+
server = DatabaseServer.create(:config => Shard.default.database_server.config)
|
73
|
+
shard = server.create_new_shard
|
74
|
+
shard.activate do
|
75
|
+
User.create!
|
76
|
+
User.count.should == 1
|
77
|
+
end
|
78
|
+
shard.drop_database
|
79
|
+
shard.activate do
|
80
|
+
lambda { User.count }.should raise_error
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe ".lookup" do
|
86
|
+
it "should work with pseudo-ids" do
|
87
|
+
Shard.lookup('default').should == Shard.default
|
88
|
+
Shard.lookup('self').should == Shard.current
|
89
|
+
@shard1.activate do
|
90
|
+
Shard.lookup('default').should == Shard.default
|
91
|
+
Shard.lookup('self').should == Shard.current
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
it "should work with string ids" do
|
96
|
+
Shard.lookup(Shard.current.id.to_s).should == Shard.current
|
97
|
+
Shard.lookup(@shard1.id.to_s).should == @shard1
|
98
|
+
end
|
99
|
+
|
100
|
+
it "should raise an error for non-ids" do
|
101
|
+
lambda { Shard.lookup('jacob') }.should raise_error(ArgumentError)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe ".with_each_shard" do
|
106
|
+
context "non-transactional" do
|
107
|
+
self.use_transactional_fixtures = false
|
108
|
+
|
109
|
+
it "should disconnect when switching among different database servers" do
|
110
|
+
User.connection
|
111
|
+
User.connected?.should be_true
|
112
|
+
Shard.with_each_shard([Shard.default, @shard2]) {}
|
113
|
+
User.connected?.should be_false
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should not disconnect when it's the current shard" do
|
117
|
+
User.connection
|
118
|
+
User.connected?.should be_true
|
119
|
+
Shard.with_each_shard([Shard.default]) {}
|
120
|
+
User.connected?.should be_true
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should not disconnect for zero shards" do
|
124
|
+
User.connection
|
125
|
+
User.connected?.should be_true
|
126
|
+
Shard.with_each_shard([]) {}
|
127
|
+
User.connected?.should be_true
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe ".partition_by_shard" do
|
133
|
+
it "should work" do
|
134
|
+
ids = [2, 48, Shard::IDS_PER_SHARD * @shard1.id + 6, Shard::IDS_PER_SHARD * @shard1.id + 8, 10, 12]
|
135
|
+
results = Shard.partition_by_shard(ids) do |ids|
|
136
|
+
(ids.length == 4 || ids.length == 2).should be_true
|
137
|
+
ids.map { |id| id + 1}
|
138
|
+
end
|
139
|
+
|
140
|
+
# could have done either shard first, but we can't sort, because we want to see the shards grouped together
|
141
|
+
(results == [3, 49, 11, 13, 7, 9] ||
|
142
|
+
results == [7, 9, 3, 49, 11, 13]).should be_true
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should work for a partition_proc that returns a shard" do
|
146
|
+
array = [{:id => 1, :shard => @shard1}, {:id => 2, :shard => @shard2}]
|
147
|
+
results = Shard.partition_by_shard(array, lambda { |a| a[:shard] }) do |objects|
|
148
|
+
objects.length.should == 1
|
149
|
+
Shard.current.should == objects.first[:shard]
|
150
|
+
objects.first[:id]
|
151
|
+
end
|
152
|
+
results.sort.should == [1, 2]
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should support shortened id syntax, and strings" do
|
156
|
+
ids = [@shard1.global_id_for(1), "#{@shard2.id}~2"]
|
157
|
+
result = Shard.partition_by_shard(ids) do |ids|
|
158
|
+
ids.length.should == 1
|
159
|
+
[@shard1, @shard2].include?(Shard.current).should be_true
|
160
|
+
ids.first.should == 1 if Shard.current == @shard1
|
161
|
+
ids.first.should == 2 if Shard.current == @shard2
|
162
|
+
ids.first
|
163
|
+
end
|
164
|
+
result.sort.should == [1, 2]
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should partition unrecognized types unchanged into current shard" do
|
168
|
+
expected_shard = Shard.current
|
169
|
+
items = [:symbol, Object.new]
|
170
|
+
result = Shard.partition_by_shard(items) do |shard_items|
|
171
|
+
[Shard.current, shard_items]
|
172
|
+
end
|
173
|
+
result.should == [expected_shard, items]
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should partition unrecognized strings unchanged into current shard" do
|
177
|
+
expected_shard = Shard.current
|
178
|
+
items = ["not an id", "something other than an id"]
|
179
|
+
result = Shard.partition_by_shard(items) do |shard_items|
|
180
|
+
[Shard.current, shard_items]
|
181
|
+
end
|
182
|
+
result.should == [expected_shard, items]
|
183
|
+
end
|
184
|
+
|
185
|
+
it "should partition recognized ids with an invalid shard unchanged into current shard" do
|
186
|
+
expected_shard = Shard.current
|
187
|
+
bad_shard_id = @shard2.id + 10000
|
188
|
+
items = ["#{bad_shard_id}~1", Shard::IDS_PER_SHARD * bad_shard_id + 1]
|
189
|
+
result = Shard.partition_by_shard(items) do |shard_items|
|
190
|
+
[Shard.current, shard_items]
|
191
|
+
end
|
192
|
+
result.should == [expected_shard, items]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
describe "#name" do
|
197
|
+
it "the default shard should not be marked as dirty after reading its name" do
|
198
|
+
s = Shard.default
|
199
|
+
s.should_not be_new_record
|
200
|
+
s.name
|
201
|
+
s.should_not be_changed
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should fall back to shard_name in the config if nil" do
|
205
|
+
db = DatabaseServer.new
|
206
|
+
db.config = { :adapter => 'mysql', :database => 'canvas', :shard_name => 'yoyoyo' }
|
207
|
+
shard = Shard.new(:database_server => db)
|
208
|
+
shard.name.should == 'yoyoyo'
|
209
|
+
end
|
210
|
+
|
211
|
+
it "should fall back to the database_server if nil" do
|
212
|
+
db = DatabaseServer.new
|
213
|
+
db.config = { :adapter => 'mysql', :database => 'canvas' }
|
214
|
+
shard = Shard.new(:database_server => db)
|
215
|
+
shard.name.should == 'canvas'
|
216
|
+
end
|
217
|
+
|
218
|
+
it "should get it from the postgres connection if not otherwise specified" do
|
219
|
+
db = DatabaseServer.create
|
220
|
+
db.config = { :adapter => 'postgresql', :database => 'notme' }
|
221
|
+
shard = Shard.new(:database_server => db)
|
222
|
+
shard.database_server = db
|
223
|
+
connection = mock()
|
224
|
+
connection.stubs(:open_transactions).returns(0)
|
225
|
+
connection.expects(:schemas).returns(['canvas', 'public']).once
|
226
|
+
connection.expects(:schema_search_path=).with(nil).once
|
227
|
+
connection.stubs(:shard).returns(Shard.default)
|
228
|
+
connection.expects(:shard=).with(shard)
|
229
|
+
connection.stubs(:adapter_name).returns('PostgreSQL')
|
230
|
+
connection.stubs(:run_callbacks).returns(nil)
|
231
|
+
::ActiveRecord::ConnectionAdapters::ConnectionPool.any_instance.stubs(:checkout).returns(connection)
|
232
|
+
begin
|
233
|
+
shard.name.should == 'canvas'
|
234
|
+
ensure
|
235
|
+
shard.activate { ::ActiveRecord::Base.connection_pool.current_pool.disconnect! }
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
describe ".shard_for" do
|
241
|
+
it "should work" do
|
242
|
+
Shard.shard_for(1).should == Shard.default
|
243
|
+
Shard.shard_for(1, @shard1).should == @shard1
|
244
|
+
Shard.shard_for(@shard1.global_id_for(1)).should == @shard1
|
245
|
+
Shard.shard_for(Shard.default.global_id_for(1)).should == Shard.default
|
246
|
+
Shard.shard_for(@shard1.global_id_for(1), @shard1).should == @shard1
|
247
|
+
Shard.shard_for(Shard.default.global_id_for(1), @shard1).should == Shard.default
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
describe ".local_id_for" do
|
252
|
+
it "should recognize shortened string ids" do
|
253
|
+
expected_id = 1
|
254
|
+
expected_shard = @shard2
|
255
|
+
id, shard = Shard.local_id_for("#{expected_shard.id}~#{expected_id}")
|
256
|
+
id.should == expected_id
|
257
|
+
shard.should == expected_shard
|
258
|
+
end
|
259
|
+
|
260
|
+
it "should recognize global ids" do
|
261
|
+
expected_id = 1
|
262
|
+
expected_shard = @shard2
|
263
|
+
id, shard = Shard.local_id_for(Shard::IDS_PER_SHARD * expected_shard.id + expected_id)
|
264
|
+
id.should == expected_id
|
265
|
+
shard.should == expected_shard
|
266
|
+
end
|
267
|
+
|
268
|
+
it "should recognize local ids with no shard" do
|
269
|
+
expected_id = 1
|
270
|
+
id, shard = Shard.local_id_for(expected_id)
|
271
|
+
id.should == expected_id
|
272
|
+
shard.should be_nil
|
273
|
+
end
|
274
|
+
|
275
|
+
it "should return nil for unrecognized input" do
|
276
|
+
id, shard = Shard.local_id_for("not an id")
|
277
|
+
id.should be_nil
|
278
|
+
shard.should be_nil
|
279
|
+
end
|
280
|
+
|
281
|
+
it "should return nil for ids with bad shard values" do
|
282
|
+
bad_shard_id = @shard2.id + 10000
|
283
|
+
id, shard = Shard.local_id_for("#{bad_shard_id}~1")
|
284
|
+
id.should be_nil
|
285
|
+
shard.should be_nil
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
context "id translation" do
|
290
|
+
before do
|
291
|
+
@local_id = 1
|
292
|
+
@global_id = Shard::IDS_PER_SHARD * @shard1.id + @local_id
|
293
|
+
end
|
294
|
+
|
295
|
+
describe ".integral_id" do
|
296
|
+
it "should return recognized ids" do
|
297
|
+
Shard.integral_id_for(@local_id).should == @local_id
|
298
|
+
Shard.integral_id_for(@local_id.to_s).should == @local_id
|
299
|
+
Shard.integral_id_for(@global_id).should == @global_id
|
300
|
+
Shard.integral_id_for(@global_id.to_s).should == @global_id
|
301
|
+
Shard.integral_id_for("#{@shard1.id}~#{@local_id}").should == @global_id
|
302
|
+
end
|
303
|
+
|
304
|
+
it "should work even for shards that don't exist" do
|
305
|
+
shard = Shard.create!
|
306
|
+
shard.destroy
|
307
|
+
global_id = shard.global_id_for(1)
|
308
|
+
Shard.integral_id_for(global_id).should == global_id
|
309
|
+
Shard.integral_id_for(global_id.to_s).should == global_id
|
310
|
+
Shard.integral_id_for("#{shard.id}~1").should == global_id
|
311
|
+
end
|
312
|
+
|
313
|
+
it "should return nil for unrecognized ids" do
|
314
|
+
Shard.integral_id_for('not an id').should == nil
|
315
|
+
end
|
316
|
+
end
|
317
|
+
|
318
|
+
describe ".local_id_for" do
|
319
|
+
it "should return id without shard for local id" do
|
320
|
+
Shard.local_id_for(@local_id).should == [@local_id, nil]
|
321
|
+
end
|
322
|
+
|
323
|
+
it "should return id with shard for global id" do
|
324
|
+
Shard.local_id_for(@global_id).should == [@local_id, @shard1]
|
325
|
+
end
|
326
|
+
|
327
|
+
it "should return nil for shards that don't exist" do
|
328
|
+
shard = Shard.create!
|
329
|
+
shard.destroy
|
330
|
+
Shard.local_id_for(shard.global_id_for(1)).should == [nil, nil]
|
331
|
+
end
|
332
|
+
|
333
|
+
it "should return nil for unrecognized ids" do
|
334
|
+
Shard.local_id_for('not an id').should == [nil, nil]
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
describe ".relative_id_for" do
|
339
|
+
it "should return recognized ids relative to the target shard" do
|
340
|
+
Shard.relative_id_for(@local_id, @shard1, @shard2).should == @global_id
|
341
|
+
Shard.relative_id_for(@local_id, @shard2, @shard2).should == @local_id
|
342
|
+
Shard.relative_id_for(@global_id, @shard1, @shard2).should == @global_id
|
343
|
+
Shard.relative_id_for(@global_id, @shard2, @shard2).should == @global_id
|
344
|
+
end
|
345
|
+
|
346
|
+
it "should return the original id for unrecognized ids" do
|
347
|
+
Shard.relative_id_for('not an id', @shard1, @shard2).should == 'not an id'
|
348
|
+
end
|
349
|
+
end
|
350
|
+
|
351
|
+
describe ".short_id_for" do
|
352
|
+
it "should return shorted strings for global ids" do
|
353
|
+
Shard.short_id_for(@local_id).should == @local_id
|
354
|
+
Shard.short_id_for("#{@local_id}").should == @local_id
|
355
|
+
Shard.short_id_for(@global_id).should == "#{@shard1.id}~#{@local_id}"
|
356
|
+
end
|
357
|
+
|
358
|
+
it "should return the original id for unrecognized ids" do
|
359
|
+
Shard.short_id_for('not an id').should == 'not an id'
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
describe ".global_id_for" do
|
364
|
+
it "should return the provided id if already global" do
|
365
|
+
local_id = 5
|
366
|
+
Shard.with_each_shard do
|
367
|
+
global_id = Shard.current.global_id_for(local_id)
|
368
|
+
Shard.global_id_for(global_id).should == global_id
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
it "should treat local ids as local to the current shard" do
|
373
|
+
local_id = 5
|
374
|
+
Shard.with_each_shard do
|
375
|
+
next if Shard.current == Shard.default
|
376
|
+
Shard.shard_for(Shard.global_id_for(local_id)).should == Shard.current
|
377
|
+
end
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
end
|
382
|
+
end
|