ar-octopus-ruby-3 0.11.2

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 (160) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +46 -0
  5. data/.rubocop_todo.yml +56 -0
  6. data/.travis.yml +18 -0
  7. data/Appraisals +16 -0
  8. data/Gemfile +4 -0
  9. data/README.mkdn +257 -0
  10. data/Rakefile +175 -0
  11. data/TODO.txt +7 -0
  12. data/ar-octopus.gemspec +44 -0
  13. data/gemfiles/rails42.gemfile +7 -0
  14. data/gemfiles/rails5.gemfile +7 -0
  15. data/gemfiles/rails51.gemfile +7 -0
  16. data/gemfiles/rails52.gemfile +7 -0
  17. data/lib/ar-octopus.rb +1 -0
  18. data/lib/octopus/abstract_adapter.rb +33 -0
  19. data/lib/octopus/association.rb +14 -0
  20. data/lib/octopus/association_shard_tracking.rb +74 -0
  21. data/lib/octopus/collection_association.rb +17 -0
  22. data/lib/octopus/collection_proxy.rb +16 -0
  23. data/lib/octopus/exception.rb +4 -0
  24. data/lib/octopus/finder_methods.rb +8 -0
  25. data/lib/octopus/load_balancing/round_robin.rb +20 -0
  26. data/lib/octopus/load_balancing.rb +4 -0
  27. data/lib/octopus/log_subscriber.rb +26 -0
  28. data/lib/octopus/migration.rb +236 -0
  29. data/lib/octopus/model.rb +216 -0
  30. data/lib/octopus/persistence.rb +45 -0
  31. data/lib/octopus/proxy.rb +399 -0
  32. data/lib/octopus/proxy_config.rb +251 -0
  33. data/lib/octopus/query_cache_for_shards.rb +24 -0
  34. data/lib/octopus/railtie.rb +11 -0
  35. data/lib/octopus/relation_proxy.rb +74 -0
  36. data/lib/octopus/result_patch.rb +19 -0
  37. data/lib/octopus/scope_proxy.rb +68 -0
  38. data/lib/octopus/shard_tracking/attribute.rb +22 -0
  39. data/lib/octopus/shard_tracking/dynamic.rb +11 -0
  40. data/lib/octopus/shard_tracking.rb +46 -0
  41. data/lib/octopus/singular_association.rb +9 -0
  42. data/lib/octopus/slave_group.rb +13 -0
  43. data/lib/octopus/version.rb +3 -0
  44. data/lib/octopus.rb +209 -0
  45. data/lib/tasks/octopus.rake +16 -0
  46. data/sample_app/.gitignore +4 -0
  47. data/sample_app/.rspec +1 -0
  48. data/sample_app/Gemfile +20 -0
  49. data/sample_app/Gemfile.lock +155 -0
  50. data/sample_app/README +3 -0
  51. data/sample_app/README.rdoc +261 -0
  52. data/sample_app/Rakefile +7 -0
  53. data/sample_app/app/assets/images/rails.png +0 -0
  54. data/sample_app/app/assets/javascripts/application.js +15 -0
  55. data/sample_app/app/assets/stylesheets/application.css +13 -0
  56. data/sample_app/app/controllers/application_controller.rb +4 -0
  57. data/sample_app/app/helpers/application_helper.rb +2 -0
  58. data/sample_app/app/mailers/.gitkeep +0 -0
  59. data/sample_app/app/models/.gitkeep +0 -0
  60. data/sample_app/app/models/item.rb +3 -0
  61. data/sample_app/app/models/user.rb +3 -0
  62. data/sample_app/app/views/layouts/application.html.erb +14 -0
  63. data/sample_app/autotest/discover.rb +2 -0
  64. data/sample_app/config/application.rb +62 -0
  65. data/sample_app/config/boot.rb +6 -0
  66. data/sample_app/config/cucumber.yml +8 -0
  67. data/sample_app/config/database.yml +28 -0
  68. data/sample_app/config/environment.rb +5 -0
  69. data/sample_app/config/environments/development.rb +37 -0
  70. data/sample_app/config/environments/production.rb +67 -0
  71. data/sample_app/config/environments/test.rb +37 -0
  72. data/sample_app/config/initializers/backtrace_silencers.rb +7 -0
  73. data/sample_app/config/initializers/inflections.rb +15 -0
  74. data/sample_app/config/initializers/mime_types.rb +5 -0
  75. data/sample_app/config/initializers/secret_token.rb +7 -0
  76. data/sample_app/config/initializers/session_store.rb +8 -0
  77. data/sample_app/config/initializers/wrap_parameters.rb +14 -0
  78. data/sample_app/config/locales/en.yml +5 -0
  79. data/sample_app/config/routes.rb +58 -0
  80. data/sample_app/config/shards.yml +28 -0
  81. data/sample_app/config.ru +4 -0
  82. data/sample_app/db/migrate/20100720172715_create_users.rb +15 -0
  83. data/sample_app/db/migrate/20100720172730_create_items.rb +16 -0
  84. data/sample_app/db/migrate/20100720210335_create_sample_users.rb +11 -0
  85. data/sample_app/db/schema.rb +29 -0
  86. data/sample_app/db/seeds.rb +16 -0
  87. data/sample_app/doc/README_FOR_APP +2 -0
  88. data/sample_app/features/migrate.feature +45 -0
  89. data/sample_app/features/seed.feature +15 -0
  90. data/sample_app/features/step_definitions/seeds_steps.rb +13 -0
  91. data/sample_app/features/step_definitions/web_steps.rb +218 -0
  92. data/sample_app/features/support/database.rb +13 -0
  93. data/sample_app/features/support/env.rb +57 -0
  94. data/sample_app/features/support/paths.rb +33 -0
  95. data/sample_app/lib/assets/.gitkeep +0 -0
  96. data/sample_app/lib/tasks/.gitkeep +0 -0
  97. data/sample_app/lib/tasks/cucumber.rake +64 -0
  98. data/sample_app/log/.gitkeep +0 -0
  99. data/sample_app/public/404.html +26 -0
  100. data/sample_app/public/422.html +26 -0
  101. data/sample_app/public/500.html +26 -0
  102. data/sample_app/public/favicon.ico +0 -0
  103. data/sample_app/public/images/rails.png +0 -0
  104. data/sample_app/public/index.html +279 -0
  105. data/sample_app/public/javascripts/application.js +2 -0
  106. data/sample_app/public/javascripts/controls.js +965 -0
  107. data/sample_app/public/javascripts/dragdrop.js +974 -0
  108. data/sample_app/public/javascripts/effects.js +1123 -0
  109. data/sample_app/public/javascripts/prototype.js +4874 -0
  110. data/sample_app/public/javascripts/rails.js +118 -0
  111. data/sample_app/public/robots.txt +5 -0
  112. data/sample_app/public/stylesheets/.gitkeep +0 -0
  113. data/sample_app/script/cucumber +10 -0
  114. data/sample_app/script/rails +6 -0
  115. data/sample_app/spec/models/item_spec.rb +5 -0
  116. data/sample_app/spec/models/user_spec.rb +5 -0
  117. data/sample_app/spec/spec_helper.rb +27 -0
  118. data/sample_app/vendor/assets/javascripts/.gitkeep +0 -0
  119. data/sample_app/vendor/assets/stylesheets/.gitkeep +0 -0
  120. data/sample_app/vendor/plugins/.gitkeep +0 -0
  121. data/spec/config/shards.yml +231 -0
  122. data/spec/migrations/10_create_users_using_replication.rb +9 -0
  123. data/spec/migrations/11_add_field_in_all_slaves.rb +11 -0
  124. data/spec/migrations/12_create_users_using_block.rb +23 -0
  125. data/spec/migrations/13_create_users_using_block_and_using.rb +15 -0
  126. data/spec/migrations/14_create_users_on_shards_of_a_group_with_versions.rb +11 -0
  127. data/spec/migrations/15_create_user_on_shards_of_default_group_with_versions.rb +9 -0
  128. data/spec/migrations/1_create_users_on_master.rb +9 -0
  129. data/spec/migrations/2_create_users_on_canada.rb +11 -0
  130. data/spec/migrations/3_create_users_on_both_shards.rb +11 -0
  131. data/spec/migrations/4_create_users_on_shards_of_a_group.rb +11 -0
  132. data/spec/migrations/5_create_users_on_multiples_groups.rb +11 -0
  133. data/spec/migrations/6_raise_exception_with_invalid_shard_name.rb +11 -0
  134. data/spec/migrations/7_raise_exception_with_invalid_multiple_shard_names.rb +11 -0
  135. data/spec/migrations/8_raise_exception_with_invalid_group_name.rb +11 -0
  136. data/spec/migrations/9_raise_exception_with_multiple_invalid_group_names.rb +11 -0
  137. data/spec/octopus/association_shard_tracking_spec.rb +1036 -0
  138. data/spec/octopus/collection_proxy_spec.rb +16 -0
  139. data/spec/octopus/load_balancing/round_robin_spec.rb +15 -0
  140. data/spec/octopus/log_subscriber_spec.rb +19 -0
  141. data/spec/octopus/migration_spec.rb +151 -0
  142. data/spec/octopus/model_spec.rb +837 -0
  143. data/spec/octopus/octopus_spec.rb +123 -0
  144. data/spec/octopus/proxy_spec.rb +303 -0
  145. data/spec/octopus/query_cache_for_shards_spec.rb +40 -0
  146. data/spec/octopus/relation_proxy_spec.rb +132 -0
  147. data/spec/octopus/replicated_slave_grouped_spec.rb +91 -0
  148. data/spec/octopus/replication_spec.rb +196 -0
  149. data/spec/octopus/scope_proxy_spec.rb +97 -0
  150. data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
  151. data/spec/octopus/sharded_spec.rb +33 -0
  152. data/spec/spec_helper.rb +18 -0
  153. data/spec/support/active_record/connection_adapters/modify_config_adapter.rb +15 -0
  154. data/spec/support/database_connection.rb +4 -0
  155. data/spec/support/database_models.rb +118 -0
  156. data/spec/support/octopus_helper.rb +66 -0
  157. data/spec/support/query_count.rb +17 -0
  158. data/spec/support/shared_contexts.rb +18 -0
  159. data/spec/tasks/octopus.rake_spec.rb +32 -0
  160. metadata +351 -0
