active_record_slave 1.1.0 → 1.2.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/LICENSE.txt +1 -1
- data/README.md +34 -1
- data/lib/active_record_slave/active_record_slave.rb +57 -25
- data/lib/active_record_slave/instance_methods.rb +28 -10
- data/lib/active_record_slave/railtie.rb +16 -1
- data/lib/active_record_slave/version.rb +1 -1
- data/test/active_record_slave_test.rb +32 -0
- data/test/database.yml +2 -2
- data/test/test.sqlite3 +0 -0
- data/test/test_slave.sqlite3 +0 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36a8b052ee4be85af18beb8a350ddd263cb4e1f2
|
4
|
+
data.tar.gz: 81d762a267ec60c7262d37a24833f55adbfaf157
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1a1feef3f440cd51d70f227f5f3cafda84468856cf273b43761eb23869290ed70a3246ed4b3c90c790c86b27470637aa24292ca5cabb2a6d40f8a6c566adafd7
|
7
|
+
data.tar.gz: 12c4a82745181c284ed244e31e0e1a103d42e7a93a251b3ac9ea6680a4a7392501bb2f98f07c23ac26974f6fbe7c33823a4621fc68bba3fa67d3d4b1d77b0513
|
data/LICENSE.txt
CHANGED
@@ -186,7 +186,7 @@
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
187
187
|
identification within third-party archives.
|
188
188
|
|
189
|
-
Copyright 2012
|
189
|
+
Copyright 2012, 2013, 2014 Reid Morrison
|
190
190
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
192
192
|
you may not use this file except in compliance with the License.
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
active_record_slave
|
1
|
+
active_record_slave [](http://travis-ci.org/reidmorrison/active_record_slave)
|
2
2
|
===================
|
3
3
|
|
4
4
|
ActiveRecord drop-in solution to efficiently redirect reads to slave databases
|
@@ -91,6 +91,39 @@ D, [2012-11-06T19:43:26.891667 #89002] DEBUG -- : SQL (0.4ms) DELETE FROM "us
|
|
91
91
|
D, [2012-11-06T19:43:26.892697 #89002] DEBUG -- : (0.9ms) commit transaction
|
92
92
|
```
|
93
93
|
|
94
|
+
## Transactions
|
95
|
+
|
96
|
+
By default ActiveRecordSlave detects when a call is inside a transaction and will
|
97
|
+
send all reads to the _master_ when a transaction is active.
|
98
|
+
|
99
|
+
With the latest Rails releases, Rails automatically wraps all Controller Action
|
100
|
+
calls with a transaction, effectively sending all reads to the master database.
|
101
|
+
|
102
|
+
It is now possible to send reads to database slaves and ignore whether currently
|
103
|
+
inside a transaction:
|
104
|
+
|
105
|
+
In file config/application.rb:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
# Read from slave even when in an active transaction
|
109
|
+
config.active_record_slave.ignore_transactions = true
|
110
|
+
```
|
111
|
+
|
112
|
+
It is important to identify any code in the application that depends on being
|
113
|
+
able to read any changes already part of the transaction, but not yet committed
|
114
|
+
and wrap those reads with `ActiveRecordSlave.read_from_master`
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
# Create a new inquiry
|
118
|
+
Inquiry.create
|
119
|
+
|
120
|
+
# Then make sure that the new inquiry that is not yet committed is visible during
|
121
|
+
# the read below:
|
122
|
+
ActiveRecordSlave.read_from_master do
|
123
|
+
count = Inquiry.count
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
94
127
|
## Dependencies
|
95
128
|
|
96
129
|
* Tested on Rails 3 and Rails 4
|
@@ -25,9 +25,11 @@ module ActiveRecordSlave
|
|
25
25
|
# Inject a new #select method into the ActiveRecord Database adapter
|
26
26
|
base = adapter_class || ActiveRecord::Base.connection.class
|
27
27
|
base.send(:include, InstanceMethods)
|
28
|
-
|
28
|
+
SELECT_METHODS.each do |select_method|
|
29
|
+
base.alias_method_chain(select_method, :slave_reader)
|
30
|
+
end
|
29
31
|
else
|
30
|
-
ActiveRecord::Base.logger.info "ActiveRecordSlave no slave database defined"
|
32
|
+
ActiveRecord::Base.logger.info "ActiveRecordSlave not installed since no slave database defined"
|
31
33
|
end
|
32
34
|
end
|
33
35
|
|
@@ -45,37 +47,67 @@ module ActiveRecordSlave
|
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
48
|
-
|
49
|
-
|
50
|
+
# Whether this thread is currently forcing all reads to go against the master database
|
51
|
+
def self.read_from_master?
|
52
|
+
thread_variable_get(:active_record_slave) == :master
|
53
|
+
end
|
50
54
|
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
+
# Force all subsequent reads on this thread and any fibers called by this thread to go the master
|
56
|
+
def self.read_from_master!
|
57
|
+
thread_variable_set(:active_record_slave, :master)
|
58
|
+
end
|
55
59
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
+
# Subsequent reads on this thread and any fibers called by this thread can go to a slave
|
61
|
+
def self.read_from_slave!
|
62
|
+
thread_variable_set(:active_record_slave, nil)
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns whether slave reads are ignoring transactions
|
66
|
+
def self.ignore_transactions?
|
67
|
+
@ignore_transactions
|
68
|
+
end
|
69
|
+
|
70
|
+
# Set whether slave reads should ignore transactions
|
71
|
+
def self.ignore_transactions=(ignore_transactions)
|
72
|
+
@ignore_transactions = ignore_transactions
|
73
|
+
end
|
60
74
|
|
61
|
-
|
62
|
-
|
63
|
-
|
75
|
+
##############################################################################
|
76
|
+
private
|
77
|
+
|
78
|
+
@ignore_transactions = false
|
79
|
+
|
80
|
+
# Returns the value of the local thread variable
|
81
|
+
#
|
82
|
+
# Parameters
|
83
|
+
# variable [Symbol]
|
84
|
+
# Name of variable to get
|
85
|
+
if RUBY_VERSION.to_i >= 2
|
86
|
+
# Fibers have their own thread local variables so use thread_variable_get
|
87
|
+
def self.thread_variable_get(variable)
|
88
|
+
Thread.current.thread_variable_get(variable)
|
64
89
|
end
|
65
90
|
else
|
66
|
-
|
67
|
-
|
68
|
-
Thread.current[:active_record_slave] == :master
|
91
|
+
def self.thread_variable_get(variable)
|
92
|
+
Thread.current[variable]
|
69
93
|
end
|
94
|
+
end
|
70
95
|
|
71
|
-
|
72
|
-
|
73
|
-
|
96
|
+
# Sets the value of the local thread variable
|
97
|
+
#
|
98
|
+
# Parameters
|
99
|
+
# variable [Symbol]
|
100
|
+
# Name of variable to set
|
101
|
+
# value [Object]
|
102
|
+
# Value to set the thread variable to
|
103
|
+
if RUBY_VERSION.to_i >= 2
|
104
|
+
# Fibers have their own thread local variables so use thread_variable_set
|
105
|
+
def self.thread_variable_set(variable, value)
|
106
|
+
Thread.current.thread_variable_set(variable, value)
|
74
107
|
end
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
Thread.current[:active_record_slave] = nil
|
108
|
+
else
|
109
|
+
def self.thread_variable_set(variable, value)
|
110
|
+
Thread.current[variable] = value
|
79
111
|
end
|
80
112
|
end
|
81
113
|
|
@@ -1,17 +1,35 @@
|
|
1
1
|
module ActiveRecordSlave
|
2
|
-
|
2
|
+
# Select Methods
|
3
|
+
SELECT_METHODS = [:select, :select_all, :select_one, :select_rows, :select_value, :select_values]
|
4
|
+
|
5
|
+
# In case in the future we are forced to intercept connection#execute if the
|
6
|
+
# above select methods are not sufficient
|
7
|
+
# SQL_READS = /\A\s*(SELECT|WITH|SHOW|CALL|EXPLAIN|DESCRIBE)/i
|
3
8
|
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
9
|
+
module InstanceMethods
|
10
|
+
SELECT_METHODS.each do |select_method|
|
11
|
+
# Database Adapter method #exec_query is called for every select call
|
12
|
+
# Replace #exec_query with one that calls the slave connection instead
|
13
|
+
eval <<-METHOD
|
14
|
+
def #{select_method}_with_slave_reader(sql, name = nil, *args)
|
15
|
+
if active_record_slave_read_from_master?
|
16
|
+
#{select_method}_without_slave_reader(sql, name, *args)
|
17
|
+
else
|
18
|
+
# Calls are going against the Slave now, prevent an infinite loop
|
19
|
+
ActiveRecordSlave.read_from_master do
|
20
|
+
Slave.connection.#{select_method}(sql, "Slave: \#{name || 'SQL'}", *args)
|
21
|
+
end
|
11
22
|
end
|
12
|
-
else
|
13
|
-
select_without_slave_reader(sql, name, *args)
|
14
23
|
end
|
24
|
+
METHOD
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns whether to read from the master database
|
28
|
+
def active_record_slave_read_from_master?
|
29
|
+
# Read from master when forced by thread variable, or
|
30
|
+
# in a transaction and not ignoring transactions
|
31
|
+
ActiveRecordSlave.read_from_master? ||
|
32
|
+
(open_transactions > 0) && !ActiveRecordSlave.ignore_transactions?
|
15
33
|
end
|
16
34
|
|
17
35
|
end
|
@@ -1,8 +1,23 @@
|
|
1
1
|
module ActiveRecordSlave #:nodoc:
|
2
2
|
class Railtie < Rails::Railtie #:nodoc:
|
3
3
|
|
4
|
+
# Make the ActiveRecordSlave configuration available in the Rails application config
|
5
|
+
#
|
6
|
+
# Example: For this application ignore the current transactions since the application
|
7
|
+
# has been coded to use ActiveRecordSlave.read_from_master whenever
|
8
|
+
# the current transaction must be visible to reads.
|
9
|
+
# In file config/application.rb
|
10
|
+
#
|
11
|
+
# Rails::Application.configure do
|
12
|
+
# # Read from slave even when in an active transaction
|
13
|
+
# # The application will use ActiveRecordSlave.read_from_master to make
|
14
|
+
# # changes in the current transaction visible to reads
|
15
|
+
# config.active_record_slave.ignore_transactions = true
|
16
|
+
# end
|
17
|
+
config.active_record_slave = ::ActiveRecordSlave
|
18
|
+
|
4
19
|
# Initialize ActiveRecordSlave
|
5
|
-
initializer "load active_record_slave" , :after=>"active_record.initialize_database" do
|
20
|
+
initializer "load active_record_slave" , :after => "active_record.initialize_database" do
|
6
21
|
ActiveRecordSlave.install!
|
7
22
|
end
|
8
23
|
|
@@ -52,6 +52,8 @@ class ActiveRecordSlaveTest < Test::Unit::TestCase
|
|
52
52
|
context 'the active_record_slave gem' do
|
53
53
|
|
54
54
|
setup do
|
55
|
+
ActiveRecordSlave.ignore_transactions = false
|
56
|
+
|
55
57
|
User.delete_all
|
56
58
|
|
57
59
|
@name = "Joe Bloggs"
|
@@ -89,7 +91,12 @@ class ActiveRecordSlaveTest < Test::Unit::TestCase
|
|
89
91
|
end
|
90
92
|
|
91
93
|
should "save to master, read from master when in a transaction" do
|
94
|
+
assert_equal false, ActiveRecordSlave.ignore_transactions?
|
95
|
+
|
92
96
|
User.transaction do
|
97
|
+
# The delete_all in setup should have cleared the table
|
98
|
+
assert_equal 0, User.count
|
99
|
+
|
93
100
|
# Read from Master
|
94
101
|
assert_equal 0, User.where(:name => @name, :address => @address).count
|
95
102
|
|
@@ -99,6 +106,31 @@ class ActiveRecordSlaveTest < Test::Unit::TestCase
|
|
99
106
|
# Read from Master
|
100
107
|
assert_equal 1, User.where(:name => @name, :address => @address).count
|
101
108
|
end
|
109
|
+
|
110
|
+
# Read from Non-replicated slave
|
111
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
112
|
+
end
|
113
|
+
|
114
|
+
should "save to master, read from slave when ignoring transactions" do
|
115
|
+
ActiveRecordSlave.ignore_transactions = true
|
116
|
+
assert_equal true, ActiveRecordSlave.ignore_transactions?
|
117
|
+
|
118
|
+
User.transaction do
|
119
|
+
# The delete_all in setup should have cleared the table
|
120
|
+
assert_equal 0, User.count
|
121
|
+
|
122
|
+
# Read from Master
|
123
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
124
|
+
|
125
|
+
# Write to master
|
126
|
+
assert_equal true, @user.save!
|
127
|
+
|
128
|
+
# Read from Non-replicated slave
|
129
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
130
|
+
end
|
131
|
+
|
132
|
+
# Read from Non-replicated slave
|
133
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
102
134
|
end
|
103
135
|
|
104
136
|
should "save to master, force a read from master even when _not_ in a transaction" do
|
data/test/database.yml
CHANGED
@@ -1,12 +1,12 @@
|
|
1
1
|
test:
|
2
2
|
adapter: sqlite3
|
3
|
-
database: test/
|
3
|
+
database: test/test.sqlite3
|
4
4
|
pool: 5
|
5
5
|
timeout: 5000
|
6
6
|
# Make the slave a separate database that is not slaved to ensure reads
|
7
7
|
# and writes go to the appropriate databases
|
8
8
|
slave:
|
9
9
|
adapter: sqlite3
|
10
|
-
database: test/
|
10
|
+
database: test/test_slave.sqlite3
|
11
11
|
pool: 5
|
12
12
|
timeout: 5000
|
data/test/test.sqlite3
ADDED
Binary file
|
Binary file
|
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.2.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: 2014-
|
11
|
+
date: 2014-07-24 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: sync_attr
|
@@ -56,6 +56,8 @@ files:
|
|
56
56
|
- lib/active_record_slave/version.rb
|
57
57
|
- test/active_record_slave_test.rb
|
58
58
|
- test/database.yml
|
59
|
+
- test/test.sqlite3
|
60
|
+
- test/test_slave.sqlite3
|
59
61
|
homepage: https://github.com/reidmorrison/active_record_slave
|
60
62
|
licenses:
|
61
63
|
- Apache License V2.0
|
@@ -83,3 +85,5 @@ summary: ActiveRecord drop-in solution to efficiently redirect reads to slave da
|
|
83
85
|
test_files:
|
84
86
|
- test/active_record_slave_test.rb
|
85
87
|
- test/database.yml
|
88
|
+
- test/test.sqlite3
|
89
|
+
- test/test_slave.sqlite3
|