standby 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6dca656d9e0b7b23ba371c78eece948316152faa75952dfba92567b4f6cd7e79
4
+ data.tar.gz: f8c9f94c39debceb101f69207ebefba5b1dd7092ec58f53713846782f87b32fc
5
+ SHA512:
6
+ metadata.gz: 93b848675402550dd940ee266f31f8c533ff27bf7f99663ff1e8c538d5dbec0a4c9e2615ec253a074f453cc89abfea95536af62640bff9e77dd326a3772db709
7
+ data.tar.gz: 48eb1b450fbf589cc5a13e65f5fdd2ed1689ad5a346e26670fead52b522abfa73d35659b97032651e2819a3d1dddbd3ac57463a1d876aa48dc9cb80dc8d6f8d6
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ gemfiles/*.lock
8
+ InstalledFiles
9
+ _yardoc
10
+ coverage
11
+ doc/
12
+ lib/bundler/man
13
+ pkg
14
+ rdoc
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
19
+
20
+ test_db
21
+ test_standby_one
22
+ test_standby_two
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,13 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.2.2
5
+ - 2.3.3
6
+
7
+ gemfile:
8
+ - Gemfile
9
+ - gemfiles/rails3.2.gemfile
10
+ - gemfiles/rails4.gemfile
11
+ - gemfiles/rails4.2.gemfile
12
+
13
+ script: bundle exec rspec spec
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec name: 'standby'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Kenn Ejima
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,189 @@
1
+ # Standby - Read from standby databases for ActiveRecord (formerly Slavery)
2
+
3
+ [![Build Status](https://travis-ci.org/kenn/slavery.svg)](https://travis-ci.org/kenn/slavery)
4
+
5
+ Standby is a simple, easy to use gem for ActiveRecord that enables conservative reading from standby databases, which means it won't automatically redirect all SELECTs to standbys.
6
+
7
+ Instead, you can do `Standby.on_standby { User.count }` to send a particular query to a standby.
8
+
9
+ Background: Probably your app started off with one single database. As it grows, you would upgrade to a primary-standby (or master-slave) replication for redundancy. At this point, all queries still go to the primary and standbys are just backups. With that configuration, it's tempting to run some long-running queries on one of the standbys. And that's exactly what Standby does.
10
+
11
+ * Conservative - Safe by default. Installing Standby won't change your app's current behavior.
12
+ * Future proof - No dirty hacks. Simply works as a proxy for `ActiveRecord::Base.connection`.
13
+ * Simple code - Intentionally small. You can read the entire source and completely stay in control.
14
+
15
+ Standby works with ActiveRecord 3 or later.
16
+
17
+ ## Install
18
+
19
+ Add this line to your application's Gemfile:
20
+
21
+ ```ruby
22
+ gem 'standby'
23
+ ```
24
+
25
+ And create standby configs for each environment.
26
+
27
+ ```yaml
28
+ development:
29
+ database: myapp_development
30
+
31
+ development_standby:
32
+ database: myapp_development
33
+ ```
34
+
35
+ By convention, config keys with `[env]_standby` are automatically used for standby reads.
36
+
37
+ Notice that we just copied the settings of `development` to `development_standby`. For `development` and `test`, it's actually recommended as probably you don't want to have replicating multiple databases on your machine. Two connections to the same identical database should be fine for testing purpose.
38
+
39
+ In case you prefer DRYer definition, YAML's aliasing and key merging might help.
40
+
41
+ ```yaml
42
+ common: &common
43
+ adapter: mysql2
44
+ username: root
45
+ database: myapp_development
46
+
47
+ development:
48
+ <<: *common
49
+
50
+ development_standby:
51
+ <<: *common
52
+ ```
53
+
54
+ Optionally, you can use a database url for your connections:
55
+
56
+ ```yaml
57
+ development: postgres://root:@localhost:5432/myapp_development
58
+ development_standby: postgres://root:@localhost:5432/myapp_development_standby
59
+ ```
60
+
61
+ At this point, Standby does nothing. Run tests and confirm that nothing is broken.
62
+
63
+ ## Usage
64
+
65
+ To start using Standby, you need to add `Standby.on_standby` in your code. Queries in the `Standby.on_standby` block run on the standby.
66
+
67
+ ```ruby
68
+ Standby.on_standby { User.count } # => runs on standby
69
+ Standby.on_standby(:two) { User.count } # => runs on another standby configured as `development_standby_two`
70
+ ```
71
+
72
+ You can nest `on_standby` and `on_primary` interchangeably. The following code works as expected.
73
+
74
+ ```ruby
75
+ Standby.on_standby do
76
+ ...
77
+ Standby.on_primary do
78
+ ...
79
+ end
80
+ ...
81
+ end
82
+ ```
83
+
84
+ Alternatively, you may call `on_standby` directly on the scope, so that the query will be read from standby when it's executed.
85
+
86
+ ```ruby
87
+ User.on_standby.where(active: true).count
88
+ ```
89
+
90
+ Caveat: `pluck` is not supported by the scope syntax, you still need `Standby.on_standby` in this case.
91
+
92
+ ## Read-only user
93
+
94
+ For an extra safeguard, it is recommended to use a read-only user for standby access.
95
+
96
+ ```yaml
97
+ development_standby:
98
+ <<: *common
99
+ username: readonly
100
+ ```
101
+
102
+ With MySQL, `GRANT SELECT` creates a read-only user.
103
+
104
+ ```SQL
105
+ GRANT SELECT ON *.* TO 'readonly'@'localhost';
106
+ ```
107
+
108
+ With this user, writes on a standby should raise an exception.
109
+
110
+ ```ruby
111
+ Standby.on_standby { User.create } # => ActiveRecord::StatementInvalid: Mysql2::Error: INSERT command denied...
112
+ ```
113
+
114
+ With Postgres you can set the entire database to be readonly:
115
+
116
+ ```SQL
117
+ ALTER DATABASE myapp_development_standby SET default_transaction_read_only = true;
118
+ ```
119
+
120
+ It is a good idea to confirm this behavior in your test code as well.
121
+
122
+ ## Disable temporarily
123
+
124
+ You can quickly disable standby reads by dropping the following line in `config/initializers/standby.rb`.
125
+
126
+ ```ruby
127
+ Standby.disabled = true
128
+ ```
129
+
130
+ With this line, Standby stops connection switching and all queries go to the primary.
131
+
132
+ This may be useful when one of the primary or the standby goes down. You would rewrite `database.yml` to make all queries go to the surviving database, until you restore or rebuild the failed one.
133
+
134
+ ## Transactional fixtures
135
+
136
+ When `use_transactional_fixtures` is set to `true`, it's NOT recommended to
137
+ write to the database besides fixtures, since the standby connection is not aware
138
+ of changes performed in the primary connection due to [transaction isolation](https://en.wikipedia.org/wiki/Isolation_(database_systems)).
139
+
140
+ In that case, you are suggested to disable Standby in the test environment by
141
+ putting the following in `test/test_helper.rb`
142
+ (or `spec/spec_helper.rb` for RSpec users):
143
+
144
+ ```ruby
145
+ Standby.disabled = true
146
+ ```
147
+
148
+ ## Upgrading from version 3 to version 4
149
+
150
+ The gem name has been changed from `slavery` to `standby`.
151
+
152
+ Update your Gemfile
153
+
154
+ ```ruby
155
+ gem 'standby'
156
+ ```
157
+
158
+ Replace `Slavery` with `Standby`, `on_slave` with `on_standby`, and `on_master` with `on_primary`.
159
+
160
+ ```sh
161
+ grep -e Slavery **/*.rake **/*.rb -s -l | xargs sed -i "" "s|Slavery|Standby|g"
162
+ grep -e on_slave **/*.rake **/*.rb -s -l | xargs sed -i "" "s|on_slave|on_standby|g"
163
+ grep -e on_master **/*.rake **/*.rb -s -l | xargs sed -i "" "s|on_master|on_primary|g"
164
+ ```
165
+
166
+ ## Upgrading from version 2 to version 3
167
+
168
+ Please note that `Standby.spec_key=` method has been removed from version 3.
169
+
170
+ ## Support for non-Rails apps
171
+
172
+ If you're using ActiveRecord in a non-Rails app (e.g. Sinatra), be sure to set `RACK_ENV` environment variable in the boot sequence, then:
173
+
174
+ ```ruby
175
+ require 'standby'
176
+
177
+ ActiveRecord::Base.configurations = {
178
+ 'development' => { adapter: 'mysql2', ... },
179
+ 'development_standby' => { adapter: 'mysql2', ... }
180
+ }
181
+ ActiveRecord::Base.establish_connection(:development)
182
+ ```
183
+
184
+ ## Changelog
185
+
186
+ * v4.0.0: Rename gem from Slavery to Standby
187
+ * v3.0.0: Support for multiple standby targets ([@punchh](https://github.com/punchh))
188
+ * v2.1.0: Debug log support / Database URL support / Rails 3.2 & 4.0 compatibility (Thanks to [@citrus](https://github.com/citrus))
189
+ * v2.0.0: Rails 5 support
data/Rakefile ADDED
@@ -0,0 +1,7 @@
1
+ # require 'bundler/gem_tasks'
2
+ Bundler::GemHelper.install_tasks(name: 'standby')
3
+
4
+ # RSpec
5
+ require 'rspec/core/rake_task'
6
+ RSpec::Core::RakeTask.new(:spec)
7
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative "../spec/spec_helper"
4
+ require "irb"
5
+
6
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT)
7
+
8
+ IRB.start
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'activerecord', '~> 3.2'
4
+
5
+ group :development, :test do
6
+ gem 'test-unit', '~> 3.0'
7
+ end
8
+
9
+ gemspec :path => '../'
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'activerecord', '~> 4.2'
4
+
5
+ gemspec :path => '../'
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem 'activerecord', '~> 4.0.0'
4
+
5
+ gemspec :path => '../'
data/lib/standby.rb ADDED
@@ -0,0 +1,29 @@
1
+ require 'active_record'
2
+ require 'standby/version'
3
+ require 'standby/base'
4
+ require 'standby/error'
5
+ require 'standby/connection_holder'
6
+ require 'standby/transaction'
7
+ require 'standby/active_record/base'
8
+ require 'standby/active_record/connection_handling'
9
+ require 'standby/active_record/relation'
10
+ require 'standby/active_record/log_subscriber'
11
+
12
+ module Standby
13
+ class << self
14
+ attr_accessor :disabled
15
+
16
+ def standby_connections
17
+ @standby_connections ||= {}
18
+ end
19
+
20
+ def on_standby(name = :null_state, &block)
21
+ raise Standby::Error.new('invalid standby target') unless name.is_a?(Symbol)
22
+ Base.new(name).run &block
23
+ end
24
+
25
+ def on_primary(&block)
26
+ Base.new(:primary).run &block
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,25 @@
1
+ module ActiveRecord
2
+ class Base
3
+ class << self
4
+ alias_method :connection_without_standby, :connection
5
+
6
+ def connection
7
+ case Thread.current[:_standby]
8
+ when :primary, NilClass
9
+ connection_without_standby
10
+ else
11
+ Standby.connection_holder(Thread.current[:_standby]).connection_without_standby
12
+ end
13
+ end
14
+
15
+ # Generate scope at top level e.g. User.on_standby
16
+ def on_standby(name = :null_state)
17
+ # Why where(nil)?
18
+ # http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
19
+ context = where(nil)
20
+ context.standby_target = name || :null_state
21
+ context
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,6 @@
1
+ module ActiveRecord
2
+ module ConnectionHandling
3
+ # Already defined in Rails 4.1+
4
+ RAILS_ENV ||= -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] }
5
+ end
6
+ end
@@ -0,0 +1,12 @@
1
+ module ActiveRecord
2
+ class LogSubscriber
3
+
4
+ alias_method :debug_without_standby, :debug
5
+
6
+ def debug(msg)
7
+ db = Standby.disabled ? "" : color("[#{Thread.current[:_standby] || "primary"}]", ActiveSupport::LogSubscriber::GREEN, true)
8
+ debug_without_standby(db + msg)
9
+ end
10
+
11
+ end
12
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ class Relation
3
+ attr_accessor :standby_target
4
+
5
+ # Supports queries like User.on_standby.to_a
6
+ alias_method :exec_queries_without_standby, :exec_queries
7
+
8
+ def exec_queries
9
+ if standby_target
10
+ Standby.on_standby(standby_target) { exec_queries_without_standby }
11
+ else
12
+ exec_queries_without_standby
13
+ end
14
+ end
15
+
16
+
17
+ # Supports queries like User.on_standby.count
18
+ alias_method :calculate_without_standby, :calculate
19
+
20
+ def calculate(*args)
21
+ if standby_target
22
+ Standby.on_standby(standby_target) { calculate_without_standby(*args) }
23
+ else
24
+ calculate_without_standby(*args)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,40 @@
1
+ module Standby
2
+ class Base
3
+ def initialize(target)
4
+ @target = decide_with(target)
5
+ end
6
+
7
+ def run(&block)
8
+ run_on @target, &block
9
+ end
10
+
11
+ private
12
+
13
+ def decide_with(target)
14
+ if Standby.disabled || target == :primary
15
+ :primary
16
+ elsif inside_transaction?
17
+ raise Standby::Error.new('on_standby cannot be used inside transaction block!')
18
+ elsif target == :null_state
19
+ :standby
20
+ elsif target.present?
21
+ "standby_#{target}".to_sym
22
+ else
23
+ raise Standby::Error.new('on_standby cannot be used with a nil target!')
24
+ end
25
+ end
26
+
27
+ def inside_transaction?
28
+ open_transactions = run_on(:primary) { ActiveRecord::Base.connection.open_transactions }
29
+ open_transactions > Standby::Transaction.base_depth
30
+ end
31
+
32
+ def run_on(target)
33
+ backup = Thread.current[:_standby] # Save for recursive nested calls
34
+ Thread.current[:_standby] = target
35
+ yield
36
+ ensure
37
+ Thread.current[:_standby] = backup
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,28 @@
1
+ module Standby
2
+ class ConnectionHolder < ActiveRecord::Base
3
+ self.abstract_class = true
4
+
5
+ class << self
6
+ # for delayed activation
7
+ def activate(target)
8
+ spec = ActiveRecord::Base.configurations["#{ActiveRecord::ConnectionHandling::RAILS_ENV.call}_#{target}"]
9
+ raise Error.new("Standby target '#{target}' is invalid!") if spec.nil?
10
+ establish_connection spec
11
+ end
12
+ end
13
+ end
14
+
15
+ class << self
16
+ def connection_holder(target)
17
+ klass_name = "Standby#{target.to_s.camelize}ConnectionHolder"
18
+ standby_connections[klass_name] ||= begin
19
+ klass = Class.new(Standby::ConnectionHolder) do
20
+ self.abstract_class = true
21
+ end
22
+ Object.const_set(klass_name, klass)
23
+ klass.activate(target)
24
+ klass
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,4 @@
1
+ module Standby
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,29 @@
1
+ module Standby
2
+ class Transaction
3
+ # The methods on ActiveSupport::TestCase which can potentially be used
4
+ # to determine if transactional fixtures are enabled
5
+ TEST_CONFIG_METHODS = [
6
+ :use_transactional_tests,
7
+ :use_transactional_fixtures
8
+ ]
9
+
10
+ class << self
11
+ def base_depth
12
+ @base_depth ||= if defined?(ActiveSupport::TestCase) &&
13
+ transactional_fixtures_enabled?
14
+ then
15
+ 1
16
+ else
17
+ 0
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def transactional_fixtures_enabled?
24
+ config = ActiveSupport::TestCase
25
+ TEST_CONFIG_METHODS.any? {|m| config.respond_to?(m) and config.send(m) }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,3 @@
1
+ module Standby
2
+ VERSION = '4.0.0'
3
+ end
data/slavery.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'standby/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.post_install_message = 'The slavery gem has been deprecated and has ' \
8
+ 'been replaced by standby. Please switch to ' \
9
+ 'standby as soon as possible.'
10
+ gem.name = 'slavery'
11
+ gem.version = Standby::VERSION
12
+ gem.authors = ['Kenn Ejima']
13
+ gem.email = ['kenn.ejima@gmail.com']
14
+ gem.description = %q{Simple, conservative slave reads for ActiveRecord}
15
+ gem.summary = %q{Simple, conservative slave reads for ActiveRecord}
16
+ gem.homepage = 'https://github.com/kenn/slavery'
17
+
18
+ gem.files = `git ls-files`.split($/)
19
+ gem.executables = gem.files.grep(%r{^exe/}).map{ |f| File.basename(f) }
20
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
21
+ gem.require_paths = ['lib']
22
+
23
+ gem.add_runtime_dependency 'activerecord', '>= 3.0.0'
24
+
25
+ gem.add_development_dependency 'rspec'
26
+ gem.add_development_dependency 'sqlite3'
27
+ end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+ require 'logger'
3
+
4
+ describe ActiveRecord::LogSubscriber do
5
+
6
+ describe 'logging' do
7
+
8
+ let(:log) { StringIO.new }
9
+ let(:logger) { Logger.new(log) }
10
+
11
+ before do
12
+ ActiveRecord::Base.logger = logger
13
+ @backup_disabled = Standby.disabled
14
+ end
15
+
16
+ after do
17
+ Standby.disabled = @backup_disabled
18
+ end
19
+
20
+ it 'it prefixes log messages with primary' do
21
+ User.count
22
+ log.rewind
23
+ expect(log.read).to include('[primary]')
24
+ end
25
+
26
+ it 'it prefixes log messages with the standby connection' do
27
+ User.on_standby.count
28
+ log.rewind
29
+ expect(log.read).to include('[standby]')
30
+ end
31
+
32
+ it 'it does nothing when standby is disabled' do
33
+ Standby.disabled = true
34
+ User.count
35
+ log.rewind
36
+ expect(log.read).to_not include('[primary]')
37
+ end
38
+
39
+ end
40
+
41
+ end
@@ -0,0 +1,34 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'configuration' do
4
+ before do
5
+ # Backup connection and configs
6
+ @backup_conn = Standby.instance_variable_get :@standby_connections
7
+ @backup_config = ActiveRecord::Base.configurations.dup
8
+ @backup_disabled = Standby.disabled
9
+ @backup_conn.each_key do |klass_name|
10
+ Object.send(:remove_const, klass_name) if Object.const_defined?(klass_name)
11
+ end
12
+ Standby.instance_variable_set :@standby_connections, {}
13
+ end
14
+
15
+ after do
16
+ # Restore connection and configs
17
+ Standby.instance_variable_set :@standby_connections, @backup_conn
18
+ ActiveRecord::Base.configurations = @backup_config
19
+ Standby.disabled = @backup_disabled
20
+ end
21
+
22
+ it 'raises error if standby configuration not specified' do
23
+ ActiveRecord::Base.configurations['test_standby'] = nil
24
+
25
+ expect { Standby.on_standby { User.count } }.to raise_error(Standby::Error)
26
+ end
27
+
28
+ it 'connects to primary if standby configuration is disabled' do
29
+ ActiveRecord::Base.configurations['test_standby'] = nil
30
+ Standby.disabled = true
31
+
32
+ expect(Standby.on_standby { User.count }).to be 2
33
+ end
34
+ end
@@ -0,0 +1,107 @@
1
+ require 'spec_helper'
2
+
3
+ describe Standby do
4
+ def standby_value
5
+ Thread.current[:_standby]
6
+ end
7
+
8
+ def on_standby?
9
+ standby_value == :standby
10
+ end
11
+
12
+ it 'sets thread local' do
13
+ Standby.on_primary { expect(standby_value).to be :primary }
14
+ Standby.on_standby { expect(standby_value).to be :standby }
15
+ Standby.on_standby(:two) { expect(standby_value).to be :standby_two}
16
+ end
17
+
18
+ it 'returns value from block' do
19
+ expect(Standby.on_primary { User.count }).to be 2
20
+ expect(Standby.on_standby { User.count }).to be 1
21
+ expect(Standby.on_standby(:two) { User.count }).to be 0
22
+ end
23
+
24
+ it 'handles nested calls' do
25
+ # Standby -> Standby
26
+ Standby.on_standby do
27
+ expect(on_standby?).to be true
28
+
29
+ Standby.on_standby do
30
+ expect(on_standby?).to be true
31
+ end
32
+
33
+ expect(on_standby?).to be true
34
+ end
35
+
36
+ # Standby -> Primary
37
+ Standby.on_standby do
38
+ expect(on_standby?).to be true
39
+
40
+ Standby.on_primary do
41
+ expect(on_standby?).to be false
42
+ end
43
+
44
+ expect(on_standby?).to be true
45
+ end
46
+ end
47
+
48
+ it 'raises error in transaction' do
49
+ User.transaction do
50
+ expect { Standby.on_standby { User.first } }.to raise_error(Standby::Error)
51
+ end
52
+ end
53
+
54
+ it 'disables by configuration' do
55
+ backup = Standby.disabled
56
+
57
+ Standby.disabled = false
58
+ Standby.on_standby { expect(standby_value).to be :standby }
59
+
60
+ Standby.disabled = true
61
+ Standby.on_standby { expect(standby_value).to be :primary }
62
+
63
+ Standby.disabled = backup
64
+ end
65
+
66
+ it 'avoids stack overflow with 3rdparty gem that defines alias_method. namely newrelic...' do
67
+ class ActiveRecord::Relation
68
+ alias_method :calculate_without_thirdparty, :calculate
69
+
70
+ def calculate(*args)
71
+ calculate_without_thirdparty(*args)
72
+ end
73
+ end
74
+
75
+ expect(User.count).to be 2
76
+
77
+ class ActiveRecord::Relation
78
+ alias_method :calculate, :calculate_without_thirdparty
79
+ end
80
+ end
81
+
82
+ it 'works with nils like standby' do
83
+ expect(User.on_standby(nil).count).to be User.on_standby.count
84
+ end
85
+
86
+ it 'raises on blanks and strings' do
87
+ expect { User.on_standby("").count }.to raise_error(Standby::Error)
88
+ expect { User.on_standby("two").count }.to raise_error(Standby::Error)
89
+ expect { User.on_standby("standby").count }.to raise_error(Standby::Error)
90
+ end
91
+
92
+ it 'raises with non existent extension' do
93
+ expect { Standby.on_standby(:non_existent) { User.first } }.to raise_error(Standby::Error)
94
+ end
95
+
96
+ it 'works with any scopes' do
97
+ expect(User.count).to be 2
98
+ expect(User.on_standby(:two).count).to be 0
99
+ expect(User.on_standby.count).to be 1
100
+
101
+ # Why where(nil)?
102
+ # http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
103
+ expect(User.where(nil).to_a.size).to be 2
104
+ expect(User.on_standby(:two).where(nil).to_a.size).to be 0
105
+ expect(User.on_standby.where(nil).to_a.size).to be 1
106
+ end
107
+ end
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ ENV['RACK_ENV'] = 'test'
5
+
6
+ require 'standby'
7
+
8
+ ActiveRecord::Base.configurations = {
9
+ 'test' => { 'adapter' => 'sqlite3', 'database' => 'test_db' },
10
+ 'test_standby' => { 'adapter' => 'sqlite3', 'database' => 'test_standby_one' },
11
+ 'test_standby_two' => { 'adapter' => 'sqlite3', 'database' => 'test_standby_two'},
12
+ 'test_standby_url' => 'postgres://root:@localhost:5432/test_standby'
13
+ }
14
+
15
+ # Prepare databases
16
+ class User < ActiveRecord::Base
17
+ has_many :items
18
+ end
19
+
20
+ class Item < ActiveRecord::Base
21
+ belongs_to :user
22
+ end
23
+
24
+ class Seeder
25
+ def run
26
+ # Populate on primary
27
+ connect(:test)
28
+ create_tables
29
+ User.create
30
+ User.create
31
+ User.first.items.create
32
+
33
+ # Populate on standby, emulating replication lag
34
+ connect(:test_standby)
35
+ create_tables
36
+ User.create
37
+
38
+ # Populate on standby two
39
+ connect(:test_standby_two)
40
+ create_tables
41
+
42
+ # Reconnect to primary
43
+ connect(:test)
44
+ end
45
+
46
+ def create_tables
47
+ ActiveRecord::Base.connection.create_table :users, force: true
48
+ ActiveRecord::Base.connection.create_table :items, force: true do |t|
49
+ t.references :user
50
+ end
51
+ end
52
+
53
+ def connect(env)
54
+ ActiveRecord::Base.establish_connection(env)
55
+ end
56
+ end
57
+
58
+ Seeder.new.run
data/standby.gemspec ADDED
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'standby/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = 'standby'
8
+ gem.version = Standby::VERSION
9
+ gem.authors = ['Kenn Ejima']
10
+ gem.email = ['kenn.ejima@gmail.com']
11
+ gem.description = %q{Read from stand-by databases for ActiveRecord}
12
+ gem.summary = %q{Read from stand-by databases for ActiveRecord}
13
+ gem.homepage = 'https://github.com/kenn/standby'
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^exe/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ['lib']
19
+
20
+ gem.add_runtime_dependency 'activerecord', '>= 3.0.0'
21
+
22
+ gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'sqlite3'
24
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: standby
3
+ version: !ruby/object:Gem::Version
4
+ version: 4.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Kenn Ejima
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-07-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: 3.0.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: 3.0.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Read from stand-by databases for ActiveRecord
56
+ email:
57
+ - kenn.ejima@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".rspec"
64
+ - ".travis.yml"
65
+ - Gemfile
66
+ - LICENSE.txt
67
+ - README.md
68
+ - Rakefile
69
+ - bin/console
70
+ - gemfiles/rails3.2.gemfile
71
+ - gemfiles/rails4.2.gemfile
72
+ - gemfiles/rails4.gemfile
73
+ - lib/standby.rb
74
+ - lib/standby/active_record/base.rb
75
+ - lib/standby/active_record/connection_handling.rb
76
+ - lib/standby/active_record/log_subscriber.rb
77
+ - lib/standby/active_record/relation.rb
78
+ - lib/standby/base.rb
79
+ - lib/standby/connection_holder.rb
80
+ - lib/standby/error.rb
81
+ - lib/standby/transaction.rb
82
+ - lib/standby/version.rb
83
+ - slavery.gemspec
84
+ - spec/active_record/log_subscriber_spec.rb
85
+ - spec/configuration_spec.rb
86
+ - spec/slavery_spec.rb
87
+ - spec/spec_helper.rb
88
+ - standby.gemspec
89
+ homepage: https://github.com/kenn/standby
90
+ licenses: []
91
+ metadata: {}
92
+ post_install_message:
93
+ rdoc_options: []
94
+ require_paths:
95
+ - lib
96
+ required_ruby_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ required_rubygems_version: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ requirements: []
107
+ rubyforge_project:
108
+ rubygems_version: 2.7.6
109
+ signing_key:
110
+ specification_version: 4
111
+ summary: Read from stand-by databases for ActiveRecord
112
+ test_files:
113
+ - spec/active_record/log_subscriber_spec.rb
114
+ - spec/configuration_spec.rb
115
+ - spec/slavery_spec.rb
116
+ - spec/spec_helper.rb