standby 4.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.
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