slavery 1.4.3 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b5623ea412368cf47d2502b70f7996d6206e8b25
4
- data.tar.gz: 9c5b645d0c4d8ab40fb9212088b22ac1a0d7622c
3
+ metadata.gz: 844d663a8d247cdb698ca1e17d781749372d980c
4
+ data.tar.gz: 7fb134582dfa4eb45971cd1afb35f08286aef10f
5
5
  SHA512:
6
- metadata.gz: d9951e3d9c4eed95ab9671f05806b9bf1b71e8b121f9ced9a628331ee8477f60dfd752a2e93bd85f8b5e265ae15f012bbaa2baaf1a417f8e7c4a1bd1f25b5631
7
- data.tar.gz: 2083dd1a78aa49edf3bd3939d92f40c95aec2f00daaa5b002964a856537303d445a949f295d9a5d653fd4781f35f169e47832a008a8e0906bfbe03fbb39079c3
6
+ metadata.gz: a227e3ba41594385d60c96692969dbbd59d16c5e952ccfa54daf423526025b25dcc99464f9f7eee4458e8f941db1873d3f57d66d9548f9d80625e7b6c600f241
7
+ data.tar.gz: 1321abb5bd5394ac032bd5531117a707873251c4fd9026456ad08e3c27fb9383e45f1cdfdc4a6aad4f9080c8bf4de3270432854cfef96d5bb2395c70eb724595
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # Slavery - Simple, conservative slave reads for ActiveRecord
2
2
 
3
- Slavery is a simple, easy to use plugin for ActiveRecord that enables conservative slave reads, which means it doesn't automatically redirect all SELECTs to slaves. Instead, it lets you specify `Slavery.on_slave` to send a particular query to a slave.
3
+ Slavery is a simple, easy to use gem for ActiveRecord that enables conservative slave reads, which means it doesn't automatically redirect all SELECTs to slaves.
4
4
 
5
- Probably you just start off with one single database. As your app grows, you would move to master-slave replication for redundancy. At this point, all queries still go to the master and slaves are just backups. With that configuration, it's tempting to run some long-running queries on the slave. And that's exactly what Slavery does.
5
+ Instead, you can do `Slavery.on_slave { User.count }` to send a particular query to a slave.
6
+
7
+ Background: Probably your app started off with one single database. As it grows, you would upgrade to a master-slave replication for redundancy. At this point, all queries still go to the master and slaves are just backups. With that configuration, it's tempting to run some long-running queries on the slave. And that's exactly what Slavery does.
6
8
 
7
9
  * Conservative - Safe by default. Installing Slavery won't change your app's current behavior.
8
- * Future proof - No dirty hacks, simply works as a proxy for `ActiveRecord::Base.connection`.
9
- * Simple - Only 100+ LOC, you can read the entire source and completely stay in control.
10
+ * Future proof - No dirty hacks. Simply works as a proxy for `ActiveRecord::Base.connection`.
11
+ * Simple code - Intentionally small. You can read the entire source and completely stay in control.
10
12
 
11
13
  Slavery works with ActiveRecord 3 or later.
12
14
 
@@ -93,7 +95,7 @@ With MySQL, `GRANT SELECT` creates a read-only user.
93
95
  GRANT SELECT ON *.* TO 'readonly'@'localhost';
94
96
  ```
95
97
 
96
- With this user, writes on slave should raises an exception.
98
+ With this user, writes on slave should raise an exception.
97
99
 
98
100
  ```ruby
99
101
  Slavery.on_slave { User.create } # => ActiveRecord::StatementInvalid: Mysql2::Error: INSERT command denied...
@@ -101,26 +103,24 @@ Slavery.on_slave { User.create } # => ActiveRecord::StatementInvalid: Mysql2::E
101
103
 
102
104
  It is a good idea to confirm this behavior in your test code as well.
103
105
 
104
- ## Database failure
105
-
106
- When one of the master or the slave goes down, you would rewrite `database.yml` to make all queries go to the surviving database, until you restore or rebuild the failed one.
106
+ ## Disable temporarily
107
107
 
108
- In such an event, you don't want to manually remove `Slavery.on_slave` from your code. Instead, just put the following line in `config/initializers/slavery.rb`.
108
+ You can quickly disable slave reads by dropping the following line in `config/initializers/slavery.rb`.
109
109
 
110
110
  ```ruby
111
111
  Slavely.disabled = true
112
112
  ```
113
113
 
