active_record_slave 1.4.0 → 1.5.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
  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