switch_connection 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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