114
- With this line, Slavery stops connection switching and all queries go to the new master.
114
+ With this line, Slavery stops connection switching and all queries go to the master.
115
+
116
+ This may be useful when one of the master or the slave goes down. You would rewrite `database.yml` to make all queries go to the surviving database, until you restore or rebuild the failed one.
115
117
 
116
118
  ## Support for non-Rails apps
117
119
 
118
- If you're using ActiveRecord in a non-Rails app (e.g. Sinatra), be sure to set `Slavery.env` in the boot sequence.
120
+ 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:
119
121
 
120
122
  ```ruby
121
- Slavery.env = 'development'
122
-
123
- ActiveRecord::Base.send(:include, Slavery)
123
+ require 'slavery'
124
124
 
125
125
  ActiveRecord::Base.configurations = {
126
126
  'development' => { adapter: 'mysql2', ... },
@@ -137,8 +137,6 @@ This is useful for deploying on EngineYard where the configuration key in databa
137
137
  Slavery.spec_key = "slave" #instead of production_slave
138
138
  ```
139
139
 
140
- Alternatively you can pass it a lambda for dynamically setting this.
140
+ ## Changelog
141
141
 
142
- ```ruby
143
- Slavery.spec_key = lambda{ "#{Slavery.env}_slave" }
144
- ```
142
+ * v2.0.0: Rails 5 support
@@ -1,105 +1,49 @@
1
- require 'slavery/version'
2
- require 'slavery/railtie'
3
1
  require 'active_record'
2
+ require 'slavery/base'
3
+ require 'slavery/error'
4
+ require 'slavery/slave_connection_holder'
5
+ require 'slavery/version'
6
+ require 'slavery/active_record/base'
7
+ require 'slavery/active_record/relation'
4
8
 
5
9
  module Slavery
6
- extend ActiveSupport::Concern
7
-
8
- included do
9
- require 'slavery/relation'
10
-
11
- class << self
12
- alias_method_chain :connection, :slavery
13
- end
14
- end
15
-
16
- class Error < StandardError; end
17
-
18
10
  class << self
19
11
  attr_accessor :disabled
20
- attr_writer :env, :spec_key
12
+ attr_writer :spec_key
21
13
 
22
14
  def spec_key
23
15
  case @spec_key
24
16
  when String then @spec_key
25
- when Proc then @spec_key = @spec_key.call
26
- when NilClass then @spec_key = "#{Slavery.env}_slave"
17
+ when NilClass then @spec_key = "#{ActiveRecord::ConnectionHandling::RAILS_ENV.call}_slave"
27
18
  end
28
19
  end
29
20
 
30
21
  def on_slave(&block)
31
- run true, &block
22
+ Base.new(:slave).run &block
32
23
  end
33
24
 
34
25
  def on_master(&block)
35
- run false, &block
36
- end
37
-
38
- def run(new_value)
39
- old_value = Thread.current[:on_slave] # Save for recursive nested calls
40
- Thread.current[:on_slave] = new_value
41
- yield
42
- ensure
43
- Thread.current[:on_slave] = old_value
44
- end
45
-
46
- def env
47
- @env ||= defined?(Rails) ? Rails.env.to_s : 'development'
48
- end
49
- end
50
-
51
- module ClassMethods
52
- def on_slave
53
- # Why where(nil)?
54
- # http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
55
- context = where(nil)
56
- context.slavery_target = :slave
57
- context
26
+ Base.new(:master).run &block
58
27
  end
59
28
 
60
- def connection_with_slavery
61
- if Thread.current[:on_slave] and slaveryable?
62
- slave_connection
63
- else
64
- master_connection
29
+ def slave_connection_holder
30
+ @slave_connection_holder ||= begin
31
+ SlaveConnectionHolder.activate
32
+ SlaveConnectionHolder
65
33
  end
66
34
  end
67
35
 
68
- def slaveryable?
36
+ def base_transaction_depth
69
37
  @base_transaction_depth ||= begin
