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.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.rubocop.yml +46 -0
- data/.rubocop_todo.yml +56 -0
- data/.travis.yml +18 -0
- data/Appraisals +16 -0
- data/Gemfile +4 -0
- data/README.mkdn +257 -0
- data/Rakefile +175 -0
- data/TODO.txt +7 -0
- data/ar-octopus.gemspec +44 -0
- data/gemfiles/rails42.gemfile +7 -0
- data/gemfiles/rails5.gemfile +7 -0
- data/gemfiles/rails51.gemfile +7 -0
- data/gemfiles/rails52.gemfile +7 -0
- data/lib/ar-octopus.rb +1 -0
- data/lib/octopus/abstract_adapter.rb +33 -0
- data/lib/octopus/association.rb +14 -0
- data/lib/octopus/association_shard_tracking.rb +74 -0
- data/lib/octopus/collection_association.rb +17 -0
- data/lib/octopus/collection_proxy.rb +16 -0
- data/lib/octopus/exception.rb +4 -0
- data/lib/octopus/finder_methods.rb +8 -0
- data/lib/octopus/load_balancing/round_robin.rb +20 -0
- data/lib/octopus/load_balancing.rb +4 -0
- data/lib/octopus/log_subscriber.rb +26 -0
- data/lib/octopus/migration.rb +236 -0
- data/lib/octopus/model.rb +216 -0
- data/lib/octopus/persistence.rb +45 -0
- data/lib/octopus/proxy.rb +399 -0
- data/lib/octopus/proxy_config.rb +251 -0
- data/lib/octopus/query_cache_for_shards.rb +24 -0
- data/lib/octopus/railtie.rb +11 -0
- data/lib/octopus/relation_proxy.rb +74 -0
- data/lib/octopus/result_patch.rb +19 -0
- data/lib/octopus/scope_proxy.rb +68 -0
- data/lib/octopus/shard_tracking/attribute.rb +22 -0
- data/lib/octopus/shard_tracking/dynamic.rb +11 -0
- data/lib/octopus/shard_tracking.rb +46 -0
- data/lib/octopus/singular_association.rb +9 -0
- data/lib/octopus/slave_group.rb +13 -0
- data/lib/octopus/version.rb +3 -0
- data/lib/octopus.rb +209 -0
- data/lib/tasks/octopus.rake +16 -0
- data/sample_app/.gitignore +4 -0
- data/sample_app/.rspec +1 -0
- data/sample_app/Gemfile +20 -0
- data/sample_app/Gemfile.lock +155 -0
- data/sample_app/README +3 -0
- data/sample_app/README.rdoc +261 -0
- data/sample_app/Rakefile +7 -0
- data/sample_app/app/assets/images/rails.png +0 -0
- data/sample_app/app/assets/javascripts/application.js +15 -0
- data/sample_app/app/assets/stylesheets/application.css +13 -0
- data/sample_app/app/controllers/application_controller.rb +4 -0
- data/sample_app/app/helpers/application_helper.rb +2 -0
- data/sample_app/app/mailers/.gitkeep +0 -0
- data/sample_app/app/models/.gitkeep +0 -0
- data/sample_app/app/models/item.rb +3 -0
- data/sample_app/app/models/user.rb +3 -0
- data/sample_app/app/views/layouts/application.html.erb +14 -0
- data/sample_app/autotest/discover.rb +2 -0
- data/sample_app/config/application.rb +62 -0
- data/sample_app/config/boot.rb +6 -0
- data/sample_app/config/cucumber.yml +8 -0
- data/sample_app/config/database.yml +28 -0
- data/sample_app/config/environment.rb +5 -0
- data/sample_app/config/environments/development.rb +37 -0
- data/sample_app/config/environments/production.rb +67 -0
- data/sample_app/config/environments/test.rb +37 -0
- data/sample_app/config/initializers/backtrace_silencers.rb +7 -0
- data/sample_app/config/initializers/inflections.rb +15 -0
- data/sample_app/config/initializers/mime_types.rb +5 -0
- data/sample_app/config/initializers/secret_token.rb +7 -0
- data/sample_app/config/initializers/session_store.rb +8 -0
- data/sample_app/config/initializers/wrap_parameters.rb +14 -0
- data/sample_app/config/locales/en.yml +5 -0
- data/sample_app/config/routes.rb +58 -0
- data/sample_app/config/shards.yml +28 -0
- data/sample_app/config.ru +4 -0
- data/sample_app/db/migrate/20100720172715_create_users.rb +15 -0
- data/sample_app/db/migrate/20100720172730_create_items.rb +16 -0
- data/sample_app/db/migrate/20100720210335_create_sample_users.rb +11 -0
- data/sample_app/db/schema.rb +29 -0
- data/sample_app/db/seeds.rb +16 -0
- data/sample_app/doc/README_FOR_APP +2 -0
- data/sample_app/features/migrate.feature +45 -0
- data/sample_app/features/seed.feature +15 -0
- data/sample_app/features/step_definitions/seeds_steps.rb +13 -0
- data/sample_app/features/step_definitions/web_steps.rb +218 -0
- data/sample_app/features/support/database.rb +13 -0
- data/sample_app/features/support/env.rb +57 -0
- data/sample_app/features/support/paths.rb +33 -0
- data/sample_app/lib/assets/.gitkeep +0 -0
- data/sample_app/lib/tasks/.gitkeep +0 -0
- data/sample_app/lib/tasks/cucumber.rake +64 -0
- data/sample_app/log/.gitkeep +0 -0
- data/sample_app/public/404.html +26 -0
- data/sample_app/public/422.html +26 -0
- data/sample_app/public/500.html +26 -0
- data/sample_app/public/favicon.ico +0 -0
- data/sample_app/public/images/rails.png +0 -0
- data/sample_app/public/index.html +279 -0
- data/sample_app/public/javascripts/application.js +2 -0
- data/sample_app/public/javascripts/controls.js +965 -0
- data/sample_app/public/javascripts/dragdrop.js +974 -0
- data/sample_app/public/javascripts/effects.js +1123 -0
- data/sample_app/public/javascripts/prototype.js +4874 -0
- data/sample_app/public/javascripts/rails.js +118 -0
- data/sample_app/public/robots.txt +5 -0
- data/sample_app/public/stylesheets/.gitkeep +0 -0
- data/sample_app/script/cucumber +10 -0
- data/sample_app/script/rails +6 -0
- data/sample_app/spec/models/item_spec.rb +5 -0
- data/sample_app/spec/models/user_spec.rb +5 -0
- data/sample_app/spec/spec_helper.rb +27 -0
- data/sample_app/vendor/assets/javascripts/.gitkeep +0 -0
- data/sample_app/vendor/assets/stylesheets/.gitkeep +0 -0
- data/sample_app/vendor/plugins/.gitkeep +0 -0
- data/spec/config/shards.yml +231 -0
- data/spec/migrations/10_create_users_using_replication.rb +9 -0
- data/spec/migrations/11_add_field_in_all_slaves.rb +11 -0
- data/spec/migrations/12_create_users_using_block.rb +23 -0
- data/spec/migrations/13_create_users_using_block_and_using.rb +15 -0
- data/spec/migrations/14_create_users_on_shards_of_a_group_with_versions.rb +11 -0
- data/spec/migrations/15_create_user_on_shards_of_default_group_with_versions.rb +9 -0
- data/spec/migrations/1_create_users_on_master.rb +9 -0
- data/spec/migrations/2_create_users_on_canada.rb +11 -0
- data/spec/migrations/3_create_users_on_both_shards.rb +11 -0
- data/spec/migrations/4_create_users_on_shards_of_a_group.rb +11 -0
- data/spec/migrations/5_create_users_on_multiples_groups.rb +11 -0
- data/spec/migrations/6_raise_exception_with_invalid_shard_name.rb +11 -0
- data/spec/migrations/7_raise_exception_with_invalid_multiple_shard_names.rb +11 -0
- data/spec/migrations/8_raise_exception_with_invalid_group_name.rb +11 -0
- data/spec/migrations/9_raise_exception_with_multiple_invalid_group_names.rb +11 -0
- data/spec/octopus/association_shard_tracking_spec.rb +1036 -0
- data/spec/octopus/collection_proxy_spec.rb +16 -0
- data/spec/octopus/load_balancing/round_robin_spec.rb +15 -0
- data/spec/octopus/log_subscriber_spec.rb +19 -0
- data/spec/octopus/migration_spec.rb +151 -0
- data/spec/octopus/model_spec.rb +837 -0
- data/spec/octopus/octopus_spec.rb +123 -0
- data/spec/octopus/proxy_spec.rb +303 -0
- data/spec/octopus/query_cache_for_shards_spec.rb +40 -0
- data/spec/octopus/relation_proxy_spec.rb +132 -0
- data/spec/octopus/replicated_slave_grouped_spec.rb +91 -0
- data/spec/octopus/replication_spec.rb +196 -0
- data/spec/octopus/scope_proxy_spec.rb +97 -0
- data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
- data/spec/octopus/sharded_spec.rb +33 -0
- data/spec/spec_helper.rb +18 -0
- data/spec/support/active_record/connection_adapters/modify_config_adapter.rb +15 -0
- data/spec/support/database_connection.rb +4 -0
- data/spec/support/database_models.rb +118 -0
- data/spec/support/octopus_helper.rb +66 -0
- data/spec/support/query_count.rb +17 -0
- data/spec/support/shared_contexts.rb +18 -0
- data/spec/tasks/octopus.rake_spec.rb +32 -0
- 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
|
data/spec/spec_helper.rb
ADDED
|
@@ -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,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
|