switch_connection 1.0.0 → 1.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3a610610ba7b802579bd70c8b25df8f8bba50299
4
- data.tar.gz: c796e8855c8f3472629390723cd8c0c6c00559ea
3
+ metadata.gz: 2dd39f7531c598f9ee00e870eb8b9606f81efbfb
4
+ data.tar.gz: 72326d1c52afa768c11fbf6bd2eea5cd0f622f13
5
5
  SHA512:
6
- metadata.gz: b3a4ad59a79b047914a09d125bae342993ab7b619cd48c23a2d7b0c53ade3be1d1d74948ae011a91741247b10729a611d5f1494e538945a641f196c9f4476958
7
- data.tar.gz: 3d2fa748e38c0dc9536d9d41d0699868d387556df4d7730ba19e621a79a69e05d593f7b4810ed8e5f9d6dbdeb933fb4d4a0f765cb94b2025ceeaccc67543aca6
6
+ metadata.gz: 7eeda4512dbf50f26561a22616393e92be166f99883af4674a22d994f0e35437c80727125471d1c844c25746386653ea473d1ae801f19b82ebc5226feb5bb8fb
7
+ data.tar.gz: bf5a1af2576ac103032ec55d91687b1a0bf25c45b465369c8fd0124f14ccc48647adc06371ffca313f9e1074220405589c8b923bcd2e756963b562a29721cd0a
@@ -7,14 +7,8 @@ AllCops:
7
7
  - "gemfiles/**/*"
8
8
  - "vendor/**/*"
9
9
 
10
- Performance/RedundantBlockCall:
11
- Enabled: false
12
-
13
- Layout/AlignParameters:
14
- Enabled: false
15
-
16
10
  LineLength:
17
- Max: 100
11
+ Max: 120
18
12
 
19
13
  Metrics:
20
14
  Enabled: false
@@ -3,6 +3,7 @@ rvm:
3
3
  - 2.3.7
4
4
  - 2.4.4
5
5
  - 2.5.1
6
+ - 2.6.5
6
7
  - ruby-head
7
8
  gemfile:
8
9
  - gemfiles/rails_3.2.gemfile
@@ -20,3 +21,21 @@ matrix:
20
21
  allow_failures:
21
22
  - rvm: ruby-head
22
23
  - gemfile: gemfiles/rails_edge.gemfile
24
+ - rvm: 2.4.4
25
+ gemfile: gemfiles/rails_3.2.gemfile
26
+ - rvm: 2.4.4
27
+ gemfile: gemfiles/rails_4.0.gemfile
28
+ - rvm: 2.4.4
29
+ gemfile: gemfiles/rails_4.1.gemfile
30
+ - rvm: 2.5.1
31
+ gemfile: gemfiles/rails_3.2.gemfile
32
+ - rvm: 2.5.1
33
+ gemfile: gemfiles/rails_4.0.gemfile
34
+ - rvm: 2.5.1
35
+ gemfile: gemfiles/rails_4.1.gemfile
36
+ - rvm: 2.6.5
37
+ gemfile: gemfiles/rails_3.2.gemfile
38
+ - rvm: 2.6.5
39
+ gemfile: gemfiles/rails_4.0.gemfile
40
+ - rvm: 2.6.5
41
+ gemfile: gemfiles/rails_4.1.gemfile
@@ -1,3 +1,6 @@
1
+ ## 1.1.0
2
+ - Add auto send read query to slave https://github.com/phamvanhung2e123/switch_point/pull/5
3
+
1
4
  ## 1.0.0 (2019-07-31)
2
5
  - Multi slaves support
3
6
  - Connect to master by default
data/README.md CHANGED
@@ -1,10 +1,10 @@
1
1
  # SwitchConnection