70
- defined?(ActiveSupport::TestCase) &&
71
- ActiveSupport::TestCase.respond_to?(:use_transactional_fixtures) &&
72
- ActiveSupport::TestCase.try(:use_transactional_fixtures) ? 1 : 0
73
- end
74
- inside_transaction = master_connection.open_transactions > @base_transaction_depth
75
- raise Error.new('on_slave cannot be used inside transaction block!') if inside_transaction
76
-
77
- !Slavery.disabled
78
- end
79
-
80
- def master_connection
81
- connection_without_slavery
82
- end
83
-
84
- def slave_connection
85
- slave_connection_holder.connection_without_slavery
86
- end
87
-
88
- # Create an anonymous AR class to hold slave connection
89
- def slave_connection_holder
90
- @slave_connection_holder ||= Class.new(ActiveRecord::Base) {
91
- self.abstract_class = true
92
-
93
- def self.name
94
- "SlaveConnectionHolder"
38
+ testcase = ActiveSupport::TestCase
39
+ if defined?(testcase) &&
40
+ testcase.respond_to?(:use_transactional_fixtures) &&
41
+ testcase.try(:use_transactional_fixtures)
42
+ 1
43
+ else
44
+ 0
95
45
  end
96
-
97
- spec = [Slavery.spec_key, Slavery.env].find do |spec_key|
98
- ActiveRecord::Base.configurations[spec_key]
99
- end or raise Error.new("#{Slavery.spec_key} or #{Slavery.env} must exist!")
100
-
101
- establish_connection spec.to_sym
102
- }
46
+ end
103
47
  end
104
48
  end
105
49
  end
@@ -0,0 +1,27 @@
1
+ module ActiveRecord
2
+ class Base
3
+ class << self
4
+ alias_method :connection_without_slavery, :connection
5
+
6
+ def connection
7
+ case Thread.current[:slavery]
8
+ when :slave
9
+ Slavery.slave_connection_holder.connection_without_slavery
10
+ when :master, NilClass
11
+ connection_without_slavery
12
+ else
13
+ raise Slavery::Error.new("invalid target: #{Thread.current[:slavery]}")
14
+ end
15
+ end
16
+
17
+ # Generate scope at top level e.g. User.on_slave
18
+ def on_slave
19
+ # Why where(nil)?
20
+ # http://stackoverflow.com/questions/18198963/with-rails-4-model-scoped-is-deprecated-but-model-all-cant-replace-it
21
+ context = where(nil)
22
+ context.slavery_target = :slave
23
+ context
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ module ActiveRecord
2
+ class Relation
3
+ attr_accessor :slavery_target
4
+
5
+ # Supports queries like User.on_slave.to_a
6
+ alias_method :exec_queries_without_slavery, :exec_queries
7
+
8
+ def exec_queries
9
+ if slavery_target == :slave
10
+ Slavery.on_slave { exec_queries_without_slavery }
11
+ else
12
+ exec_queries_without_slavery
13
+ end
14
+ end
15
+
16
+
17
+ # Supports queries like User.on_slave.count
18
+ alias_method :calculate_without_slavery, :calculate
19
+
20
+ def calculate(*args)
21
+ if slavery_target == :slave
22
+ Slavery.on_slave { calculate_without_slavery(*args) }
23
+ else
24
+ calculate_without_slavery(*args)
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,36 @@
1
+ module Slavery
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
+ raise Slavery::Error.new('on_slave cannot be used inside transaction block!') if inside_transaction?
15
+
16
+ if Slavery.disabled
17
+ :master
18
+ else
19
+ target
20
+ end
21
+ end
22
+
23
+ def inside_transaction?
24
+ open_transactions = run_on(:master) { ActiveRecord::Base.connection.open_transactions }
25
+ open_transactions > Slavery.base_transaction_depth
26
+ end
27
+
28
+ def run_on(target)
29
+ backup = Thread.current[:slavery] # Save for recursive nested calls
30
+ Thread.current[:slavery] = target
31
+ yield
32
+ ensure
33
+ Thread.current[:slavery] = backup
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,4 @@
1
+ module Slavery
2
+ class Error < StandardError
3
+ end
4
+ end
@@ -0,0 +1,13 @@
1
+ module Slavery
2
+ class SlaveConnectionHolder < ActiveRecord::Base
3
+ self.abstract_class = true
4
+
5
+ class << self
6
+ # for delayed activation
7
+ def activate
8
+ raise Error.new('Slavery.spec_key invalid!') unless ActiveRecord::Base.configurations[Slavery.spec_key]
9
+ establish_connection Slavery.spec_key.to_sym
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,3 +1,3 @@
1
1
  module Slavery
2
- VERSION = '1.4.3'
2
+ VERSION = '2.0.0'
3
3
  end
@@ -1,13 +1,17 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe Slavery do
4
+ def slavery_value
5
+ Thread.current[:slavery]
6
+ end
7
+
4
8
  def on_slave?
