active_record_slave 0.2.0 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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>
|