slavery 1.4.3 → 2.0.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: 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