5
- Thread.current[:on_slave]
9
+ slavery_value == :slave
6
10
  end
7
11
 
8
12
  it 'sets thread local' do
9
- Slavery.on_master { expect(on_slave?).to be false }
10
- Slavery.on_slave { expect(on_slave?).to be true }
13
+ Slavery.on_master { expect(slavery_value).to be :master }
14
+ Slavery.on_slave { expect(slavery_value).to be :slave }
11
15
  end
12
16
 
13
17
  it 'returns value from block' do
@@ -39,36 +43,46 @@ describe Slavery do
39
43
  end
40
44
  end
41
45
 
42
- it 'disables in transaction' do
46
+ it 'raises error in transaction' do
43
47
  User.transaction do
44
- expect { User.slaveryable? }.to raise_error(Slavery::Error)
48
+ expect { Slavery.on_slave { User.first } }.to raise_error(Slavery::Error)
45
49
  end
46
50
  end
47
51
 
48
52
  it 'disables by configuration' do
49
- allow(Slavery).to receive(:disabled).and_return(false)
50
- Slavery.on_slave { expect(User.slaveryable?).to be true }
53
+ backup = Slavery.disabled
54
+
55
+ Slavery.disabled = false
56
+ Slavery.on_slave { expect(slavery_value).to be :slave }
51
57
 
52
- allow(Slavery).to receive(:disabled).and_return(true)
53
- Slavery.on_slave { expect(User.slaveryable?).to be false }
58
+ Slavery.disabled = true
59
+ Slavery.on_slave { expect(slavery_value).to be :master }
60
+
61
+ Slavery.disabled = backup
54
62
  end
55
63
 
56
64
  it 'sets the Slavery database spec name by configuration' do
57
- Slavery.spec_key = "custom_slave"
65
+ Slavery.spec_key = 'custom_slave'
58
66
  expect(Slavery.spec_key).to eq 'custom_slave'
67
+ end
59
68
 
60
- Slavery.spec_key = lambda{
61
- "kewl_slave"
62
- }
63
- expect(Slavery.spec_key).to eq "kewl_slave"
69
+ it 'avoids stack overflow with 3rdparty gem that defines alias_method. namely newrelic...' do
70
+ class ActiveRecord::Relation
71
+ alias_method :calculate_without_thirdparty, :calculate
64
72
 
65
- Slavery.spec_key = lambda{
66
- "#{Slavery.env}_slave"
67
- }
68
- expect(Slavery.spec_key).to eq "test_slave"
73
+ def calculate(*args)
74
+ calculate_without_thirdparty(*args)
75
+ end
76
+ end
77
+
78
+ expect(User.count).to be 2
79
+
80
+ class ActiveRecord::Relation
81
+ alias_method :calculate, :calculate_without_thirdparty
82
+ end
69
83
  end
70
84
 
71
- it 'works with scopes' do
85
+ it 'works with any scopes' do
72
86
  expect(User.count).to be 2
73
87
  expect(User.on_slave.count).to be 1
74
88
 
@@ -81,28 +95,30 @@ describe Slavery do
81
95
  describe 'configuration' do
82
96
  before do
83
97
  # Backup connection and configs
84
- @old_conn = User.instance_variable_get :@slave_connection_holder
85
- @old_config = ActiveRecord::Base.configurations.dup
86
- User.instance_variable_set :@slave_connection_holder, nil
98
+ @backup_conn = Slavery.instance_variable_get :@slave_connection_holder
99
+ @backup_config = ActiveRecord::Base.configurations.dup
100
+ @backup_disabled = Slavery.disabled
101
+ Slavery.instance_variable_set :@slave_connection_holder, nil
87
102
  end
88
103
 
89
104
  after do
90
105
  # Restore connection and configs
91
- User.instance_variable_set :@slave_connection_holder, @old_conn
92
- ActiveRecord::Base.configurations = @old_config
106
+ Slavery.instance_variable_set :@slave_connection_holder, @backup_conn
107
+ ActiveRecord::Base.configurations = @backup_config
108
+ Slavery.disabled = @backup_disabled
93
109
  end
94
110
 
95
- it 'connects to master if slave configuration not specified' do
96
- ActiveRecord::Base.configurations[Slavery.spec_key] = nil
111
+ it 'raises error if slave configuration not specified' do
112
+ ActiveRecord::Base.configurations['test_slave'] = nil
97
113
 