2
- [![Gem Version](https://badge.fury.io/rb/switch_point.svg)](http://badge.fury.io/rb/switch_point)
3
- [![Build Status](https://travis-ci.org/phamvanhung2e123/switch_point.svg?branch=master)](https://travis-ci.org/phamvannhung22123/switch_point)
4
- [![Coverage Status](https://img.shields.io/coveralls/phamvanhung2e123/switch_point.svg?branch=master)](https://coveralls.io/r/phamvannhung2e123/switch_point?branch=master)
5
- [![Code Climate](https://codeclimate.com/github/phamvanhung2e123/switch_point/badges/gpa.svg)](https://codeclimate.com/github/phamvannhung2e123/switch_point)
2
+ [![Gem Version](https://badge.fury.io/rb/switch_connection.svg)](https://badge.fury.io/rb/switch_connection)
3
+ [![Build Status](https://travis-ci.org/phamvanhung2e123/switch_point.svg?branch=master)](https://travis-ci.org/phamvanhung2e123/switch_point)
4
+ [![Coverage Status](https://img.shields.io/coveralls/phamvanhung2e123/switch_point.svg?branch=master)](https://coveralls.io/r/phamvanhung2e123/switch_point?branch=master)
5
+ [![Code Climate](https://codeclimate.com/github/phamvanhung2e123/switch_point/badges/gpa.svg)](https://codeclimate.com/github/phamvanhung2e123/switch_point)
6
6
 
7
- Switching database connection between slave one and writable one. Fork from `switch_point` gem.
7
+ Switching database connection between multiple slave and writable one. Fork from `switch_point` gem.
8
8
  Original Version: https://github.com/eagletmt/switch_point.
9
9
 
10
10
  ## Installation
@@ -72,22 +72,26 @@ end
72
72
 
73
73
  ### Switching connections
74
74
 
75
+ - Write query automatically go master database, read query automatically go to slave database.
75
76
  ```ruby
76
- Article.with_slave { Article.first } # Read from db-blog-slave
77
- Category.with_slave { Category.first } # Also read from db-blog-slave
78
- Comment.with_slave { Comment.first } # Read from db-comment-slave
79
-
80
- Article.with_slave do
81
- article = Article.first # Read from db-blog-slave
82
- article.title = 'new title'
83
- Article.with_master do
84
- article.save! # Write to db-blog-master
85
- article.reload # Read from db-blog-master
86
- Category.first # Read from db-blog-master
87
- end
88
- end
77
+ article = Article.find(1) # read query go to slave
78
+ article.name = "hoge"
79
+ article.save # write query go to master
89
80
  ```
90
81
 
82
+ - Use with_master to force query go to master database.
83
+ ```ruby
84
+ Article.with_master do
85
+ article.save! # Write to master db
86
+ Article.first # Read from master db
87
+ end
88
+ ```
89
+ - Force query to master database.
90
+ ```ruby
91
+ Article.with_master { Article.all }
92
+ Article.with_master { Article.find(1) }
93
+ Article.with_master { Article.where(name: "foobar").to_a }
94
+ ```
91
95
  - with_switch_point
92
96
  ```ruby
93
97
  Book.with_switch_point(:main) { Book.count }
@@ -95,45 +99,10 @@ Book.with_switch_point(:main) { Book.count }
95
99
 
96
100
  Note that Article and Category shares their connections.
97
101
 
98
- ## Notes
99
-
100
- ### auto_master
101
- `auto_master` by default.
102
-
103
- When `auto_master` is enabled, destructive queries is sent to writable connection even in slave mode.
104
- But it does NOT work well on transactions.
105
-
106
- Suppose `after_save` callback is set to User model. When `User.create` is called, it proceeds as follows.
107
-
108
- 1. BEGIN TRANSACTION is sent to READONLY connection.
109
- 2. switch_point switches the connection to WRITABLE.
110
- 3. INSERT statement is sent to WRITABLE connection.
111
- 4. switch_point reset the connection to READONLY.
112
- 5. after_save callback is called.
113
- - At this point, the connection is READONLY and in a transaction.
114
- 6. COMMIT TRANSACTION is sent to READONLY connection.
115
-
116
- ### connection-related methods of model
117
- Model has several connection-related methods: `connection_handler`, `connection_pool`, `connected?` and so on.
118
- Since only `connection` method is monkey-patched, other connection-related methods doesn't work properly.
119
- If you'd like to use those methods, send it to `Model.switch_point_proxy.model_for_connection`.
120
-
121
- ## Internals
122
- There's a proxy which holds two connections: slave one and writable one.
123
- A proxy has a thread-local state indicating the current mode: slave or writable.
124
-
125
- Each ActiveRecord model refers to a proxy.
126
- `ActiveRecord::Base.connection` is hooked and delegated to the referred proxy.
127
-
128
- When the writable connection is requested to execute destructive query, the slave connection clears its query cache.
129
-
130
- ![switch_point](https://gyazo.wanko.cc/switch_point.svg)
131
-
132
102
  ### Special case: ActiveRecord::Base.connection
133
103
  Basically, each connection managed by a proxy isn't shared between proxies.
134
104
  But there's one exception: ActiveRecord::Base.
135
105
 
136
-
137
106
  ## Contributing
138
107
 
139
108
  1. Fork it ( https://github.com/phamvanmhung2e123/switch_point/fork )
@@ -62,5 +62,7 @@ module SwitchConnection
62
62
  end
63
63
  ActiveSupport.on_load(:active_record) do
64
64
  require 'switch_connection/model'
65
+ require 'switch_connection/connection_routing'
65
66
  ActiveRecord::Base.include(SwitchConnection::Model)
67
+ ActiveRecord::Relation.prepend(SwitchConnection::Relation::MonkeyPatch)
66
68
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ # This Module is for MonkeyPatch ActiveRecord::Relation
4
+ module SwitchConnection
5
+ module Relation
6
+ module MonkeyPatch
7
+ def calculate(*args, &block)
8
+ if @klass.switch_point_proxy && !lock_value && @klass.connection.open_transactions.zero?
9
+ @klass.with_slave do
10
+ super
11
+ end
12
+ else
13
+ super
14
+ end
15
+ end
16
+
17
+ def exists?(*args, &block)
18
+ if @klass.switch_point_proxy && !lock_value && @klass.connection.open_transactions.zero?
19
+ @klass.with_slave do
20
+ super
21
+ end
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ def pluck(*args, &block)
28
+ if @klass.switch_point_proxy && !lock_value && @klass.connection.open_transactions.zero?
29
+ @klass.with_slave do
30
+ super
31
+ end
32
+ else
33
+ super
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -10,6 +10,25 @@ module SwitchConnection
10
10
  model.singleton_class.class_eval do
11
11
  include ClassMethods
12
12
  prepend MonkeyPatch
13
+ def find_by_sql(*args, &block)
14
+ if switch_point_proxy && connection.open_transactions.zero?
15
+ with_slave do
16
+ super
17
+ end
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def count_by_sql(*args, &block)
24
+ if switch_point_proxy && connection.open_transactions.zero?
25
+ with_slave do
26
+ super
27
+ end
28
+ else
29
+ super
30
+ end
31
+ end
13
32
  end
14
33
  end
15
34
 
@@ -29,12 +48,18 @@ module SwitchConnection
29
48
  self.class.transaction_with(*models, &block)
30
49
  end
31
50
 
51
+ def reload(*args, &block)
52
+ self.class.with_master do
53
+ super(*args, &block)
54
+ end
55
+ end
56
+
32
57
  module ClassMethods
33
58
  def with_slave(&block)
34
59
  if switch_point_proxy
35
60
  switch_point_proxy.with_slave(&block)
36
61
  else
37
- raise UnconfiguredError.new("#{name} isn't configured to use switch_point")
62
+ yield
38
63
  end
39
64
  end
40
65
 
@@ -42,7 +67,7 @@ module SwitchConnection
42
67
  if switch_point_proxy
43
68
  switch_point_proxy.with_master(&block)
44
69
  else
45
- raise UnconfiguredError.new("#{name} isn't configured to use switch_point")
70
+ yield
46
71
  end
47
72
  end
48
73
 
@@ -117,7 +142,7 @@ module SwitchConnection
117
142
  def connection
118
143
  if switch_point_proxy
119
144
  connection = switch_point_proxy.connection
120
- connection.connection_name = switch_point_name
145
+ connection.connection_name = "#{switch_point_name} #{switch_point_proxy.mode}"
121
146
  connection
122
147
  else
123
148
  super
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'switch_connection/error'
4
-
5
4
  module SwitchConnection
6
5
  class Proxy
7
6
  attr_reader :initial_name
@@ -65,6 +64,18 @@ module SwitchConnection
65
64
  thread_local_mode || @global_mode
66
65
  end
67
66
 
67
+ def switch_connection_level=(level)
68
+ Thread.current[:"switch_point_#{@current_name}_level"] = level
69
+ end
70
+
71
+ def switch_connection_level
72
+ Thread.current[:"switch_point_#{@current_name}_level"] || 0
73
+ end
74
+
75
+ def switch_top_level_connection?
76
+ switch_connection_level.zero?
77
+ end
78
+
68
79
  def slave!
69
80
  if thread_local_mode
70
81
  self.thread_local_mode = :slave
@@ -99,14 +110,21 @@ module SwitchConnection
99
110
 
100
111
  def with_mode(new_mode, &block)
101
112
  unless AVAILABLE_MODES.include?(new_mode)
113
+ self.switch_connection_level += 1
102
114
  raise ArgumentError.new("Unknown mode: #{new_mode}")
103
115
  end
104
116
 
105
117
  saved_mode = thread_local_mode
106
- self.thread_local_mode = new_mode
118
+ if new_mode == :slave && switch_top_level_connection?
119
+ self.thread_local_mode = :slave
120
+ elsif new_mode == :master
121
+ self.thread_local_mode = :master
122
+ end
123
+ self.switch_connection_level += 1
107
124
  block.call
108
125
  ensure
109
126
  self.thread_local_mode = saved_mode
127
+ self.switch_connection_level -= 1
110
128
  end
111
129
 
112
130
  def switch_name(new_name, &block)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwitchConnection
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.0'
5
5
  end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'pry'
3
4
  SwitchConnection.configure do |config|
4
5
  config.define_switch_point :main,
5
6
  slaves: [:main_slave],
@@ -139,3 +140,30 @@ ActiveRecord::Base.connection # Create connection
139
140
  end
140
141
  end
141
142
  end
143
+
144
+ module SwitchConnection
145
+ module LogSubscriber
146
+ def self.included(base)
147
+ base.send(:attr_accessor, :connection_name)
148
+ base.send(:alias_method, :sql_without_connection_name, :sql)
149
+ base.send(:alias_method, :sql, :sql_with_connection_name)
150
+
151
+ base.send(:alias_method, :debug_without_connection_name, :debug)
152
+ base.send(:alias_method, :debug, :debug_with_connection_name)
153
+ end
154
+
155
+ def sql_with_connection_name(event)
156
+ self.connection_name = event.payload[:connection_name]
157
+ sql_without_connection_name(event)
158
+ end
159
+
160
+ def debug_with_connection_name(msg)
161
+ conn = connection_name ? color(" [#{connection_name}]", ActiveSupport::LogSubscriber::BLUE, true) : ''
162
+ debug_without_connection_name(conn + msg)
163
+ end
164
+ end
165
+ end
166
+
167
+ ActiveRecord::LogSubscriber.include(SwitchConnection::LogSubscriber)
168
+ require 'logger'
169
+ ActiveRecord::Base.logger = Logger.new STDOUT
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'pry'
4
+ require 'logger'
5
+
6
+ RSpec.describe SwitchConnection::Relation::MonkeyPatch do
7
+ before do
8
+ Book.create
9
+ end
10
+
11
+ let(:first_id_in_master_db) { Book.with_master { Book.first.id } }
12
+ describe '.pluck' do
13
+ subject { Book.pluck(:id) }
14
+ context 'when connect to master' do
15
+ it 'id is found' do
16
+ Book.with_master { is_expected.to eq([first_id_in_master_db]) }
17
+ expect(Book.with_master { Book.pluck(:id) }).to eq [first_id_in_master_db]
18
+ end
19
+ end
20
+
21
+ context 'when connect to slave' do
22
+ it 'id is not found' do
23
+ is_expected.to eq([])
24
+ end
25
+ end
26
+
27
+ context 'when thread safe' do
28
+ it 'work with thread save' do
29
+ Thread.start do
30
+ Book.with_master { expect(Book.pluck(:id)).to eq([first_id_in_master_db]) }
31
+ expect(Book.with_master { Book.pluck(:id) }).to eq [first_id_in_master_db]
32
+ expect(Book.pluck(:id)).to eq([])
33
+ end.join
34
+ end
35
+ end
36
+ end
37
+
38
+ describe '.exists?' do
39
+ subject { Book.where(id: first_id_in_master_db).exists? }
40
+
41
+ context 'when connect to master' do
42
+ it 'id is exist' do
43
+ Book.with_master { is_expected.to eq true }
44
+ expect(Book.with_master { Book.where(id: first_id_in_master_db).exists? }).to eq true
45
+ end
46
+ end
47
+
48
+ context 'when connect to slave' do
49
+ it 'id is not exist' do
50
+ is_expected.to eq false
51
+ end
52
+ end
53
+
54
+ context 'when in multi thread' do
55
+ it 'thread safe' do
56
+ Thread.start do
57
+ Book.with_master { expect(Book.where(id: first_id_in_master_db).exists?).to eq true }
58
+ expect(Book.with_master { Book.where(id: first_id_in_master_db).exists? }).to eq true
59
+ expect(Book.where(id: first_id_in_master_db).exists?).to eq false
60
+ end.join
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '.calculate' do
66
+ subject { Book.where(id: first_id_in_master_db).count }
67
+
68
+ context 'when connect to master' do
69
+ it 'id is exist' do
70
+ Book.with_master { is_expected.to eq 1 }
71
+ expect(Book.with_master { Book.where(id: first_id_in_master_db).count }).to eq 1
72
+ end
73
+ end
74
+
75
+ context 'when connect to slave' do
76
+ it 'id is not exist' do
77
+ is_expected.to eq 0
78
+ end
79
+ end
80
+
81
+ context 'when in multi thread' do
82
+ it 'thread safe' do
83
+ Thread.start do
84
+ Book.with_master { expect(Book.where(id: first_id_in_master_db).count).to eq 1 }
85
+ expect(Book.with_master { Book.where(id: first_id_in_master_db).count }).to eq 1
86
+ expect(Book.where(id: first_id_in_master_db).count).to eq 0
87
+ end.join
88
+ end
89
+ end
90
+ end
91
+ end
@@ -198,12 +198,6 @@ RSpec.describe SwitchConnection::Model do
198
198
  end
199
199
  end
200
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
201
  it 'affects thread-locally' do
208
202
  Book.with_slave do
209
203
  expect(Book).to connect_to('main_slave.sqlite3')
@@ -399,4 +393,75 @@ RSpec.describe SwitchConnection::Model do
399
393
  expect { book.transaction_with(Book3) {} }.to raise_error(SwitchConnection::Error)
400
394
  end
401
395
  end
396
+
397
+ describe '.find_by_sql' do
398
+ before do
399
+ Book.create
400
+ end
401
+
402
+ context 'when call find_by_sql from slave' do
403
+ it 'empty array' do
404
+ expect(Book.find_by_sql('SELECT * FROM books')).to eq([])
405
+ end
406
+ end
407
+
408
+ context 'when call find_by_sql from master' do
409
+ it 'not empty array' do
410
+ expect(Book.with_master { Book.find_by_sql('SELECT * FROM books') }).not_to eq([])
411
+ end
412
+ end
413
+ end
414
+
415
+ describe '.count_of_sql' do
416
+ before do
417
+ Book.create
418
+ end
419
+
420
+ context 'when count from slave' do
421
+ it 'return 0' do
422
+ expect(Book.count_by_sql('select count(*) from books')).to eq(0)
423
+ end
424
+ end
425
+
426
+ context 'when count from master' do
427
+ it 'return 1' do
428
+ Book.with_master { expect(Book.count_by_sql('select count(*) from books')).to eq(1) }
429
+ expect(Book.with_master { Book.count_by_sql('select count(*) from books') }).to eq(1)
430
+ end
431
+ end
432
+ end
433
+
434
+ describe '.cache' do
435
+ before do
436
+ Book.create
437
+ end
438
+
439
+ context 'when call count' do
440
+ it 'return 0' do
441
+ Book.cache {
442
+ expect(Book.count).to eq 0
443
+ expect(Book.count).to eq 0
444
+ expect(Book.with_master { Book.count }).to eq 1
445
+ expect(Book.with_master { Book.count }).to eq 1
446
+ }
447
+ end
448
+ end
449
+ end
450
+
451
+ describe '.uncached' do
452
+ before do
453
+ Book.create
454
+ end
455
+
456
+ context 'when call count' do
457
+ it 'return 0' do
458
+ Book.uncached {
459
+ expect(Book.count).to eq 0
460
+ expect(Book.count).to eq 0
461
+ expect(Book.with_master { Book.count }).to eq 1
462
+ expect(Book.with_master { Book.count }).to eq 1
463
+ }
464
+ end
465
+ end
466
+ end
402
467
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: switch_connection
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kohei Suzuki
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-07-31 00:00:00.000000000 Z
11
+ date: 2020-02-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -224,6 +224,7 @@ files:
224
224
  - gemfiles/rails_edge.gemfile
225
225
  - lib/switch_connection.rb
226
226
  - lib/switch_connection/config.rb
227
+ - lib/switch_connection/connection_routing.rb
227
228
  - lib/switch_connection/error.rb
228
229
  - lib/switch_connection/model.rb
229
230
  - lib/switch_connection/proxy.rb
@@ -231,6 +232,7 @@ files:
231
232
  - lib/switch_connection/version.rb
232
233
  - spec/models.rb
233
234
  - spec/spec_helper.rb
235
+ - spec/switch_connection/connection_routing_spec.rb
234
236
  - spec/switch_connection/model_spec.rb
235
237
  - spec/switch_connection_spec.rb
236
238
  - switch_connection.gemspec
@@ -261,5 +263,6 @@ summary: Switching database connection between readonly one and writable one.
261
263
  test_files:
262
264
  - spec/models.rb
263
265
  - spec/spec_helper.rb
266
+ - spec/switch_connection/connection_routing_spec.rb
264
267
  - spec/switch_connection/model_spec.rb
265
268
  - spec/switch_connection_spec.rb