switchman 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|