active_record_slave 0.2.0 → 1.0.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.
- data/Gemfile +15 -0
- data/Gemfile.lock +34 -0
- data/README.md +44 -1
- data/Rakefile +1 -1
- data/lib/active_record_slave/active_record_slave.rb +18 -5
- data/lib/active_record_slave/instance_methods.rb +3 -3
- data/lib/active_record_slave/version.rb +1 -1
- data/test/active_record_slave_test.rb +121 -0
- data/test/database.yml +12 -0
- metadata +30 -46
- data/active_record_slave-0.1.0.gem +0 -0
- data/nbproject/private/config.properties +0 -0
- data/nbproject/private/private.properties +0 -3
- data/nbproject/private/private.xml +0 -4
- data/nbproject/private/rake-d.txt +0 -4
- data/nbproject/project.properties +0 -8
- data/nbproject/project.xml +0 -15
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
activemodel (3.2.8)
|
5
|
+
activesupport (= 3.2.8)
|
6
|
+
builder (~> 3.0.0)
|
7
|
+
activerecord (3.2.8)
|
8
|
+
activemodel (= 3.2.8)
|
9
|
+
activesupport (= 3.2.8)
|
10
|
+
arel (~> 3.0.2)
|
11
|
+
tzinfo (~> 0.3.29)
|
12
|
+
activesupport (3.2.8)
|
13
|
+
i18n (~> 0.6)
|
14
|
+
multi_json (~> 1.0)
|
15
|
+
arel (3.0.2)
|
16
|
+
builder (3.0.4)
|
17
|
+
i18n (0.6.1)
|
18
|
+
multi_json (1.3.7)
|
19
|
+
shoulda (3.3.2)
|
20
|
+
shoulda-context (~> 1.0.1)
|
21
|
+
shoulda-matchers (~> 1.4.1)
|
22
|
+
shoulda-context (1.0.1)
|
23
|
+
shoulda-matchers (1.4.1)
|
24
|
+
activesupport (>= 3.0.0)
|
25
|
+
sqlite3 (1.3.6)
|
26
|
+
tzinfo (0.3.35)
|
27
|
+
|
28
|
+
PLATFORMS
|
29
|
+
ruby
|
30
|
+
|
31
|
+
DEPENDENCIES
|
32
|
+
activerecord
|
33
|
+
shoulda
|
34
|
+
sqlite3
|
data/README.md
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
active_record_slave
|
2
2
|
===================
|
3
3
|
|
4
|
+
ActiveRecord drop-in solution to efficiently redirect reads to slave databases
|
5
|
+
|
4
6
|
* http://github.com/ClarityServices/active_record_slave
|
5
7
|
|
6
8
|
## Introduction
|
@@ -13,6 +15,7 @@ database to ensure data consistency.
|
|
13
15
|
## Features
|
14
16
|
|
15
17
|
* Redirecting reads to a single slave database
|
18
|
+
* Works with any database driver that works with ActiveRecord
|
16
19
|
* Supports all Rails 3 read apis, including dynamic finders, AREL, and ActiveRecord::Base.select
|
17
20
|
* Transaction aware. Detects when a query is inside of a transaction and sends
|
18
21
|
those reads to the master
|
@@ -47,6 +50,39 @@ Log file output:
|
|
47
50
|
03-13-12 06:02:09 pm,[2608],b[0],[0], Role Load (2.0ms) SELECT `roles`.* FROM `roles` WHERE `roles`.`name` = 'manager' LIMIT 1
|
48
51
|
03-13-12 06:02:09 pm,[2608],b[0],[0], AREL (2.0ms) UPDATE `roles` SET `description` = 'Manager' WHERE `roles`.`id` = 4
|
49
52
|
|
53
|
+
### Forcing a read against the master
|
54
|
+
|
55
|
+
Sometimes it is necessary to read from the master:
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
ActiveRecordSlave.read_from_master do
|
59
|
+
r = Role.where(:name => "manager").first
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
## Usage Notes
|
64
|
+
|
65
|
+
### delete_all
|
66
|
+
|
67
|
+
Delete all executes against the master database since it is only a delete:
|
68
|
+
|
69
|
+
```
|
70
|
+
D, [2012-11-06T19:47:29.125932 #89772] DEBUG -- : SQL (1.0ms) DELETE FROM "users"
|
71
|
+
```
|
72
|
+
|
73
|
+
### destroy_all
|
74
|
+
|
75
|
+
First performs a read against the slave database and then deletes the corresponding
|
76
|
+
data from the master
|
77
|
+
|
78
|
+
```
|
79
|
+
D, [2012-11-06T19:43:26.890674 #89002] DEBUG -- : Slave: User Load (0.1ms) SELECT "users".* FROM "users"
|
80
|
+
D, [2012-11-06T19:43:26.890972 #89002] DEBUG -- : (0.0ms) begin transaction
|
81
|
+
D, [2012-11-06T19:43:26.891667 #89002] DEBUG -- : SQL (0.4ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 3]]
|
82
|
+
D, [2012-11-06T19:43:26.892697 #89002] DEBUG -- : (0.9ms) commit transaction
|
83
|
+
```
|
84
|
+
|
85
|
+
|
50
86
|
## Requirements
|
51
87
|
|
52
88
|
* ActiveRecord 3 or greater (Rails 3 or greater)
|
@@ -114,9 +150,16 @@ slave reads only on the linux host 'batch':
|
|
114
150
|
pool: 20
|
115
151
|
<% end %>
|
116
152
|
|
153
|
+
## Tests
|
154
|
+
|
155
|
+
* active_record_slave is running in a large production system and was tested
|
156
|
+
directly as part of that solution
|
157
|
+
* I welcome anyone that can submit tests to verify this gem in a standalone outside
|
158
|
+
environment
|
159
|
+
|
117
160
|
## Possible Future Enhancements
|
118
161
|
|
119
|
-
* Support multiple slaves (ask for it by submitting a ticket)
|
162
|
+
* Support for multiple slaves (ask for it by submitting a ticket)
|
120
163
|
|
121
164
|
Meta
|
122
165
|
----
|
data/Rakefile
CHANGED
@@ -18,7 +18,7 @@ task :gem do |t|
|
|
18
18
|
s.date = Date.today.to_s
|
19
19
|
s.summary = "ActiveRecord read from slave"
|
20
20
|
s.description = "ActiveRecordSlave is a library to seamlessly enable reading from database slaves in a Rails 3 project, written in Ruby."
|
21
|
-
s.files = FileList["./**/*"].exclude(
|
21
|
+
s.files = FileList["./**/*"].exclude(/.gem$/, /.log$/,/nbproject/,/sqlite3$/).map{|f| f.sub(/^\.\//, '')}
|
22
22
|
s.has_rdoc = true
|
23
23
|
#s.add_dependency 'activerecord', '>= 3.0.0'
|
24
24
|
end
|
@@ -4,17 +4,30 @@
|
|
4
4
|
module ActiveRecordSlave
|
5
5
|
|
6
6
|
# Install ActiveRecord::Slave into ActiveRecord to redirect reads to the slave
|
7
|
-
#
|
8
|
-
#
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# Parameters:
|
8
|
+
# adapter_class:
|
9
|
+
# By default, only the default Database adapter (ActiveRecord::Base.connection.class)
|
10
|
+
# is extended with slave read capabilities
|
11
|
+
#
|
12
|
+
# environment:
|
13
|
+
# In a non-Rails environment, supply the environment such as
|
14
|
+
# 'development', 'production'
|
15
|
+
def self.install!(adapter_class = nil, environment = nil)
|
16
|
+
slave_config = if ActiveRecord::Base.connection.respond_to?(:config)
|
17
|
+
ActiveRecord::Base.connection.config[:slave]
|
18
|
+
else
|
19
|
+
ActiveRecord::Base.configurations[environment || Rails.env]['slave']
|
20
|
+
end
|
21
|
+
if slave_config
|
22
|
+
ActiveRecord::Base.logger.info "ActiveRecordSlave.install! v#{ActiveRecordSlave::VERSION} Establishing connection to slave database"
|
12
23
|
Slave.establish_connection(slave_config)
|
13
24
|
|
14
25
|
# Inject a new #select method into the ActiveRecord Database adapter
|
15
26
|
base = adapter_class || ActiveRecord::Base.connection.class
|
16
27
|
base.send(:include, InstanceMethods)
|
17
28
|
base.alias_method_chain(:select, :slave_reader)
|
29
|
+
else
|
30
|
+
ActiveRecord::Base.logger.info "ActiveRecordSlave no slave database defined"
|
18
31
|
end
|
19
32
|
end
|
20
33
|
|
@@ -3,14 +3,14 @@ module ActiveRecordSlave
|
|
3
3
|
|
4
4
|
# Database Adapter method #select is called for every select call
|
5
5
|
# Replace #select with one that calls the slave connection instead
|
6
|
-
def select_with_slave_reader(sql, name = nil)
|
6
|
+
def select_with_slave_reader(sql, name = nil, *args)
|
7
7
|
# Only read from slave when not in a transaction and when this is not already the slave connection
|
8
8
|
if (open_transactions == 0) && (Thread.current[:active_record_slave] != :master)
|
9
9
|
ActiveRecordSlave.read_from_master do
|
10
|
-
Slave.connection.select(sql, "Slave: #{name || 'SQL'}")
|
10
|
+
Slave.connection.select(sql, "Slave: #{name || 'SQL'}", *args)
|
11
11
|
end
|
12
12
|
else
|
13
|
-
select_without_slave_reader(sql, name)
|
13
|
+
select_without_slave_reader(sql, name, *args)
|
14
14
|
end
|
15
15
|
end
|
16
16
|
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# Allow examples to be run in-place without requiring a gem install
|
2
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'logger'
|
6
|
+
require 'erb'
|
7
|
+
require 'test/unit'
|
8
|
+
require 'shoulda'
|
9
|
+
require 'active_record'
|
10
|
+
require 'active_record_slave'
|
11
|
+
|
12
|
+
l = Logger.new('test.log')
|
13
|
+
l.level = ::Logger::DEBUG
|
14
|
+
ActiveRecord::Base.logger = l
|
15
|
+
ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read('test/database.yml')).result)
|
16
|
+
|
17
|
+
# Define Schema in second database (slave)
|
18
|
+
# Note: This is not be required when the master database is being replicated to the slave db
|
19
|
+
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']['slave'])
|
20
|
+
|
21
|
+
# Create table users in database active_record_slave_test
|
22
|
+
ActiveRecord::Schema.define :version => 0 do
|
23
|
+
create_table :users, :force => true do |t|
|
24
|
+
t.string :name
|
25
|
+
t.string :address
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Define Schema in master database
|
30
|
+
ActiveRecord::Base.establish_connection(:test)
|
31
|
+
|
32
|
+
# Create table users in database active_record_slave_test
|
33
|
+
ActiveRecord::Schema.define :version => 0 do
|
34
|
+
create_table :users, :force => true do |t|
|
35
|
+
t.string :name
|
36
|
+
t.string :address
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
# AR Model
|
41
|
+
class User < ActiveRecord::Base
|
42
|
+
end
|
43
|
+
|
44
|
+
# Install ActiveRecord slave. Done automatically by railtie in a Rails environment
|
45
|
+
# Also tell it to use the test environment since Rails.env is not available
|
46
|
+
ActiveRecordSlave.install!(nil, 'test')
|
47
|
+
|
48
|
+
#
|
49
|
+
# Unit Test for active_record_slave
|
50
|
+
#
|
51
|
+
class ActiveRecordSlaveTest < Test::Unit::TestCase
|
52
|
+
context 'the active_record_slave gem' do
|
53
|
+
|
54
|
+
setup do
|
55
|
+
User.delete_all
|
56
|
+
|
57
|
+
@name = "Joe Bloggs"
|
58
|
+
@address = "Somewhere"
|
59
|
+
@user = User.new(
|
60
|
+
:name => @name,
|
61
|
+
:address => @address
|
62
|
+
)
|
63
|
+
end
|
64
|
+
|
65
|
+
teardown do
|
66
|
+
User.delete_all
|
67
|
+
end
|
68
|
+
|
69
|
+
should "save to master" do
|
70
|
+
assert_equal true, @user.save!
|
71
|
+
end
|
72
|
+
|
73
|
+
#
|
74
|
+
# NOTE:
|
75
|
+
#
|
76
|
+
# There is no automated replication between the SQL lite databases
|
77
|
+
# so the tests will be verifying that reads going to the "slave" (second)
|
78
|
+
# database do not find data written to the master.
|
79
|
+
#
|
80
|
+
should "save to master, read from slave" do
|
81
|
+
# Read from slave
|
82
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
83
|
+
|
84
|
+
# Write to master
|
85
|
+
assert_equal true, @user.save!
|
86
|
+
|
87
|
+
# Read from slave
|
88
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
89
|
+
end
|
90
|
+
|
91
|
+
should "save to master, read from master when in a transaction" do
|
92
|
+
User.transaction do
|
93
|
+
# Read from Master
|
94
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
95
|
+
|
96
|
+
# Write to master
|
97
|
+
assert_equal true, @user.save!
|
98
|
+
|
99
|
+
# Read from Master
|
100
|
+
assert_equal 1, User.where(:name => @name, :address => @address).count
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
should "save to master, force a read from master even when _not_ in a transaction" do
|
105
|
+
# Read from slave
|
106
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
107
|
+
|
108
|
+
# Write to master
|
109
|
+
assert_equal true, @user.save!
|
110
|
+
|
111
|
+
# Read from slave
|
112
|
+
assert_equal 0, User.where(:name => @name, :address => @address).count
|
113
|
+
|
114
|
+
# Read from Master
|
115
|
+
ActiveRecordSlave.read_from_master do
|
116
|
+
assert_equal 1, User.where(:name => @name, :address => @address).count
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
end
|
data/test/database.yml
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
test:
|
2
|
+
adapter: sqlite3
|
3
|
+
database: test/db/test.sqlite3
|
4
|
+
pool: 5
|
5
|
+
timeout: 5000
|
6
|
+
# Make the slave a separate database that is not slaved to ensure reads
|
7
|
+
# and writes go to the appropriate databases
|
8
|
+
slave:
|
9
|
+
adapter: sqlite3
|
10
|
+
database: test/db/test_slave.sqlite3
|
11
|
+
pool: 5
|
12
|
+
timeout: 5000
|
metadata
CHANGED
@@ -1,34 +1,26 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_record_slave
|
3
|
-
version: !ruby/object:Gem::Version
|
4
|
-
|
5
|
-
|
6
|
-
- 0
|
7
|
-
- 2
|
8
|
-
- 0
|
9
|
-
version: 0.2.0
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
prerelease:
|
10
6
|
platform: ruby
|
11
|
-
authors:
|
7
|
+
authors:
|
12
8
|
- Reid Morrison
|
13
9
|
autorequire:
|
14
10
|
bindir: bin
|
15
11
|
cert_chain: []
|
16
|
-
|
17
|
-
date: 2012-03-16 00:00:00 -04:00
|
18
|
-
default_executable:
|
12
|
+
date: 2012-11-06 00:00:00.000000000 Z
|
19
13
|
dependencies: []
|
20
|
-
|
21
|
-
|
22
|
-
email:
|
14
|
+
description: ActiveRecordSlave is a library to seamlessly enable reading from database
|
15
|
+
slaves in a Rails 3 project, written in Ruby.
|
16
|
+
email:
|
23
17
|
- reidmo@gmail.com
|
24
18
|
executables: []
|
25
|
-
|
26
19
|
extensions: []
|
27
|
-
|
28
20
|
extra_rdoc_files: []
|
29
|
-
|
30
|
-
|
31
|
-
-
|
21
|
+
files:
|
22
|
+
- Gemfile
|
23
|
+
- Gemfile.lock
|
32
24
|
- lib/active_record_slave/active_record_slave.rb
|
33
25
|
- lib/active_record_slave/instance_methods.rb
|
34
26
|
- lib/active_record_slave/railtie.rb
|
@@ -36,43 +28,35 @@ files:
|
|
36
28
|
- lib/active_record_slave/version.rb
|
37
29
|
- lib/active_record_slave.rb
|
38
30
|
- LICENSE.txt
|
39
|
-
- nbproject/private/config.properties
|
40
|
-
- nbproject/private/private.properties
|
41
|
-
- nbproject/private/private.xml
|
42
|
-
- nbproject/private/rake-d.txt
|
43
|
-
- nbproject/project.properties
|
44
|
-
- nbproject/project.xml
|
45
31
|
- Rakefile
|
46
32
|
- README.md
|
47
|
-
|
33
|
+
- test/active_record_slave_test.rb
|
34
|
+
- test/database.yml
|
48
35
|
homepage: https://github.com/ClarityServices/active_record_slave
|
49
36
|
licenses: []
|
50
|
-
|
51
37
|
post_install_message:
|
52
38
|
rdoc_options: []
|
53
|
-
|
54
|
-
require_paths:
|
39
|
+
require_paths:
|
55
40
|
- lib
|
56
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
41
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
segments:
|
61
48
|
- 0
|
62
|
-
|
63
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
version: "0"
|
49
|
+
hash: -2259159571471524106
|
50
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
70
56
|
requirements: []
|
71
|
-
|
72
57
|
rubyforge_project:
|
73
|
-
rubygems_version: 1.
|
58
|
+
rubygems_version: 1.8.24
|
74
59
|
signing_key:
|
75
60
|
specification_version: 3
|
76
61
|
summary: ActiveRecord read from slave
|
77
62
|
test_files: []
|
78
|
-
|
Binary file
|
File without changes
|
data/nbproject/project.xml
DELETED
@@ -1,15 +0,0 @@
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
2
|
-
<project xmlns="http://www.netbeans.org/ns/project/1">
|
3
|
-
<type>org.netbeans.modules.ruby.rubyproject</type>
|
4
|
-
<configuration>
|
5
|
-
<data xmlns="http://www.netbeans.org/ns/ruby-project/1">
|
6
|
-
<name>active_record_slave</name>
|
7
|
-
<source-roots>
|
8
|
-
<root id="src.lib.dir" name="Source Files"/>
|
9
|
-
</source-roots>
|
10
|
-
<test-roots>
|
11
|
-
<root id="test.test.dir" name="Test Files"/>
|
12
|
-
</test-roots>
|
13
|
-
</data>
|
14
|
-
</configuration>
|
15
|
-
</project>
|