switch_connection 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +26 -0
- data/.hound.yml +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +63 -0
- data/.rubocop_todo.yml +2 -0
- data/.ruby-version +1 -0
- data/.travis.yml +22 -0
- data/Appraisals +66 -0
- data/CHANGELOG.md +73 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +143 -0
- data/Rakefile +16 -0
- data/assets/switch_point.svg +2 -0
- data/benchmark/proxy.rb +71 -0
- data/gemfiles/rails_3.2.gemfile +8 -0
- data/gemfiles/rails_4.0.gemfile +8 -0
- data/gemfiles/rails_4.1.gemfile +8 -0
- data/gemfiles/rails_4.2.gemfile +9 -0
- data/gemfiles/rails_5.0.gemfile +9 -0
- data/gemfiles/rails_5.1.gemfile +8 -0
- data/gemfiles/rails_5.2.gemfile +9 -0
- data/gemfiles/rails_edge.gemfile +9 -0
- data/lib/switch_connection.rb +66 -0
- data/lib/switch_connection/config.rb +94 -0
- data/lib/switch_connection/error.rb +12 -0
- data/lib/switch_connection/model.rb +144 -0
- data/lib/switch_connection/proxy.rb +165 -0
- data/lib/switch_connection/proxy_repository.rb +30 -0
- data/lib/switch_connection/version.rb +5 -0
- data/spec/models.rb +141 -0
- data/spec/spec_helper.rb +91 -0
- data/spec/switch_connection/model_spec.rb +402 -0
- data/spec/switch_connection_spec.rb +108 -0
- data/switch_connection.gemspec +36 -0
- metadata +265 -0
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'switch_connection/error'
|
4
|
+
|
5
|
+
module SwitchConnection
|
6
|
+
class Proxy
|
7
|
+
attr_reader :initial_name
|
8
|
+
|
9
|
+
AVAILABLE_MODES = %i[master slave].freeze
|
10
|
+
DEFAULT_MODE = :master
|
11
|
+
|
12
|
+
def initialize(name)
|
13
|
+
@initial_name = name
|
14
|
+
@current_name = name
|
15
|
+
define_master_model(name)
|
16
|
+
define_slave_model(name)
|
17
|
+
@global_mode = DEFAULT_MODE
|
18
|
+
end
|
19
|
+
|
20
|
+
def define_master_model(name)
|
21
|
+
model_name = SwitchConnection.config.master_model_name(name)
|
22
|
+
if model_name
|
23
|
+
model = Class.new(ActiveRecord::Base)
|
24
|
+
Proxy.const_set(model_name, model)
|
25
|
+
master_database_name = SwitchConnection.config.master_database_name(name)
|
26
|
+
model.establish_connection(db_specific(master_database_name))
|
27
|
+
model
|
28
|
+
else
|
29
|
+
ActiveRecord::Base
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def define_slave_model(name)
|
34
|
+
return unless SwitchConnection.config.slave_exist?(name)
|
35
|
+
|
36
|
+
slave_count = SwitchConnection.config.slave_count(name)
|
37
|
+
(0..(slave_count - 1)).map do |index|
|
38
|
+
model_name = SwitchConnection.config.slave_mode_name(name, index)
|
39
|
+
next ActiveRecord::Base unless model_name
|
40
|
+
|
41
|
+
model = Class.new(ActiveRecord::Base)
|
42
|
+
Proxy.const_set(model_name, model)
|
43
|
+
model.establish_connection(db_specific(SwitchConnection.config.slave_database_name(name, index)))
|
44
|
+
model
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def db_specific(db_name)
|
49
|
+
base_config = ::ActiveRecord::Base.configurations.fetch(SwitchConnection.config.env)
|
50
|
+
return base_config if db_name == :default
|
51
|
+
|
52
|
+
db_name.to_s.split('.').inject(base_config) { |h, n| h[n] }
|
53
|
+
end
|
54
|
+
|
55
|
+
def thread_local_mode
|
56
|
+
Thread.current[:"switch_point_#{@current_name}_mode"]
|
57
|
+
end
|
58
|
+
|
59
|
+
def thread_local_mode=(mode)
|
60
|
+
Thread.current[:"switch_point_#{@current_name}_mode"] = mode
|
61
|
+
end
|
62
|
+
private :thread_local_mode=
|
63
|
+
|
64
|
+
def mode
|
65
|
+
thread_local_mode || @global_mode
|
66
|
+
end
|
67
|
+
|
68
|
+
def slave!
|
69
|
+
if thread_local_mode
|
70
|
+
self.thread_local_mode = :slave
|
71
|
+
else
|
72
|
+
@global_mode = :slave
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def slave?
|
77
|
+
mode == :slave
|
78
|
+
end
|
79
|
+
|
80
|
+
def master!
|
81
|
+
if thread_local_mode
|
82
|
+
self.thread_local_mode = :master
|
83
|
+
else
|
84
|
+
@global_mode = :master
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def master?
|
89
|
+
mode == :master
|
90
|
+
end
|
91
|
+
|
92
|
+
def with_slave(&block)
|
93
|
+
with_mode(:slave, &block)
|
94
|
+
end
|
95
|
+
|
96
|
+
def with_master(&block)
|
97
|
+
with_mode(:master, &block)
|
98
|
+
end
|
99
|
+
|
100
|
+
def with_mode(new_mode, &block)
|
101
|
+
unless AVAILABLE_MODES.include?(new_mode)
|
102
|
+
raise ArgumentError.new("Unknown mode: #{new_mode}")
|
103
|
+
end
|
104
|
+
|
105
|
+
saved_mode = thread_local_mode
|
106
|
+
self.thread_local_mode = new_mode
|
107
|
+
block.call
|
108
|
+
ensure
|
109
|
+
self.thread_local_mode = saved_mode
|
110
|
+
end
|
111
|
+
|
112
|
+
def switch_name(new_name, &block)
|
113
|
+
if block
|
114
|
+
begin
|
115
|
+
old_name = @current_name
|
116
|
+
@current_name = new_name
|
117
|
+
block.call
|
118
|
+
ensure
|
119
|
+
@current_name = old_name
|
120
|
+
end
|
121
|
+
else
|
122
|
+
@current_name = new_name
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def reset_name!
|
127
|
+
@current_name = @initial_name
|
128
|
+
end
|
129
|
+
|
130
|
+
def model_for_connection
|
131
|
+
ProxyRepository.checkout(@current_name) # Ensure the target proxy is created
|
132
|
+
model_name = SwitchConnection.config.model_name(@current_name, mode)
|
133
|
+
if model_name
|
134
|
+
Proxy.const_get(model_name)
|
135
|
+
elsif mode == :slave
|
136
|
+
# When only master is specified, re-use master connection.
|
137
|
+
with_master do
|
138
|
+
model_for_connection
|
139
|
+
end
|
140
|
+
else
|
141
|
+
ActiveRecord::Base
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def connection
|
146
|
+
model_for_connection.connection
|
147
|
+
end
|
148
|
+
|
149
|
+
def connected?
|
150
|
+
model_for_connection.connected?
|
151
|
+
end
|
152
|
+
|
153
|
+
def cache(&block)
|
154
|
+
r = with_slave { model_for_connection }
|
155
|
+
w = with_master { model_for_connection }
|
156
|
+
r.cache { w.cache(&block) }
|
157
|
+
end
|
158
|
+
|
159
|
+
def uncached(&block)
|
160
|
+
r = with_slave { model_for_connection }
|
161
|
+
w = with_master { model_for_connection }
|
162
|
+
r.uncached { w.uncached(&block) }
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'switch_connection/proxy'
|
5
|
+
|
6
|
+
module SwitchConnection
|
7
|
+
class ProxyRepository
|
8
|
+
include Singleton
|
9
|
+
|
10
|
+
def self.checkout(name)
|
11
|
+
instance.checkout(name)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.find(name)
|
15
|
+
instance.find(name)
|
16
|
+
end
|
17
|
+
|
18
|
+
def checkout(name)
|
19
|
+
proxies[name] ||= Proxy.new(name)
|
20
|
+
end
|
21
|
+
|
22
|
+
def find(name)
|
23
|
+
proxies.fetch(name)
|
24
|
+
end
|
25
|
+
|
26
|
+
def proxies
|
27
|
+
@proxies ||= {}
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
data/spec/models.rb
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
SwitchConnection.configure do |config|
|
4
|
+
config.define_switch_point :main,
|
5
|
+
slaves: [:main_slave],
|
6
|
+
master: :main_master
|
7
|
+
config.define_switch_point :main2,
|
8
|
+
slaves: [:main2_slave],
|
9
|
+
master: :main2_master
|
10
|
+
config.define_switch_point :user,
|
11
|
+
slaves: [:user],
|
12
|
+
master: :user
|
13
|
+
config.define_switch_point :comment,
|
14
|
+
slaves: [:comment_slave],
|
15
|
+
master: :comment_master
|
16
|
+
config.define_switch_point :special,
|
17
|
+
slaves: [:main_slave_special],
|
18
|
+
master: :main_master
|
19
|
+
config.define_switch_point :nanika1,
|
20
|
+
slaves: [:main_slave],
|
21
|
+
master: :main_master
|
22
|
+
config.define_switch_point :nanika2,
|
23
|
+
slaves: [:main_slave],
|
24
|
+
master: :main_master
|
25
|
+
config.define_switch_point :nanika3,
|
26
|
+
master: :comment_master
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'active_record'
|
30
|
+
|
31
|
+
class Book < ActiveRecord::Base
|
32
|
+
use_switch_point :main
|
33
|
+
after_save :do_after_save
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def do_after_save; end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Book2 < ActiveRecord::Base
|
41
|
+
use_switch_point :main
|
42
|
+
end
|
43
|
+
|
44
|
+
class Book3 < ActiveRecord::Base
|
45
|
+
use_switch_point :main2
|
46
|
+
end
|
47
|
+
|
48
|
+
class Publisher < ActiveRecord::Base
|
49
|
+
use_switch_point :main
|
50
|
+
end
|
51
|
+
|
52
|
+
class Comment < ActiveRecord::Base
|
53
|
+
use_switch_point :comment
|
54
|
+
end
|
55
|
+
|
56
|
+
class User < ActiveRecord::Base
|
57
|
+
use_switch_point :user
|
58
|
+
end
|
59
|
+
|
60
|
+
class BigData < ActiveRecord::Base
|
61
|
+
use_switch_point :special
|
62
|
+
end
|
63
|
+
|
64
|
+
class Note < ActiveRecord::Base
|
65
|
+
end
|
66
|
+
|
67
|
+
class Nanika1 < ActiveRecord::Base
|
68
|
+
use_switch_point :nanika1
|
69
|
+
end
|
70
|
+
|
71
|
+
class Nanika2 < ActiveRecord::Base
|
72
|
+
use_switch_point :nanika2
|
73
|
+
end
|
74
|
+
|
75
|
+
class Nanika3 < ActiveRecord::Base
|
76
|
+
use_switch_point :nanika3
|
77
|
+
end
|
78
|
+
|
79
|
+
class AbstractNanika < ActiveRecord::Base
|
80
|
+
use_switch_point :main
|
81
|
+
self.abstract_class = true
|
82
|
+
end
|
83
|
+
|
84
|
+
class DerivedNanika1 < AbstractNanika
|
85
|
+
end
|
86
|
+
|
87
|
+
class DerivedNanika2 < AbstractNanika
|
88
|
+
use_switch_point :main2
|
89
|
+
end
|
90
|
+
|
91
|
+
base =
|
92
|
+
if RUBY_PLATFORM == 'java'
|
93
|
+
{ adapter: 'jdbcsqlite3' }
|
94
|
+
else
|
95
|
+
{ adapter: 'sqlite3', pool: 10 }
|
96
|
+
end
|
97
|
+
|
98
|
+
databases = {
|
99
|
+
test: {
|
100
|
+
'main_slave' => base.merge(database: 'main_slave.sqlite3'),
|
101
|
+
'main_master' => base.merge(database: 'main_master.sqlite3'),
|
102
|
+
'main2_slave' => base.merge(database: 'main2_slave.sqlite3'),
|
103
|
+
'main2_master' => base.merge(database: 'main2_master.sqlite3'),
|
104
|
+
'main_slave_special' => base.merge(database: 'main_slave_special.sqlite3'),
|
105
|
+
'user' => base.merge(database: 'user.sqlite3'),
|
106
|
+
'comment_slave' => base.merge(database: 'comment_slave.sqlite3'),
|
107
|
+
'comment_master' => base.merge(database: 'comment_master.sqlite3'),
|
108
|
+
'default' => base.merge(database: 'default.sqlite3')
|
109
|
+
}
|
110
|
+
}
|
111
|
+
ActiveRecord::Base.configurations =
|
112
|
+
# ActiveRecord.gem_version was introduced in ActiveRecord 4.0
|
113
|
+
if ActiveRecord.respond_to?(:gem_version) && ActiveRecord.gem_version >= Gem::Version.new('5.1.0')
|
114
|
+
{ 'test' => databases }
|
115
|
+
else
|
116
|
+
databases
|
117
|
+
end
|
118
|
+
|
119
|
+
default_database_config = ActiveRecord::Base.configurations[SwitchConnection.config.env]['default']
|
120
|
+
ActiveRecord::Base.establish_connection(default_database_config)
|
121
|
+
|
122
|
+
# XXX: Check connection laziness
|
123
|
+
[Book, User, Note, Nanika1, ActiveRecord::Base].each do |model|
|
124
|
+
if model.connected?
|
125
|
+
raise "ActiveRecord::Base didn't establish connection lazily!"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
ActiveRecord::Base.connection # Create connection
|
129
|
+
|
130
|
+
[Book, User, Nanika3].each do |model|
|
131
|
+
model.with_master do
|
132
|
+
if model.switch_point_proxy.connected?
|
133
|
+
raise "#{model.name} didn't establish connection lazily!"
|
134
|
+
end
|
135
|
+
end
|
136
|
+
model.with_slave do
|
137
|
+
if model.switch_point_proxy.connected?
|
138
|
+
raise "#{model.name} didn't establish connection lazily!"
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV['RAILS_ENV'] ||= 'test'
|
4
|
+
|
5
|
+
require 'coveralls'
|
6
|
+
require 'simplecov'
|
7
|
+
|
8
|
+
SimpleCov.formatters = [
|
9
|
+
SimpleCov::Formatter::HTMLFormatter,
|
10
|
+
Coveralls::SimpleCov::Formatter,
|
11
|
+
]
|
12
|
+
SimpleCov.start do
|
13
|
+
add_filter Bundler.bundle_path.to_s
|
14
|
+
add_filter File.dirname(__FILE__)
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'switch_connection'
|
18
|
+
require 'models'
|
19
|
+
|
20
|
+
RSpec.configure do |config|
|
21
|
+
config.filter_run :focus
|
22
|
+
config.run_all_when_everything_filtered = true
|
23
|
+
|
24
|
+
if config.files_to_run.one?
|
25
|
+
config.full_backtrace = true
|
26
|
+
config.default_formatter = 'doc'
|
27
|
+
end
|
28
|
+
|
29
|
+
config.order = :random
|
30
|
+
Kernel.srand config.seed
|
31
|
+
|
32
|
+
config.expect_with :rspec do |expectations|
|
33
|
+
expectations.syntax = :expect
|
34
|
+
end
|
35
|
+
|
36
|
+
config.mock_with :rspec do |mocks|
|
37
|
+
mocks.syntax = :expect
|
38
|
+
mocks.verify_partial_doubles = true
|
39
|
+
end
|
40
|
+
|
41
|
+
config.before(:suite) do
|
42
|
+
Book.with_master do
|
43
|
+
Book.connection.execute('CREATE TABLE books (id integer primary key autoincrement)')
|
44
|
+
end
|
45
|
+
|
46
|
+
Book2.with_master do
|
47
|
+
Book2.connection.execute('CREATE TABLE book2s (id integer primary key autoincrement)')
|
48
|
+
end
|
49
|
+
|
50
|
+
FileUtils.cp('main_master.sqlite3', 'main_slave.sqlite3')
|
51
|
+
|
52
|
+
Book3.with_master do
|
53
|
+
Book3.connection.execute('CREATE TABLE book3s (id integer primary key autoincrement)')
|
54
|
+
end
|
55
|
+
|
56
|
+
FileUtils.cp('main2_master.sqlite3', 'main2_slave.sqlite3')
|
57
|
+
|
58
|
+
Note.connection.execute('CREATE TABLE notes (id integer primary key autoincrement)')
|
59
|
+
|
60
|
+
Nanika3.connection.execute('CREATE TABLE nanika3s (id integer primary key)')
|
61
|
+
end
|
62
|
+
|
63
|
+
config.after(:suite) do
|
64
|
+
ActiveRecord::Base.configurations[SwitchConnection.config.env].each_value do |c|
|
65
|
+
FileUtils.rm_f(c[:database])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
config.after(:each) do
|
70
|
+
Book.with_master do
|
71
|
+
Book.delete_all
|
72
|
+
end
|
73
|
+
FileUtils.cp('main_master.sqlite3', 'main_slave.sqlite3')
|
74
|
+
|
75
|
+
Nanika3.delete_all
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
RSpec::Matchers.define :connect_to do |expected|
|
80
|
+
database_name = lambda do |model|
|
81
|
+
model.connection.pool.spec.config[:database]
|
82
|
+
end
|
83
|
+
|
84
|
+
match do |actual|
|
85
|
+
database_name.call(actual) == expected
|
86
|
+
end
|
87
|
+
|
88
|
+
failure_message do |actual|
|
89
|
+
"expected #{actual.name} to connect to #{expected} but connected to #{database_name.call(actual)}"
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,402 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
RSpec.describe SwitchConnection::Model do
|
4
|
+
describe '.use_switch_point' do
|
5
|
+
after do
|
6
|
+
Book.use_switch_point :main
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'changes connection' do
|
10
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
11
|
+
Book.use_switch_point :comment
|
12
|
+
expect(Book).to connect_to('comment_master.sqlite3')
|
13
|
+
|
14
|
+
Thread.start do
|
15
|
+
Book.with_switch_point(:main) do
|
16
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
17
|
+
end
|
18
|
+
Book.with_switch_point(:comment) do
|
19
|
+
expect(Book).to connect_to('comment_master.sqlite3')
|
20
|
+
end
|
21
|
+
end.join
|
22
|
+
end
|
23
|
+
|
24
|
+
context 'with non-existing switch point name' do
|
25
|
+
it 'raises error' do
|
26
|
+
expect {
|
27
|
+
Class.new(ActiveRecord::Base) do
|
28
|
+
use_switch_point :not_found
|
29
|
+
end
|
30
|
+
}.to raise_error(KeyError)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe '.connection' do
|
36
|
+
it 'returns master connection by default' do
|
37
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
38
|
+
expect(Publisher).to connect_to('main_master.sqlite3')
|
39
|
+
expect(User).to connect_to('user.sqlite3')
|
40
|
+
expect(Comment).to connect_to('comment_master.sqlite3')
|
41
|
+
expect(Note).to connect_to('default.sqlite3')
|
42
|
+
expect(Book.switch_point_proxy).to be_master
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when auto_master is disabled' do
|
46
|
+
it 'raises error when destructive query is requested in slave mode' do
|
47
|
+
expect { Book.create }.to_not raise_error
|
48
|
+
expect { Book.with_master { Book.create } }.to_not raise_error
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
context 'when auto_master is enabled' do
|
53
|
+
around do |example|
|
54
|
+
SwitchConnection.configure do |config|
|
55
|
+
config.auto_master = true
|
56
|
+
end
|
57
|
+
example.run
|
58
|
+
SwitchConnection.configure do |config|
|
59
|
+
config.auto_master = false
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'executes after_save callback in slave mode!' do
|
64
|
+
book = Book.new
|
65
|
+
expect(book).to receive(:do_after_save) {
|
66
|
+
expect(Book.switch_point_proxy).to be_master
|
67
|
+
expect(Book.connection.open_transactions).to eq(1)
|
68
|
+
}
|
69
|
+
book.save!
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
it 'works with newly checked-out connection' do
|
74
|
+
Thread.start do
|
75
|
+
Book.with_master do
|
76
|
+
Book.create
|
77
|
+
end
|
78
|
+
Book.with_slave { expect(Book.count).to eq(0) }
|
79
|
+
Book.with_master { expect(Book.count).to eq(1) }
|
80
|
+
end.join
|
81
|
+
end
|
82
|
+
|
83
|
+
context 'without switch_point configuration' do
|
84
|
+
it 'returns default connection' do
|
85
|
+
expect(Note.connection).to equal(ActiveRecord::Base.connection)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'with the same switch point name' do
|
90
|
+
it 'shares connection' do
|
91
|
+
expect(Book.connection).to equal(Publisher.connection)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
context 'with the same database name' do
|
96
|
+
it 'does NOT shares a connection' do
|
97
|
+
expect(Book.connection).to_not equal(BigData.connection)
|
98
|
+
Book.with_master do
|
99
|
+
BigData.with_master do
|
100
|
+
expect(Book.connection).to_not equal(BigData.connection)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'when superclass uses use_switch_point' do
|
107
|
+
context 'without use_switch_point in derived class' do
|
108
|
+
it 'inherits switch_point configuration' do
|
109
|
+
expect(DerivedNanika1).to connect_to('main_master.sqlite3')
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'shares connection with superclass' do
|
113
|
+
expect(DerivedNanika1.connection).to equal(AbstractNanika.connection)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context 'with use_switch_point in derived class' do
|
118
|
+
it 'overrides superclass' do
|
119
|
+
expect(DerivedNanika2).to connect_to('main2_master.sqlite3')
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context 'when superclass changes switch_point' do
|
124
|
+
after do
|
125
|
+
AbstractNanika.use_switch_point :main
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'follows' do
|
129
|
+
AbstractNanika.use_switch_point :main2
|
130
|
+
expect(DerivedNanika1).to connect_to('main2_master.sqlite3')
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'without :master' do
|
136
|
+
it 'sends destructive queries to ActiveRecord::Base' do
|
137
|
+
expect(Nanika1).to connect_to('main_master.sqlite3')
|
138
|
+
Nanika1.with_master do
|
139
|
+
expect(Nanika1).to connect_to('main_master.sqlite3')
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
context 'without :slave' do
|
145
|
+
it 'sends all queries to :master' do
|
146
|
+
expect(Nanika3).to connect_to('comment_master.sqlite3')
|
147
|
+
Nanika3.with_master do
|
148
|
+
expect(Nanika3).to connect_to('comment_master.sqlite3')
|
149
|
+
Nanika3.create
|
150
|
+
end
|
151
|
+
expect(Nanika3.count).to eq(1)
|
152
|
+
expect(Nanika3.with_slave { Nanika3.connection }).to equal(Nanika3.with_master { Nanika3.connection })
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
describe '.with_master' do
|
158
|
+
it 'changes connection locally' do
|
159
|
+
Book.with_master do
|
160
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
161
|
+
expect(Book.switch_point_proxy).to be_master
|
162
|
+
end
|
163
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
164
|
+
expect(Book.switch_point_proxy).to be_master
|
165
|
+
end
|
166
|
+
|
167
|
+
it 'changes connection locally' do
|
168
|
+
Book.with_switch_point(:comment) do
|
169
|
+
expect(Book.switch_point_proxy).to be_master
|
170
|
+
expect(Book).to connect_to('comment_master.sqlite3')
|
171
|
+
Thread.start do
|
172
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
173
|
+
end.join
|
174
|
+
end
|
175
|
+
|
176
|
+
expect(Book.switch_point_proxy).to be_master
|
177
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'affects to other models with the same switch point' do
|
181
|
+
Book.with_master do
|
182
|
+
expect(Publisher).to connect_to('main_master.sqlite3')
|
183
|
+
end
|
184
|
+
expect(Publisher).to connect_to('main_master.sqlite3')
|
185
|
+
end
|
186
|
+
|
187
|
+
it 'does not affect to other models with different switch point' do
|
188
|
+
Book.with_master do
|
189
|
+
expect(Comment).to connect_to('comment_master.sqlite3')
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
context 'with the same switch point' do
|
194
|
+
it 'shares connection' do
|
195
|
+
Book.with_master do
|
196
|
+
expect(Book.connection).to equal(Publisher.connection)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
context 'without use_switch_point' do
|
202
|
+
it 'raises error' do
|
203
|
+
expect { Note.with_master { :bypass } }.to raise_error(SwitchConnection::UnconfiguredError)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
it 'affects thread-locally' do
|
208
|
+
Book.with_slave do
|
209
|
+
expect(Book).to connect_to('main_slave.sqlite3')
|
210
|
+
Thread.start do
|
211
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
212
|
+
end.join
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
describe '#with_master' do
|
218
|
+
it 'behaves like .with_master' do
|
219
|
+
book = Book.with_master { Book.create! }
|
220
|
+
book.with_master do
|
221
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
222
|
+
end
|
223
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
describe '.with_slave' do
|
228
|
+
context 'when master! is called globally' do
|
229
|
+
before do
|
230
|
+
SwitchConnection.master!(:main)
|
231
|
+
end
|
232
|
+
|
233
|
+
it 'locally overwrites global mode' do
|
234
|
+
Book.with_slave do
|
235
|
+
expect(Book).to connect_to('main_slave.sqlite3')
|
236
|
+
end
|
237
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
describe '#with_slave' do
|
243
|
+
it 'behaves like .with_slave' do
|
244
|
+
book = Book.create!
|
245
|
+
book.with_slave do
|
246
|
+
expect(Book).to connect_to('main_slave.sqlite3')
|
247
|
+
end
|
248
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
describe '#with_mode' do
|
253
|
+
it 'raises error if unknown mode is given' do
|
254
|
+
expect { SwitchConnection::ProxyRepository.checkout(:main).with_mode(:typo) }.to raise_error(ArgumentError)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
describe '.switch_name' do
|
259
|
+
after do
|
260
|
+
Book.switch_point_proxy.reset_name!
|
261
|
+
end
|
262
|
+
|
263
|
+
it 'switches proxy configuration' do
|
264
|
+
Book.switch_point_proxy.switch_name(:comment)
|
265
|
+
expect(Book).to connect_to('comment_master.sqlite3')
|
266
|
+
expect(Publisher).to connect_to('comment_master.sqlite3')
|
267
|
+
end
|
268
|
+
|
269
|
+
context 'with block' do
|
270
|
+
it 'switches proxy configuration locally' do
|
271
|
+
Book.switch_point_proxy.switch_name(:comment) do
|
272
|
+
expect(Book).to connect_to('comment_master.sqlite3')
|
273
|
+
expect(Publisher).to connect_to('comment_master.sqlite3')
|
274
|
+
end
|
275
|
+
expect(Book).to connect_to('main_master.sqlite3')
|
276
|
+
expect(Publisher).to connect_to('main_master.sqlite3')
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
describe '.transaction_with' do
|
282
|
+
context 'when each model has a same master' do
|
283
|
+
before do
|
284
|
+
@before_book_count = Book.count
|
285
|
+
@before_book2_count = Book2.count
|
286
|
+
|
287
|
+
Book.transaction_with(Book2) do
|
288
|
+
Book.create
|
289
|
+
Book2.create
|
290
|
+
end
|
291
|
+
|
292
|
+
@after_book_count = Book.with_master do
|
293
|
+
Book.count
|
294
|
+
end
|
295
|
+
@after_book2_count = Book2.with_master do
|
296
|
+
Book2.count
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
it 'should create a new record' do
|
301
|
+
expect(
|
302
|
+
Book.with_master do
|
303
|
+
Book.count
|
304
|
+
end
|
305
|
+
).to be > @before_book_count
|
306
|
+
|
307
|
+
expect(
|
308
|
+
Book2.with_master do
|
309
|
+
Book2.count
|
310
|
+
end
|
311
|
+
).to be > @before_book2_count
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
context 'when each model has a other master' do
|
316
|
+
it {
|
317
|
+
expect {
|
318
|
+
Book.transaction_with(Book3) do
|
319
|
+
Book.create
|
320
|
+
Book3.create
|
321
|
+
end
|
322
|
+
}.to raise_error(SwitchConnection::Error)
|
323
|
+
}
|
324
|
+
end
|
325
|
+
|
326
|
+
context 'when raise exception in transaction that include some model, and models each have other master' do
|
327
|
+
before do
|
328
|
+
@before_book_count = Book.count
|
329
|
+
@before_book3_count = Book3.count
|
330
|
+
|
331
|
+
Book.transaction_with(Book2) do
|
332
|
+
Book.create
|
333
|
+
Book3.with_master do
|
334
|
+
Book3.create
|
335
|
+
end
|
336
|
+
raise ActiveRecord::Rollback
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
it 'Book should not create a new record (rollbacked)' do
|
341
|
+
expect(
|
342
|
+
Book.with_master do
|
343
|
+
Book.count
|
344
|
+
end
|
345
|
+
).to eq @before_book_count
|
346
|
+
end
|
347
|
+
|
348
|
+
it 'Book3 should create a new record (not rollbacked)' do
|
349
|
+
expect(
|
350
|
+
Book3.with_master do
|
351
|
+
Book3.count
|
352
|
+
end
|
353
|
+
).to be > @before_book3_count
|
354
|
+
end
|
355
|
+
end
|
356
|
+
|
357
|
+
context 'when nested transaction_with then parent transaction rollbacked' do
|
358
|
+
before do
|
359
|
+
@before_book_count = Book.count
|
360
|
+
@before_book3_count = Book3.count
|
361
|
+
|
362
|
+
Book.transaction_with do
|
363
|
+
Book.create
|
364
|
+
|
365
|
+
Book3.transaction_with do
|
366
|
+
Book3.create
|
367
|
+
end
|
368
|
+
|
369
|
+
raise ActiveRecord::Rollback
|
370
|
+
end
|
371
|
+
|
372
|
+
it {
|
373
|
+
expect(
|
374
|
+
Book.with_master do
|
375
|
+
Book.count
|
376
|
+
end
|
377
|
+
).to be = @before_book_count
|
378
|
+
|
379
|
+
expect(
|
380
|
+
Book3.with_master do
|
381
|
+
Book3.count
|
382
|
+
end
|
383
|
+
).to be > @before_book3_count
|
384
|
+
}
|
385
|
+
end
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
describe '#transaction_with' do
|
390
|
+
it 'behaves like .transaction_with' do
|
391
|
+
book = Book.with_master { Book.create! }
|
392
|
+
expect(Book.with_master { Book.count }).to eq(1)
|
393
|
+
book.transaction_with(Book2) do
|
394
|
+
Book.create!
|
395
|
+
raise ActiveRecord::Rollback
|
396
|
+
end
|
397
|
+
expect(Book.with_master { Book.count }).to eq(1)
|
398
|
+
|
399
|
+
expect { book.transaction_with(Book3) {} }.to raise_error(SwitchConnection::Error)
|
400
|
+
end
|
401
|
+
end
|
402
|
+
end
|