active_record_slave 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 85a394f332c93d140dda4319bd811a50332bbb3a
4
- data.tar.gz: 9eb811c94d7d43de483569245d587530a7a4725f
3
+ metadata.gz: 36a8b052ee4be85af18beb8a350ddd263cb4e1f2
4
+ data.tar.gz: 81d762a267ec60c7262d37a24833f55adbfaf157
5
5
  SHA512:
6
- metadata.gz: 00eb5c81081b4c205843f7a65aa7fef94f4e6614f35dfb662b0832bb2ec79813dd25fc50edb7aaa8cd612d545bf0eeb9053383d6653ee021d22fe702bce13402
7
- data.tar.gz: 3e9ffeb4eb9cbe4bfea45ad2450af2c420a49b4095680a45baaa9e607d1fd49fed600d57f9b24be673da6698518eb75c36df0eed497d98251071605e3762f47f
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 Clarity Services, Inc.
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 [![Build Status](https://secure.travis-ci.org/reidmorrison/active_record_slave.png?branch=master)](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
- base.alias_method_chain(:select, :slave_reader)
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
- if RUBY_VERSION.to_i >= 2
49
- # Fibers have their own thread local variables so use thread_variable_get
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
- # Whether this thread is currently forcing all reads to go against the master database
52
- def self.read_from_master?
53
- Thread.current.thread_variable_get(:active_record_slave) == :master
54
- end
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
- # Force all subsequent reads on this thread and any fibers called by this thread to go the master
57
- def self.read_from_master!
58
- Thread.current.thread_variable_set(:active_record_slave, :master)
59
- end
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
- # Subsequent reads on this thread and any fibers called by this thread can go to a slave
62
- def self.read_from_slave!
63
- Thread.current.thread_variable_set(:active_record_slave, nil)
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
- # Whether this thread is currently forcing all reads to go against the master database
67
- def self.read_from_master?
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
- # Force all subsequent reads on this thread and any fibers called by this thread to go the master
72
- def self.read_from_master!
73
- Thread.current[:active_record_slave] = :master
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
- # Subsequent reads on this thread and any fibers called by this thread can go to a slave
77
- def self.read_from_slave!
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
- module InstanceMethods
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
- # Database Adapter method #select is called for every select call
5
- # Replace #select with one that calls the slave connection instead
6
- def select_with_slave_reader(sql, name = nil, *args)
7
- # Only read from slave when not in a transaction and when this is not already the slave connection
8
- if (open_transactions == 0) && !ActiveRecordSlave.read_from_master?
9
- ActiveRecordSlave.read_from_master do
10
- Slave.connection.select(sql, "Slave: #{name || 'SQL'}", *args)
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
 
@@ -1,3 +1,3 @@
1
1
  module ActiveRecordSlave #:nodoc
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -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/db/test.sqlite3
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/db/test_slave.sqlite3
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.1.0
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-04-28 00:00:00.000000000 Z
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