98
- expect(Slavery.on_slave { User.count }).to be 2
114
+ expect { Slavery.on_slave { User.count } }.to raise_error(Slavery::Error)
99
115
  end
100
116
 
101
- it 'raises error when no configuration found' do
102
- ActiveRecord::Base.configurations['test'] = nil
103
- ActiveRecord::Base.configurations[Slavery.spec_key] = nil
117
+ it 'connects to master if slave configuration not specified' do
118
+ ActiveRecord::Base.configurations['test_slave'] = nil
119
+ Slavery.disabled = true
104
120
 
105
- expect { Slavery.on_slave { User.count } }.to raise_error(Slavery::Error)
121
+ expect(Slavery.on_slave { User.count }).to be 2
106
122
  end
107
123
  end
108
124
  end
@@ -1,36 +1,28 @@
1
1
  require 'rubygems'
2
2
  require 'bundler/setup'
3
3
 
4
- require 'slavery'
5
-
6
- # Activate Slavery
7
- ActiveRecord::Base.send(:include, Slavery)
4
+ ENV['RACK_ENV'] = 'test'
8
5
 
9
- # Prepare databases
10
- class User < ActiveRecord::Base
11
- end
12
-
13
- # Should be equal to Rails.env
14
- Slavery.env = 'test'
6
+ require 'slavery'
15
7
 
16
8
  ActiveRecord::Base.configurations = {
17
9
  'test' => { adapter: 'sqlite3', database: 'test_db' },
18
10
  'test_slave' => { adapter: 'sqlite3', database: 'test_slave_db' }
19
11
  }
20
12
 
13
+ # Prepare databases
14
+ class User < ActiveRecord::Base
15
+ end
16
+
21
17
  # Create two records on master
22
18
  ActiveRecord::Base.establish_connection(:test)
23
- ActiveRecord::Base.connection.create_table :users, force: true do |t|
24
- t.boolean :disabled
25
- end
19
+ ActiveRecord::Base.connection.create_table :users, force: true
26
20
  User.create
27
21
  User.create
28
22
 
29
23
  # Create one record on slave, emulating replication lag
30
24
  ActiveRecord::Base.establish_connection(:test_slave)
31
- ActiveRecord::Base.connection.create_table :users, force: true do |t|
32
- t.boolean :disabled
33
- end
25
+ ActiveRecord::Base.connection.create_table :users, force: true
34
26
  User.create
35
27
 
36
28
  # Reconnect to master
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: slavery
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.3
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kenn Ejima
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-03 00:00:00.000000000 Z
11
+ date: 2016-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -66,8 +66,11 @@ files:
66
66
  - README.md
67
67
  - Rakefile
68
68
  - lib/slavery.rb
69
- - lib/slavery/railtie.rb
70
- - lib/slavery/relation.rb
69
+ - lib/slavery/active_record/base.rb
70
+ - lib/slavery/active_record/relation.rb
71
+ - lib/slavery/base.rb
72
+ - lib/slavery/error.rb
73
+ - lib/slavery/slave_connection_holder.rb
71
74
  - lib/slavery/version.rb
72
75
  - slavery.gemspec
73
76
  - spec/slavery_spec.rb
@@ -1,11 +0,0 @@
1
- module Slavery
2
- if defined? Rails::Railtie
3
- class Railtie < Rails::Railtie
4
- initializer 'slavery.insert_into_active_record' do |app|
5
- ActiveSupport.on_load :active_record do
6
- include Slavery
7
- end
8
- end
9
- end
10
- end
11
- end
@@ -1,24 +0,0 @@
1
- class ActiveRecord::Relation
2
- attr_accessor :slavery_target
3
-
4
- # Supports queries like User.on_slave.to_a
5
- def exec_queries_with_slavery
6
- if slavery_target == :slave
7
- Slavery.on_slave { exec_queries_without_slavery }
8
- else
9
- exec_queries_without_slavery
10
- end
11
- end
12
-
13
- # Supports queries like User.on_slave.count
14
- def calculate_with_slavery(operation, column_name, options = {})
15
- if slavery_target == :slave
16
- Slavery.on_slave { calculate_without_slavery(operation, column_name, options) }
17
- else
18
- calculate_without_slavery(operation, column_name, options)
19
- end
20
- end
21
-
22
- alias_method_chain :exec_queries, :slavery
23
- alias_method_chain :calculate, :slavery
24
- end