switch_connection 1.0.0
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 +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
|