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.
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