active_record_slave 1.4.0 → 1.5.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
  SHA256:
3
- metadata.gz: 6235db3e4c19fc1b58a00eaacde74d778e7cb8a8ef3672fe2ca63ca20f10702c
4
- data.tar.gz: cad224213432afad1c87358923595f992ce61d63c1d467ce7dbc84b6af5d37cc
3
+ metadata.gz: 28bf8e5c0b1aa126621369c792755c5bf4c3da94d7aecbd1f60af335908b8078
4
+ data.tar.gz: 959fa6afaecb8c24934085e2e5b1a018891286b3e0234d293a6cb0a27ca75f9d
5
5
  SHA512:
6
- metadata.gz: 7e26f4116785cc6ef8777d4850d44b2034e52448c20b13c6f1a920e47d41a6a9ceded5a47d8aaed5bb12fc9ee327013f0fa40214b720bc51e0fe88c666b153a2
7
- data.tar.gz: cef0b388839d44b90e4c34c1c0a9fa9978e2155621042742ab58c6fb7b5ccd40b811d746e6b3509934accd0d5654006e1e96bf0637417d106baa70947b3825cf
6
+ metadata.gz: d327d0e133592d461d954340bb28111956259ce1333765cdaf756a8e146d73728da556faf38ad994510e86e29f85bbe333c6d468ae0343cb9c92050508fb4cc5
7
+ data.tar.gz: 1345aaa4edbf866b22372c47bbd1cad0cae08ec42f169b6c4a43fe38eac079500e3803031629ec1d0afcc55984b3df08e8f195f875f3cf4ccb699770249fbee3
data/README.md CHANGED
@@ -26,7 +26,7 @@ Production Ready. Actively used in large production environments.
26
26
  * Detects when a query is inside of a transaction and sends those reads to the master by default.
27
27
  * Can be configured to send reads in a transaction to slave databases.
28
28
  * Lightweight footprint.
29
- * No overhead whatsoever when a slave is not configured.
29
+ * No overhead whatsoever when a slave is _not_ configured.
30
30
  * Negligible overhead when redirecting reads to the slave.
31
31
  * Connection Pools to both databases are retained and maintained independently by ActiveRecord.
32
32
  * The master and slave databases do not have to be of the same type.
@@ -132,13 +132,81 @@ end
132
132
 
133
133
  ## Note
134
134
 
135
- ActiveRecord::Base.execute is sometimes used to perform custom SQL calls against
136
- the database to bypass ActiveRecord. It is necessary to replace these calls
137
- with the standard ActiveRecord::Base.select call for them to be picked up by
138
- active_record_slave and redirected to the slave.
135
+ Active Record Slave is a very simple layer that inserts itself into the call chain whenever a slave is configured.
136
+ By observation we noticed that all reads are made to a select set of methods and
137
+ all writes are made directly to one method: `execute`.
139
138
 
140
- This is because ActiveRecord::Base.execute can also be used for database updates
141
- which we do not want redirected to the slave
139
+ Using this observation Active Record Slave only needs to intercept calls to the known select apis:
140
+ * select_all
141
+ * select_one
142
+ * select_rows
143
+ * select_value
144
+ * select_values
145
+
146
+ Calls to the above methods are redirected to the slave active record model `ActiveRecordSlave::Slave`.
147
+ This model is 100% managed by the regular Active Record mechanisms such as connection pools etc.
148
+
149
+ This lightweight approach ensures that all calls to the above API's are redirected to the slave without impacting:
150
+ * Transactions
151
+ * Writes
152
+ * Any SQL calls directly to `execute`
153
+
154
+ One of the limitations with this approach is that any code that performs a query by calling `execute` direct will not
155
+ be redirected to the slave instance. In this case replace the use of `execute` with one of the the above select methods.
156
+
157
+
158
+ ## Note when using `dependent: destroy`
159
+
160
+ When performing in-memory only model assignments Active Record will create a transaction against the master even though
161
+ the transaction may never be used.
162
+
163
+ Even though the transaction is unused it sends the following messages to the master database:
164
+ ~~~
165
+ SET autocommit=0
166
+ commit
167
+ SET autocommit=1
168
+ ~~~
169
+
170
+ This will impact the master database if sufficient calls are made, such as in batch workers.
171
+
172
+ For Example:
173
+
174
+ ~~~ruby
175
+ class Parent < ActiveRecord::Base
176
+ has_one :child, dependent: :destroy
177
+ end
178
+
179
+ class Child < ActiveRecord::Base
180
+ belongs_to :parent
181
+ end
182
+
183
+ # The following code will create an unused transaction against the master, even when reads are going to slaves:
184
+ parent = Parent.new
185
+ parent.child = Child.new
186
+ ~~~
187
+
188
+ If the `dependent: :destroy` is removed it no longer creates a transaction, but it also means dependents are not
189
+ destroyed when a parent is destroyed.
190
+
191
+ For this scenario when we are 100% confident no writes are being performed the following can be performed to
192
+ ignore any attempt Active Record makes at creating the transaction:
193
+
194
+ ~~~ruby
195
+ ActiveRecordSlave.skip_transactions do
196
+ parent = Parent.new
197
+ parent.child = Child.new
198
+ end
199
+ ~~~
200
+
201
+ To help identify any code within a block that is creating transactions, wrap the code with
202
+ `ActiveRecordSlave.block_transactions` to make it raise an exception anytime a transaction is attempted:
203
+
204
+ ~~~ruby
205
+ ActiveRecordSlave.block_transactions do
206
+ parent = Parent.new
207
+ parent.child = Child.new
208
+ end
209
+ ~~~
142
210
 
