switchman 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +7 -0
  2. data/Rakefile +30 -0
  3. data/app/models/switchman/shard.rb +502 -0
  4. data/db/migrate/20130328212039_create_switchman_shards.rb +9 -0
  5. data/db/migrate/20130328224244_create_default_shard.rb +9 -0
  6. data/lib/switchman.rb +9 -0
  7. data/lib/switchman/active_record/abstract_adapter.rb +11 -0
  8. data/lib/switchman/active_record/association.rb +108 -0
  9. data/lib/switchman/active_record/attribute_methods.rb +104 -0
  10. data/lib/switchman/active_record/base.rb +95 -0
  11. data/lib/switchman/active_record/calculations.rb +63 -0
  12. data/lib/switchman/active_record/connection_handler.rb +147 -0
  13. data/lib/switchman/active_record/connection_pool.rb +117 -0
  14. data/lib/switchman/active_record/finder_methods.rb +25 -0
  15. data/lib/switchman/active_record/log_subscriber.rb +43 -0
  16. data/lib/switchman/active_record/postgresql_adapter.rb +13 -0
  17. data/lib/switchman/active_record/query_cache.rb +12 -0
  18. data/lib/switchman/active_record/query_methods.rb +184 -0
  19. data/lib/switchman/active_record/relation.rb +69 -0
  20. data/lib/switchman/cache_extensions.rb +12 -0
  21. data/lib/switchman/connection_pool_proxy.rb +62 -0
  22. data/lib/switchman/database_server.rb +197 -0
  23. data/lib/switchman/default_shard.rb +28 -0
  24. data/lib/switchman/engine.rb +91 -0
  25. data/lib/switchman/r_spec_helper.rb +124 -0
  26. data/lib/switchman/shackles.rb +34 -0
  27. data/lib/switchman/test_helper.rb +65 -0
  28. data/lib/switchman/version.rb +3 -0
  29. data/spec/dummy/Rakefile +7 -0
  30. data/spec/dummy/app/models/appendage.rb +24 -0
  31. data/spec/dummy/app/models/digit.rb +9 -0
  32. data/spec/dummy/app/models/feature.rb +5 -0
  33. data/spec/dummy/app/models/mirror_user.rb +5 -0
  34. data/spec/dummy/app/models/user.rb +23 -0
  35. data/spec/dummy/config.ru +4 -0
  36. data/spec/dummy/config/application.rb +59 -0
  37. data/spec/dummy/config/boot.rb +10 -0
  38. data/spec/dummy/config/database.yml +17 -0
  39. data/spec/dummy/config/database.yml.example +25 -0
  40. data/spec/dummy/config/environment.rb +5 -0
  41. data/spec/dummy/config/environments/development.rb +37 -0
  42. data/spec/dummy/config/environments/production.rb +67 -0
  43. data/spec/dummy/config/environments/test.rb +37 -0
  44. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  45. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  46. data/spec/dummy/config/initializers/session_store.rb +8 -0
  47. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  48. data/spec/dummy/config/routes.rb +8 -0
  49. data/spec/dummy/db/migrate/20130403132607_create_users.rb +10 -0
  50. data/spec/dummy/db/migrate/20130411202442_create_appendages.rb +10 -0
  51. data/spec/dummy/db/migrate/20130411202551_create_mirror_users.rb +9 -0
  52. data/spec/dummy/db/migrate/20131022202028_create_digits.rb +10 -0
  53. data/spec/dummy/db/migrate/20131206172923_create_features.rb +12 -0
  54. data/spec/dummy/db/schema.rb +57 -0
  55. data/spec/dummy/log/development.log +504 -0
  56. data/spec/dummy/log/test.log +29907 -0
  57. data/spec/dummy/script/rails +6 -0
  58. data/spec/dummy/tmp/cache/2E2/830/shard%2F2 +0 -0
  59. data/spec/dummy/tmp/cache/2E3/840/shard%2F3 +0 -0
  60. data/spec/dummy/tmp/cache/313/970/shard%2F30 +0 -0
  61. data/spec/dummy/tmp/cache/314/980/shard%2F31 +0 -0
  62. data/spec/dummy/tmp/cache/316/980/shard%2F15 +1 -0
  63. data/spec/dummy/tmp/cache/316/9D0/shard%2F60 +0 -0
  64. data/spec/dummy/tmp/cache/317/990/shard%2F16 +0 -0
  65. data/spec/dummy/tmp/cache/317/9C0/shard%2F43 +1 -0
  66. data/spec/dummy/tmp/cache/317/9E0/shard%2F61 +0 -0
  67. data/spec/dummy/tmp/cache/318/9A0/shard%2F17 +0 -0
  68. data/spec/dummy/tmp/cache/318/9D0/shard%2F44 +0 -0
  69. data/spec/dummy/tmp/cache/318/9F0/shard%2F62 +1 -0
  70. data/spec/dummy/tmp/cache/319/9E0/shard%2F45 +0 -0
  71. data/spec/dummy/tmp/cache/319/9F0/shard%2F54 +1 -0
  72. data/spec/dummy/tmp/cache/319/A10/shard%2F72 +1 -0
  73. data/spec/dummy/tmp/cache/319/A30/shard%2F90 +0 -0
  74. data/spec/dummy/tmp/cache/31B/9E0/shard%2F29 +1 -0
  75. data/spec/dummy/tmp/cache/321/AA0/shard%2F89 +0 -0
  76. data/spec/dummy/tmp/cache/322/AC0/shard%2F99 +1 -0
  77. data/spec/dummy/tmp/cache/344/D70/shard%2F103 +1 -0
  78. data/spec/dummy/tmp/cache/345/D80/shard%2F104 +0 -0
  79. data/spec/dummy/tmp/cache/345/DB0/shard%2F131 +1 -0
  80. data/spec/dummy/tmp/cache/345/DC0/shard%2F140 +0 -0
  81. data/spec/dummy/tmp/cache/346/D90/shard%2F105 +0 -0
  82. data/spec/dummy/tmp/cache/346/DB0/shard%2F123 +0 -0
  83. data/spec/dummy/tmp/cache/346/DD0/shard%2F222 +1 -0
  84. data/spec/dummy/tmp/cache/346/DE0/shard%2F150 +0 -0
  85. data/spec/dummy/tmp/cache/346/DF0/shard%2F240 +1 -0
  86. data/spec/dummy/tmp/cache/347/DA0/shard%2F106 +1 -0
  87. data/spec/dummy/tmp/cache/347/DC0/shard%2F124 +0 -0
  88. data/spec/dummy/tmp/cache/347/DC0/shard%2F205 +1 -0
  89. data/spec/dummy/tmp/cache/347/E10/shard%2F250 +1 -0
  90. data/spec/dummy/tmp/cache/348/DF0/shard%2F143 +1 -0
  91. data/spec/dummy/tmp/cache/348/DF0/shard%2F224 +1 -0
  92. data/spec/dummy/tmp/cache/348/E10/shard%2F161 +1 -0
  93. data/spec/dummy/tmp/cache/349/DD0/shard%2F117 +1 -0
  94. data/spec/dummy/tmp/cache/349/E40/shard%2F180 +1 -0
  95. data/spec/dummy/tmp/cache/34A/DF0/shard%2F127 +1 -0
  96. data/spec/dummy/tmp/cache/34A/DF0/shard%2F208 +1 -0
  97. data/spec/dummy/tmp/cache/34A/E10/shard%2F145 +1 -0
  98. data/spec/dummy/tmp/cache/34A/E60/shard%2F190 +1 -0
  99. data/spec/dummy/tmp/cache/34B/E30/shard%2F155 +1 -0
  100. data/spec/dummy/tmp/cache/34D/E30/shard%2F139 +0 -0
  101. data/spec/dummy/tmp/cache/34E/E50/shard%2F149 +0 -0
  102. data/spec/dummy/tmp/cache/353/EF0/shard%2F199 +1 -0
  103. data/spec/dummy/tmp/cache/3A4/E90/shard%2F10003 +1 -0
  104. data/spec/dummy/tmp/cache/3A5/ED0/shard%2F10031 +1 -0
  105. data/spec/dummy/tmp/cache/3A9/EF0/shard%2F10017 +1 -0
  106. data/spec/lib/active_record/association_spec.rb +305 -0
  107. data/spec/lib/active_record/attribute_methods_spec.rb +108 -0
  108. data/spec/lib/active_record/base_spec.rb +66 -0
  109. data/spec/lib/active_record/calculations_spec.rb +119 -0
  110. data/spec/lib/active_record/connection_handler_spec.rb +45 -0
  111. data/spec/lib/active_record/connection_pool_spec.rb +23 -0
  112. data/spec/lib/active_record/finder_methods_spec.rb +29 -0
  113. data/spec/lib/active_record/query_cache_spec.rb +20 -0
  114. data/spec/lib/active_record/query_methods_spec.rb +130 -0
  115. data/spec/lib/active_record/relation_spec.rb +38 -0
  116. data/spec/lib/cache_extensions_spec.rb +27 -0
  117. data/spec/lib/connection_pool_proxy_spec.rb +13 -0
  118. data/spec/lib/database_server_spec.rb +154 -0
  119. data/spec/lib/shackles_spec.rb +147 -0
  120. data/spec/models/shard_spec.rb +382 -0
  121. data/spec/spec_helper.rb +32 -0
  122. 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