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.
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwitchConnection
4
+ VERSION = '1.0.0'
5
+ 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
@@ -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