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 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