143
211
  ## Install
144
212
 
@@ -255,14 +323,8 @@ production:
255
323
 
256
324
  ## Dependencies
257
325
 
258
- * Tested on Rails 3 and Rails 4
259
-
260
326
  See [.travis.yml](https://github.com/reidmorrison/active_record_slave/blob/master/.travis.yml) for the list of tested Ruby platforms
261
327
 
262
- ## Possible Future Enhancements
263
-
264
- * Support for multiple named slaves (ask for it by submitting an issue)
265
-
266
328
  ## Versioning
267
329
 
268
330
  This project uses [Semantic Versioning](http://semver.org/).
@@ -1,6 +1,7 @@
1
1
  require 'active_record'
2
2
  require 'active_record/base'
3
3
  require 'active_record_slave/version'
4
+ require 'active_record_slave/errors'
4
5
  require 'active_record_slave/slave'
5
6
  require 'active_record_slave/instance_methods'
6
7
  require 'active_record_slave/active_record_slave'
@@ -2,7 +2,6 @@
2
2
  # ActiveRecord read from a slave
3
3
  #
4
4
  module ActiveRecordSlave
5
-
6
5
  # Install ActiveRecord::Slave into ActiveRecord to redirect reads to the slave
7
6
  # Parameters:
8
7
  # adapter_class:
@@ -36,12 +35,11 @@ module ActiveRecordSlave
36
35
  def self.read_from_master
37
36
  return yield if read_from_master?
38
37
  begin
39
- # Set :master indicator in thread local storage so that it is visible
40
- # during the select call
38
+ previous = Thread.current.thread_variable_get(:active_record_slave)
41
39
  read_from_master!
42
40
  yield
43
41
  ensure
44
- read_from_slave!
42
+ Thread.current.thread_variable_set(:active_record_slave, previous)
45
43
  end
46
44
  end
47
45
 
@@ -54,33 +52,68 @@ module ActiveRecordSlave
54
52
  def self.read_from_slave
55
53
  return yield if read_from_slave?
56
54
  begin
57
- # Set nil indicator in thread local storage so that it is visible
58
- # during the select call
55
+ previous = Thread.current.thread_variable_get(:active_record_slave)
59
56
  read_from_slave!
60
57
  yield
61
58
  ensure
62
- read_from_master!
59
+ Thread.current.thread_variable_set(:active_record_slave, previous)
60
+ end
61
+ end
62
+
63
+ # When only reading from a slave it is important to prevent entering any into
64
+ # a transaction since the transaction still sends traffic to the master
65
+ # that will cause the master database to slow down processing empty transactions.
66
+ def self.block_transactions
67
+ begin
68
+ previous = Thread.current.thread_variable_get(:active_record_slave_transaction)
69
+ Thread.current.thread_variable_set(:active_record_slave_transaction, :block)
70
+ yield
71
+ ensure
72
+ Thread.current.thread_variable_set(:active_record_slave_transaction, previous)
73
+ end
74
+ end
75
+
76
+ # During this block any attempts to start or end transactions will be ignored.
77
+ # This extreme action should only be taken when 100% certain no writes are going to be
78
+ # performed.
79
+ def self.skip_transactions
80
+ begin
81
+ previous = Thread.current.thread_variable_get(:active_record_slave_transaction)
82
+ Thread.current.thread_variable_set(:active_record_slave_transaction, :skip)
83
+ yield
84
+ ensure
85
+ Thread.current.thread_variable_set(:active_record_slave_transaction, previous)
63
86
  end
64
87
  end
65
88
 
66
89
  # Whether this thread is currently forcing all reads to go against the master database
67
90
  def self.read_from_master?
68
- thread_variable_get(:active_record_slave) == :master
91
+ Thread.current.thread_variable_get(:active_record_slave) == :master
69
92
  end
70
93
 
71
94
  # Whether this thread is currently forcing all reads to go against the slave database
72
95
  def self.read_from_slave?
73
- thread_variable_get(:active_record_slave) == nil
96
+ Thread.current.thread_variable_get(:active_record_slave).nil?
74
97
  end
75
98
 
76
99
  # Force all subsequent reads on this thread and any fibers called by this thread to go the master
77
100
  def self.read_from_master!
78
- thread_variable_set(:active_record_slave, :master)
101
+ Thread.current.thread_variable_set(:active_record_slave, :master)
79
102
  end
80
103
 
81
104
  # Subsequent reads on this thread and any fibers called by this thread can go to a slave
82
105
  def self.read_from_slave!
83
- thread_variable_set(:active_record_slave, nil)
106
+ Thread.current.thread_variable_set(:active_record_slave, nil)
107
+ end
108
+
109
+ # Whether any attempt to start a transaction should result in an exception
110
+ def self.block_transactions?
111
+ Thread.current.thread_variable_get(:active_record_slave_transaction) == :block
112
+ end
113
+
114
+ # Whether any attempt to start a transaction should be skipped.
115
+ def self.skip_transactions?
116
+ Thread.current.thread_variable_get(:active_record_slave_transaction) == :skip
84
117
  end
85
118
 
86
119
  # Returns whether slave reads are ignoring transactions
@@ -93,43 +126,7 @@ module ActiveRecordSlave
93
126
  @ignore_transactions = ignore_transactions
94
127
  end
95
128
 
96
- ##############################################################################
97
129
  private
98
130
 
99
131
  @ignore_transactions = false
100
-
101
- # Returns the value of the local thread variable
102
- #
103
- # Parameters
104
- # variable [Symbol]
105
- # Name of variable to get
106
- if (RUBY_VERSION.to_i >= 2) && !defined?(Rubinius::VERSION)
107
- # Fibers have their own thread local variables so use thread_variable_get
108
- def self.thread_variable_get(variable)
109
- Thread.current.thread_variable_get(variable)
110
- end
111
- else
112
- def self.thread_variable_get(variable)
113
- Thread.current[variable]
114
- end
115
- end
116
-
117
- # Sets the value of the local thread variable
118
- #
119
- # Parameters
120
- # variable [Symbol]
121
- # Name of variable to set
122
- # value [Object]
123
- # Value to set the thread variable to
124
- if (RUBY_VERSION.to_i >= 2) && !defined?(Rubinius::VERSION)
125
- # Fibers have their own thread local variables so use thread_variable_set
126
- def self.thread_variable_set(variable, value)
127
- Thread.current.thread_variable_set(variable, value)
128
- end
129
- else
130
- def self.thread_variable_set(variable, value)
131
- Thread.current[variable] = value
132
- end
133
- end
134
-
135
132
  end
@@ -0,0 +1,5 @@
1
+ module ActiveRecordSlave
2
+ # Attempting to start a transaction during a read-only database connection.
3
+ class TransactionAttempted < StandardError
4
+ end
5
+ end
@@ -21,6 +21,41 @@ module ActiveRecordSlave
21
21
  METHOD
22
22
  end
23
23
 
24
+ def begin_db_transaction
25
+ return if ActiveRecordSlave.skip_transactions?
26
+ return super unless ActiveRecordSlave.block_transactions?
27
+
28
+ raise(TransactionAttempted, 'Attempting to begin a transaction during a read-only database connection.')
29
+ end
30
+
31
+ def commit_db_transaction
32
+ return if ActiveRecordSlave.skip_transactions?
33
+ return super unless ActiveRecordSlave.block_transactions?
34
+
35
+ raise(TransactionAttempted, 'Attempting to commit a transaction during a read-only database connection.')
36
+ end
37
+
38
+ def create_savepoint(name = current_savepoint_name(true))
39
+ return if ActiveRecordSlave.skip_transactions?
40
+ return super unless ActiveRecordSlave.block_transactions?
41
+
42
+ raise(TransactionAttempted, 'Attempting to create a savepoint during a read-only database connection.')
43
+ end
44
+
45
+ def rollback_to_savepoint(name = current_savepoint_name(true))
46
+ return if ActiveRecordSlave.skip_transactions?
47
+ return super unless ActiveRecordSlave.block_transactions?
48
+
49
+ raise(TransactionAttempted, 'Attempting to rollback a savepoint during a read-only database connection.')
50
+ end
51
+
52
+ def release_savepoint(name = current_savepoint_name(true))
53
+ return if ActiveRecordSlave.skip_transactions?
54
+ return super unless ActiveRecordSlave.block_transactions?
55
+
56
+ raise(TransactionAttempted, 'Attempting to release a savepoint during a read-only database connection.')
57
+ end
58
+
24
59
  # Returns whether to read from the master database
25
60
  def active_record_slave_read_from_master?
26
61
  # Read from master when forced by thread variable, or
@@ -1,6 +1,5 @@
1
- module ActiveRecordSlave #:nodoc:
2
- class Railtie < Rails::Railtie #:nodoc:
3
-
1
+ module ActiveRecordSlave
2
+ class Railtie < Rails::Railtie
4
3
  # Make the ActiveRecordSlave configuration available in the Rails application config
5
4
  #
6
5
  # Example: For this application ignore the current transactions since the application
@@ -1,3 +1,3 @@
1
- module ActiveRecordSlave #:nodoc
2
- VERSION = '1.4.0'
1
+ module ActiveRecordSlave
2
+ VERSION = '1.5.0'
3
3
  end
@@ -41,6 +41,10 @@ ActiveRecordSlave.install!(nil, 'test')
41
41
  #
42
42
  # Unit Test for active_record_slave
43
43
  #
44
+ # Since their is no database replication in this test environment, it will
45
+ # use 2 separate databases. Writes go to the first database and reads to the second.
46
+ # As a result any writes to the first database will not be visible when trying to read from
47
+ # the second test database.
44
48
  class ActiveRecordSlaveTest < Minitest::Test
45
49
  describe 'the active_record_slave gem' do
46
50
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_record_slave
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.0
4
+ version: 1.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Reid Morrison
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-03-29 00:00:00.000000000 Z
11
+ date: 2019-04-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -36,18 +36,17 @@ files:
36
36
  - Rakefile
37
37
  - lib/active_record_slave.rb
38
38
  - lib/active_record_slave/active_record_slave.rb
39
+ - lib/active_record_slave/errors.rb
39
40
  - lib/active_record_slave/instance_methods.rb
40
41
  - lib/active_record_slave/railtie.rb
41
42
  - lib/active_record_slave/slave.rb
42
43
  - lib/active_record_slave/version.rb
43
44
  - test/active_record_slave_test.rb
44
45
  - test/database.yml
45
- - test/test.sqlite3
46
46
  - test/test_helper.rb
47
- - test/test_slave.sqlite3
48
47
  homepage: https://github.com/rocketjob/active_record_slave
49
48
  licenses:
50
- - Apache License V2.0
49
+ - Apache-2.0
51
50
  metadata: {}
52
51
  post_install_message:
53
52
  rdoc_options: []
@@ -57,22 +56,19 @@ required_ruby_version: !ruby/object:Gem::Requirement
57
56
  requirements:
58
57
  - - ">="
59
58
  - !ruby/object:Gem::Version
60
- version: '0'
59
+ version: '2.0'
61
60
  required_rubygems_version: !ruby/object:Gem::Requirement
62
61
  requirements:
63
62
  - - ">="
64
63
  - !ruby/object:Gem::Version
65
64
  version: '0'
66
65
  requirements: []
67
- rubyforge_project:
68
- rubygems_version: 2.7.3
66
+ rubygems_version: 3.0.2
69
67
  signing_key:
70
68
  specification_version: 4
71
69
  summary: Redirect ActiveRecord (Rails) reads to slave databases while ensuring all
72
70
  writes go to the master database.
73
71
  test_files:
74
- - test/test_slave.sqlite3
75
72
  - test/active_record_slave_test.rb
76
- - test/test.sqlite3
77
73
  - test/database.yml
78
74
  - test/test_helper.rb
data/test/test.sqlite3 DELETED
Binary file
Binary file