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 +4 -4
- data/README.md +75 -13
- data/lib/active_record_slave.rb +1 -0
- data/lib/active_record_slave/active_record_slave.rb +44 -47
- data/lib/active_record_slave/errors.rb +5 -0
- data/lib/active_record_slave/instance_methods.rb +35 -0
- data/lib/active_record_slave/railtie.rb +2 -3
- data/lib/active_record_slave/version.rb +2 -2
- data/test/active_record_slave_test.rb +4 -0
- metadata +6 -10
- data/test/test.sqlite3 +0 -0
- data/test/test_slave.sqlite3 +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 28bf8e5c0b1aa126621369c792755c5bf4c3da94d7aecbd1f60af335908b8078
|
4
|
+
data.tar.gz: 959fa6afaecb8c24934085e2e5b1a018891286b3e0234d293a6cb0a27ca75f9d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
141
|
-
|
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/).
|
data/lib/active_record_slave.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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)
|
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
|
@@ -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
|
2
|
-
class Railtie < Rails::Railtie
|
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
|
2
|
-
VERSION = '1.
|
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
|
+
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:
|
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
|
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
|
-
|
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
|
data/test/test_slave.sqlite3
DELETED
Binary file
|