@@ -0,0 +1,196 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'when the database is replicated' do
4
+ let(:slave_pool) do
5
+ ActiveRecord::Base.connection_proxy.shards['slave1']
6
+ end
7
+
8
+ let(:slave_connection) do
9
+ slave_pool.connection
10
+ end
11
+
12
+ let(:master_pool) do
13
+ ActiveRecord::Base.connection_proxy.shards['master']
14
+ end
15
+
16
+ let(:master_connection) do
17
+ master_pool.connection
18
+ end
19
+
20
+ it 'should send all writes/reads queries to master when you have a non replicated model' do
21
+ OctopusHelper.using_environment :production_replicated do
22
+ u = User.create!(:name => 'Replicated')
23
+ expect(User.count).to eq(1)
24
+ expect(User.find(u.id)).to eq(u)
25
+ end
26
+ end
27
+
28
+ it 'should send all writes queries to master' do
29
+ OctopusHelper.using_environment :production_replicated do
30
+ Cat.create!(:name => 'Slave Cat')
31
+ expect(Cat.find_by_name('Slave Cat')).to be_nil
32
+ Client.create!(:name => 'Slave Client')
33
+ expect(Client.find_by_name('Slave Client')).not_to be_nil
34
+ end
35
+ end
36
+
37
+ it 'should allow to create multiple models on the master' do
38
+ OctopusHelper.using_environment :production_replicated do
39
+ Cat.create!([{ :name => 'Slave Cat 1' }, { :name => 'Slave Cat 2' }])
40
+ expect(Cat.find_by_name('Slave Cat 1')).to be_nil
41
+ expect(Cat.find_by_name('Slave Cat 2')).to be_nil
42
+ end
43
+ end
44
+
45
+ context 'when updating model' do
46
+ it 'should send writes to master' do
47
+ OctopusHelper.using_environment :replicated_with_one_slave do
48
+ Cat.using(:slave1).create!(:name => 'Cat')
49
+ cat = Cat.find_by_name('Cat')
50
+ cat.name = 'New name'
51
+
52
+ expect(master_connection).to receive(:update).and_call_original
53
+
54
+ cat.save!
55
+ end
56
+ end
57
+ end
58
+
59
+ context 'when querying' do
60
+ it 'Reads from slave' do
61
+ OctopusHelper.using_environment :replicated_with_one_slave do
62
+ expect(master_connection).not_to receive(:select)
63
+
64
+ Cat.where(:name => 'Catman2').first
65
+ end
66
+ end
67
+ end
68
+
69
+ context 'When record is read from slave' do
70
+ it 'Should write associations to master' do
71
+ OctopusHelper.using_environment :replicated_with_one_slave do
72
+ client = Client.using(:slave1).create!(:name => 'Client')
73
+
74
+ client = Client.find(client.id)
75
+
76
+ expect(master_connection).to receive(:insert).and_call_original
77
+
78
+ client.items.create!(:name => 'Item')
79
+ end
80
+ end
81
+ end
82
+
83
+
84
+ describe 'When enabling the query cache' do
85
+ include_context 'with query cache enabled' do
86
+ it 'should do the queries with cache' do
87
+ OctopusHelper.using_environment :replicated_with_one_slave do
88
+ cat1 = Cat.using(:master).create!(:name => 'Master Cat 1')
89
+ _ct2 = Cat.using(:master).create!(:name => 'Master Cat 2')
90
+ expect(Cat.using(:master).find(cat1.id)).to eq(cat1)
91
+ expect(Cat.using(:master).find(cat1.id)).to eq(cat1)
92
+ expect(Cat.using(:master).find(cat1.id)).to eq(cat1)
93
+
94
+ cat3 = Cat.using(:slave1).create!(:name => 'Slave Cat 3')
95
+ _ct4 = Cat.using(:slave1).create!(:name => 'Slave Cat 4')
96
+ expect(Cat.find(cat3.id).id).to eq(cat3.id)
97
+ expect(Cat.find(cat3.id).id).to eq(cat3.id)
98
+ expect(Cat.find(cat3.id).id).to eq(cat3.id)
99
+
100
+ # Rails 5.1 count the cached queries as regular queries.
101
+ # TODO: How we can verify if the queries are using cache on Rails 5.1? - @thiagopradi
102
+ expected_records = Octopus.rails51? || Octopus.rails52? ? 19 : 14
103
+
104
+ expect(counter.query_count).to eq(expected_records)
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ describe 'When enabling the query cache with slave unavailable' do
111
+ it "should not raise can't connect error" do
112
+ OctopusHelper.using_environment :replicated_with_one_slave_unavailable do
113
+ expect {
114
+ ActiveRecord::Base.connection.enable_query_cache!
115
+ }.to_not raise_error
116
+ end
117
+ end
118
+ end
119
+
120
+ it 'should allow #using syntax to send queries to master' do
121
+ Cat.create!(:name => 'Master Cat')
122
+
123
+ OctopusHelper.using_environment :production_fully_replicated do
124
+ expect(Cat.using(:master).find_by_name('Master Cat')).not_to be_nil
125
+ end
126
+ end
127
+
128
+ it 'should send the count query to a slave' do
129
+ OctopusHelper.using_environment :production_replicated do
130
+ Cat.create!(:name => 'Slave Cat')
131
+ expect(Cat.count).to eq(0)
132
+ end
133
+ end
134
+
135
+ def active_support_subscribed(callback, *args, &_block)
136
+ subscriber = ActiveSupport::Notifications.subscribe(*args, &callback)
137
+ yield
138
+ ensure
139
+ ActiveSupport::Notifications.unsubscribe(subscriber)
140
+ end
141
+ end
142
+
143
+ describe 'when the database is replicated and the entire application is replicated' do
144
+ before(:each) do
145
+ allow(Octopus).to receive(:env).and_return('production_fully_replicated')
146
+ OctopusHelper.clean_connection_proxy
147
+ end
148
+
149
+ it 'should send all writes queries to master' do
150
+ OctopusHelper.using_environment :production_fully_replicated do
151
+ Cat.create!(:name => 'Slave Cat')
152
+ expect(Cat.find_by_name('Slave Cat')).to be_nil
153
+ Client.create!(:name => 'Slave Client')
154
+ expect(Client.find_by_name('Slave Client')).to be_nil
155
+ end
156
+ end
157
+
158
+ it 'should send all writes queries to master' do
159
+ OctopusHelper.using_environment :production_fully_replicated do
160
+ Cat.create!(:name => 'Slave Cat')
161
+ expect(Cat.find_by_name('Slave Cat')).to be_nil
162
+ Client.create!(:name => 'Slave Client')
163
+ expect(Client.find_by_name('Slave Client')).to be_nil
164
+ end
165
+ end
166
+
167
+ it 'should work with validate_uniquess_of' do
168
+ Keyboard.create!(:name => 'thiago')
169
+
170
+ OctopusHelper.using_environment :production_fully_replicated do
171
+ k = Keyboard.new(:name => 'thiago')
172
+ expect(k.save).to be false
173
+ expect(k.errors.full_messages).to eq(['Name has already been taken'])
174
+ end
175
+ end
176
+
177
+ it 'should reset current shard if slave throws an exception' do
178
+ OctopusHelper.using_environment :production_fully_replicated do
179
+ Cat.create!(:name => 'Slave Cat')
180
+ expect(Cat.connection.current_shard).to eql(:master)
181
+ Cat.where(:rubbish => true)
182
+ expect(Cat.connection.current_shard).to eql(:master)
183
+ end
184
+ end
185
+
186
+ it 'should reset current shard if slave throws an exception with custom master' do
187
+ OctopusHelper.using_environment :production_fully_replicated do
188
+ Octopus.config[:master_shard] = :slave2
189
+ Cat.create!(:name => 'Slave Cat')
190
+ expect(Cat.connection.current_shard).to eql(:slave2)
191
+ Cat.where(:rubbish => true)
192
+ expect(Cat.connection.current_shard).to eql(:slave2)
193
+ Octopus.config[:master_shard] = nil
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopus::ScopeProxy do
4
+ it 'should allow nested queries' do
5
+ @user1 = User.using(:brazil).create!(:name => 'Thiago P', :number => 3)
6
+ @user2 = User.using(:brazil).create!(:name => 'Thiago', :number => 1)
7
+ @user3 = User.using(:brazil).create!(:name => 'Thiago', :number => 2)
8
+
9
+ expect(User.using(:brazil).where(:name => 'Thiago').where(:number => 4).order(:number).all).to eq([])
10
+ expect(User.using(:brazil).where(:name => 'Thiago').using(:canada).where(:number => 2).using(:brazil).order(:number).all).to eq([@user3])
11
+ expect(User.using(:brazil).where(:name => 'Thiago').using(:canada).where(:number => 4).using(:brazil).order(:number).all).to eq([])
12
+ end
13
+
14
+ context 'When array-like-selecting an item in a group' do
15
+ before(:each) do
16
+ User.using(:brazil).create!(:name => 'Evan', :number => 1)
17
+ User.using(:brazil).create!(:name => 'Evan', :number => 2)
18
+ User.using(:brazil).create!(:name => 'Evan', :number => 3)
19
+ @evans = User.using(:brazil).where(:name => 'Evan')
20
+ end
21
+
22
+ it 'allows a block to select an item' do
23
+ expect(@evans.select { |u| u.number == 2 }.first.number).to eq(2)
24
+ end
25
+ end
26
+
27
+ context 'When selecting a field within a scope' do
28
+ before(:each) do
29
+ User.using(:brazil).create!(:name => 'Evan', :number => 4)
30
+ @evan = User.using(:brazil).where(:name => 'Evan')
31
+ end
32
+
33
+ it 'allows single field selection' do
34
+ expect(@evan.select('name').first.name).to eq('Evan')
35
+ end
36
+
37
+ it 'allows selection by array' do
38
+ expect(@evan.select(['name']).first.name).to eq('Evan')
39
+ end
40
+
41
+ it 'allows multiple selection by string' do
42
+ expect(@evan.select('id, name').first.id).to be_a(Fixnum)
43
+ end
44
+
45
+ it 'allows multiple selection by array' do
46
+ expect(@evan.select(%w(id name)).first.id).to be_a(Fixnum)
47
+ end
48
+
49
+ it 'allows multiple selection by symbol' do
50
+ expect(@evan.select(:id, :name).first.id).to be_a(Fixnum)
51
+ end
52
+
53
+ it 'allows multiple selection by string and symbol' do
54
+ expect(@evan.select(:id, 'name').first.id).to be_a(Fixnum)
55
+ end
56
+ end
57
+
58
+ it "should raise a exception when trying to send a query to a shard that don't exists" do
59
+ expect { User.using(:dont_exists).all }.to raise_exception('Nonexistent Shard Name: dont_exists')
60
+ end
61
+
62
+ context "dup / clone" do
63
+ before(:each) do
64
+ User.using(:brazil).create!(:name => 'Thiago', :number => 1)
65
+ end
66
+
67
+ it "should change it's object id" do
68
+ user = User.using(:brazil).where(id: 1)
69
+ dupped_object = user.dup
70
+ cloned_object = user.clone
71
+
72
+ expect(dupped_object.object_id).not_to eq(user.object_id)
73
+ expect(cloned_object.object_id).not_to eq(user.object_id)
74
+ end
75
+ end
76
+
77
+ context 'When iterated with Enumerable methods' do
78
+ before(:each) do
79
+ User.using(:brazil).create!(:name => 'Evan', :number => 1)
80
+ User.using(:brazil).create!(:name => 'Evan', :number => 2)
81
+ User.using(:brazil).create!(:name => 'Evan', :number => 3)
82
+ @evans = User.using(:brazil).where(:name => 'Evan')
83
+ end
84
+
85
+ it 'allows each method' do
86
+ expect(@evans.each.count).to eq(3)
87
+ end
88
+
89
+ it 'allows each_with_index method' do
90
+ expect(@evans.each_with_index.to_a.flatten.count).to eq(6)
91
+ end
92
+
93
+ it 'allows map method' do
94
+ expect(@evans.map(&:number)).to eq([1, 2, 3])
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'when the database is both sharded and replicated' do
4
+
5
+ it 'should pick the shard based on current_shard when you have a sharded model' do
6
+
7
+ OctopusHelper.using_environment :sharded_replicated_slave_grouped do
8
+ Octopus.using(:russia) do
9
+ Cat.create!(:name => 'Thiago1')
10
+ Cat.create!(:name => 'Thiago2')
11
+ end
12
+
13
+ # We must stub here to make it effective (not in the `before(:each)` block)
14
+ allow(Octopus).to receive(:env).and_return('sharded_replicated_slave_grouped')
15
+
16
+ expect(Cat.using(:russia).count).to eq(2)
17
+ # It distributes queries between two slaves in the slave group
18
+ expect(Cat.using(:shard => :russia, :slave_group => :slaves1).count).to eq(0)
19
+ expect(Cat.using(:shard => :russia, :slave_group => :slaves1).count).to eq(2)
20
+ expect(Cat.using(:shard => :russia, :slave_group => :slaves1).count).to eq(0)
21
+ # It distributes queries between two slaves in the slave group
22
+ expect(Cat.using(:shard => :russia, :slave_group => :slaves2).count).to eq(2)
23
+ expect(Cat.using(:shard => :russia, :slave_group => :slaves2).count).to eq(0)
24
+ expect(Cat.using(:shard => :russia, :slave_group => :slaves2).count).to eq(2)
25
+
26
+ expect(Cat.using(:europe).count).to eq(0)
27
+ expect(Cat.using(:shard => :europe, :slave_group => :slaves1)
28
+ .count).to eq(0)
29
+ expect(Cat.using(:shard => :europe, :slave_group => :slaves2)
30
+ .count).to eq(2)
31
+ end
32
+ end
33
+
34
+ it 'should make queries to master when slave groups are configured for the shard but not selected' do
35
+ OctopusHelper.using_environment :sharded_replicated_slave_grouped do
36
+ Octopus.using(:europe) do
37
+ # All the queries go to :master(`octopus_shard_1`)
38
+
39
+ Cat.create!(:name => 'Thiago1')
40
+ Cat.create!(:name => 'Thiago2')
41
+
42
+ # In `database.yml` and `shards.yml`, we have configured 1 master and 6 slaves for `sharded_replicated_slave_grouped`
43
+ # So we can ensure Octopus is not distributing queries between them
44
+ # by asserting 1 + 6 = 7 queries go to :master(`octopus_shard_1`)
45
+ expect(Cat.count).to eq(2)
46
+ expect(Cat.count).to eq(2)
47
+ expect(Cat.count).to eq(2)
48
+ expect(Cat.count).to eq(2)
49
+ expect(Cat.count).to eq(2)
50
+ expect(Cat.count).to eq(2)
51
+ expect(Cat.count).to eq(2)
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,33 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'when the database is not entire sharded' do
4
+ before(:each) do
5
+ allow(Octopus).to receive(:env).and_return('not_entire_sharded')
6
+ OctopusHelper.clean_connection_proxy
7
+ end
8
+
9
+ it 'should not send all queries to the specified slave' do
10
+ skip
11
+ # User.create!(:name => "Thiago")
12
+ #
13
+ # using_environment :not_entire_sharded do
14
+ # Octopus.using(:russia) do
15
+ # User.create!(:name => "Thiago")
16
+ # end
17
+ # end
18
+ #
19
+ # User.count.should == 2
20
+ end
21
+
22
+ it 'should pick the shard based on current_shard when you have a sharded model' do
23
+ Cat.create!(:name => 'Thiago')
24
+
25
+ OctopusHelper.using_environment :not_entire_sharded do
26
+ Octopus.using(:russia) do
27
+ Cat.create!(:name => 'Thiago')
28
+ end
29
+ end
30
+
31
+ expect(Cat.count).to eq(1)
32
+ end
33
+ end
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'pry'
3
+ require 'bundler/setup'
4
+ require 'octopus'
5
+
6
+ Octopus.instance_variable_set(:@directory, File.dirname(__FILE__))
7
+
8
+ BaseOctopusMigrationClass = (Octopus.rails4? ? ActiveRecord::Migration : ActiveRecord::Migration[ActiveRecord::VERSION::STRING[0..2]])
9
+
10
+ # Requires supporting files with custom matchers and macros, etc,
11
+ # in ./support/ and its subdirectories.
12
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
13
+
14
+ RSpec.configure do |config|
15
+ config.before(:each) do |example|
16
+ OctopusHelper.clean_all_shards(example.metadata[:shards])
17
+ end
18
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveRecord
2
+ class Base
3
+ def self.modify_config_connection(config)
4
+ ConnectionAdapters::ModifyConfigAdapter.new(config)
5
+ end
6
+ end
7
+
8
+ module ConnectionAdapters
9
+ class ModifyConfigAdapter < AbstractAdapter
10
+ def initialize(config)
11
+ config.replace(config.symbolize_keys)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,4 @@
1
+ require 'logger'
2
+
3
+ ActiveRecord::Base.establish_connection(:adapter => 'mysql2', :database => 'octopus_shard_1', :username => "#{ENV['MYSQL_USER'] || 'root'}", :password => '')
4
+ ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'a'))
@@ -0,0 +1,118 @@
1
+ # Rails 3.1 needs to do some introspection around the base class, which requires
2
+ # the model be a descendent of ActiveRecord::Base.
3
+ class BlankModel < ActiveRecord::Base; end
4
+
5
+ # The user class is just sharded, not replicated
6
+ class User < ActiveRecord::Base
7
+ scope :thiago, -> { where(:name => 'Thiago') }
8
+
9
+ def awesome_queries
10
+ Octopus.using(:canada) do
11
+ User.create(:name => 'teste')
12
+ end
13
+ end
14
+ end
15
+
16
+ # The client class isn't replicated
17
+ class Client < ActiveRecord::Base
18
+ has_many :items
19
+ has_many :comments, :as => :commentable
20
+ end
21
+
22
+ # This class is replicated
23
+ class Cat < ActiveRecord::Base
24
+ replicated_model
25
+ # sharded_model()
26
+ end
27
+
28
+ # This class sets its own connection
29
+ class CustomConnection < ActiveRecord::Base
30
+ self.table_name = 'custom'
31
+ octopus_establish_connection(:adapter => 'mysql2', :database => 'octopus_shard_2', :username => "#{ENV['MYSQL_USER'] || ''}", :password => '')
32
+ end
33
+
34
+ # This items belongs to a client
35
+ class Item < ActiveRecord::Base
36
+ belongs_to :client
37
+ has_many :parts
38
+ end
39
+
40
+ class Part < ActiveRecord::Base
41
+ belongs_to :item
42
+ end
43
+
44
+ class Keyboard < ActiveRecord::Base
45
+ replicated_model
46
+ validates_uniqueness_of :name
47
+ belongs_to :computer
48
+ end
49
+
50
+ class Computer < ActiveRecord::Base
51
+ has_one :keyboard
52
+ end
53
+
54
+ class Role < ActiveRecord::Base
55
+ has_and_belongs_to_many :permissions
56
+ end
57
+
58
+ class Permission < ActiveRecord::Base
59
+ has_and_belongs_to_many :roles
60
+ end
61
+
62
+ class Assignment < ActiveRecord::Base
63
+ belongs_to :programmer
64
+ belongs_to :project
65
+ end
66
+
67
+ class Programmer < ActiveRecord::Base
68
+ has_many :assignments
69
+ has_many :projects, :through => :assignments
70
+ end
71
+
72
+ class Project < ActiveRecord::Base
73
+ has_many :assignments
74
+ has_many :programmers, :through => :assignments
75
+ end
76
+
77
+ class Comment < ActiveRecord::Base
78
+ belongs_to :commentable, :polymorphic => true
79
+ scope :open, -> { where(open: true) }
80
+ end
81
+
82
+ class Bacon < ActiveRecord::Base
83
+ self.table_name = 'yummy'
84
+ end
85
+
86
+ class Cheese < ActiveRecord::Base
87
+ self.table_name = 'yummy'
88
+ end
89
+
90
+ class Ham < ActiveRecord::Base
91
+ self.table_name = 'yummy'
92
+ end
93
+
94
+ # This class sets its own connection
95
+ class Advert < ActiveRecord::Base
96
+ establish_connection(:adapter => 'postgresql', :database => 'octopus_shard_1', :username => ENV['POSTGRES_USER'] || 'daniel', :password => '1234', :host => '')
97
+ end
98
+
99
+ class MmorpgPlayer < ActiveRecord::Base
100
+ has_many :weapons
101
+ has_many :skills
102
+ end
103
+
104
+ class Weapon < ActiveRecord::Base
105
+ belongs_to :mmorpg_player, :inverse_of => :weapons
106
+ validates :hand, :uniqueness => { :scope => :mmorpg_player_id }
107
+ validates_presence_of :mmorpg_player
108
+ has_many :skills
109
+ end
110
+
111
+ class Skill < ActiveRecord::Base
112
+ belongs_to :weapon, :inverse_of => :skills
113
+ belongs_to :mmorpg_player, :inverse_of => :skills
114
+
115
+ validates_presence_of :weapon
116
+ validates_presence_of :mmorpg_player
117
+ validates :name, :uniqueness => { :scope => :mmorpg_player_id }
118
+ end
@@ -0,0 +1,66 @@
1
+ module OctopusHelper
2
+ def self.clean_all_shards(shards)
3
+ if shards.nil?
4
+ shards = BlankModel.using(:master).connection.shards.keys
5
+ end
6
+
7
+ shards.each do |shard_symbol|
8
+ %w(schema_migrations users clients cats items keyboards computers permissions_roles roles permissions assignments projects programmers yummy adverts).each do |tables|
9
+ BlankModel.using(shard_symbol).connection.execute("DELETE FROM #{tables}")
10
+ end
11
+ if shard_symbol == 'alone_shard'
12
+ %w(mmorpg_players weapons skills).each do |table|
13
+ BlankModel.using(shard_symbol).connection.execute("DELETE FROM #{table}")
14
+ end
15
+ end
16
+ BlankModel.using(:master).connection.shards[shard_symbol].disconnect if Octopus.atleast_rails50?
17
+ end
18
+ end
19
+
20
+ def self.clean_connection_proxy
21
+ Thread.current['octopus.current_model'] = nil
22
+ Thread.current['octopus.current_shard'] = nil
23
+ Thread.current['octopus.current_group'] = nil
24
+ Thread.current['octopus.current_slave_group'] = nil
25
+ Thread.current['octopus.block'] = nil
26
+
27
+ ActiveRecord::Base.class_variable_set(:@@connection_proxy, nil)
28
+ end
29
+
30
+ def self.migrating_to_version(version, &_block)
31
+ migrations_root = File.expand_path(File.join(File.dirname(__FILE__), '..', 'migrations'))
32
+
33
+ begin
34
+ migrate_to_version(:up, migrations_root, version)
35
+ yield
36
+ ensure
37
+ migrate_to_version(:down, migrations_root, version)
38
+ end
39
+ end
40
+
41
+ def self.migrate_to_version(direction, root, version)
42
+
43
+ if Octopus.atleast_rails52?
44
+ migrations = ActiveRecord::MigrationContext.new(root).migrations.select {|mig| version == mig.version }
45
+ ActiveRecord::Migrator.new(direction, migrations, version).run
46
+ else
47
+ schema = ActiveRecord::SchemaMigration
48
+ migrations = ActiveRecord::MigrationContext.new(root, schema).migrations.select {|mig| version == mig.version }
49
+ ActiveRecord::Migrator.new(direction, migrations, schema, version).run
50
+ end
51
+ end
52
+
53
+ def self.using_environment(environment, &_block)
54
+ self.octopus_env = environment.to_s
55
+ clean_connection_proxy
56
+ yield
57
+ ensure
58
+ self.octopus_env = 'octopus'
59
+ clean_connection_proxy
60
+ end
61
+
62
+ def self.octopus_env=(env)
63
+ Octopus.instance_variable_set(:@config, nil)
64
+ Octopus.stub(:env).and_return(env)
65
+ end
66
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveRecord
2
+ class QueryCounter
3
+ attr_accessor :query_count
4
+
5
+ def initialize
6
+ @query_count = 0
7
+ end
8
+
9
+ def to_proc
10
+ lambda(&method(:callback))
11
+ end
12
+
13
+ def callback(_name, _start, _finish, _message_id, values)
14
+ @query_count += 1 unless %w(CACHE SCHEMA).include?(values[:name])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ shared_context 'with query cache enabled' do
2
+ let!(:counter) { ActiveRecord::QueryCounter.new }
3
+
4
+ before(:each) do
5
+ ActiveRecord::Base.connection.enable_query_cache!
6
+ counter.query_count = 0
7
+ end
8
+
9
+ after(:each) do
10
+ ActiveRecord::Base.connection.disable_query_cache!
11
+ end
12
+
13
+ around(:each) do |example|
14
+ active_support_subscribed(counter.to_proc, 'sql.active_record') do
15
+ example.run
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,32 @@
1
+ require 'spec_helper'
2
+ require 'rake'
3
+
4
+ describe 'octopus.rake' do
5
+ before do
6
+ load File.expand_path('../../../lib/tasks/octopus.rake', __FILE__)
7
+ Rake::Task.define_task(:environment)
8
+ end
9
+
10
+ describe 'octopus:copy_schema_versions' do
11
+ class SchemaMigration < ActiveRecord::Base; end
12
+
13
+ before do
14
+ Rake::Task['octopus:copy_schema_versions'].reenable
15
+
16
+ path = File.expand_path('../../migrations', __FILE__)
17
+ ActiveRecord::Migrator.migrations_paths = [path]
18
+ end
19
+
20
+ it 'assumes each shard migrated to the current master version' do
21
+ SchemaMigration.create(:version => 1)
22
+ SchemaMigration.create(:version => 2)
23
+ SchemaMigration.create(:version => 3)
24
+
25
+ Rake::Task['octopus:copy_schema_versions'].invoke
26
+
27
+ ActiveRecord::Base.connection.shard_names.each do |shard_name|
28
+ expect(Octopus.using(shard_name) { ActiveRecord::SchemaMigration.all.pluck(:version).map(&:to_i).sort }).to eq([1, 2, 3])
29
+ end
30
+ end
31
+ end
32
+ end