replica 1.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.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+ *.log
21
+
22
+ ## PROJECT::SPECIFIC
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Zendesk
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,33 @@
1
+ = replica
2
+
3
+ == Example
4
+
5
+ Ticket.with_slave do
6
+ Ticket.first(50)
7
+ end
8
+
9
+ == Install
10
+
11
+ gem install replica
12
+
13
+ Add the slave database to config/database.yml:
14
+ development_slave:
15
+ adapter: mysql
16
+ ...
17
+
18
+ == Note on Patches/Pull Requests
19
+
20
+ * Fork the project.
21
+ * Make your feature addition or bug fix.
22
+ * Add tests for it. This is important so I don't break it in a
23
+ future version unintentionally.
24
+ * Commit, do not mess with rakefile, version, or history.
25
+ (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
26
+ * Send me a pull request. Bonus points for topic branches.
27
+
28
+ == Copyright
29
+
30
+ Copyright (c) 2009 Zendesk. See LICENSE for details.
31
+
32
+ == Authors
33
+ Mick Staugaard
data/Rakefile ADDED
@@ -0,0 +1,53 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "replica"
8
+ gem.summary = %Q{Simple database switching for ActiveRecord.}
9
+ gem.description = %Q{Easily run queries on replicated/slave databases.}
10
+ gem.email = "eac@zendesk.com"
11
+ gem.homepage = "http://github.com/eac/replica"
12
+ gem.authors = ["Eric Chapweske", "Mick Staugaard"]
13
+ gem.add_development_dependency "thoughtbot-shoulda", ">= 0"
14
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
15
+ end
16
+ Jeweler::GemcutterTasks.new
17
+ rescue LoadError
18
+ puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
19
+ end
20
+
21
+ require 'rake/testtask'
22
+ Rake::TestTask.new(:test) do |test|
23
+ test.libs << 'lib' << 'test'
24
+ test.pattern = 'test/**/test_*.rb'
25
+ test.verbose = true
26
+ end
27
+
28
+ begin
29
+ require 'rcov/rcovtask'
30
+ Rcov::RcovTask.new do |test|
31
+ test.libs << 'test'
32
+ test.pattern = 'test/**/test_*.rb'
33
+ test.verbose = true
34
+ end
35
+ rescue LoadError
36
+ task :rcov do
37
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
38
+ end
39
+ end
40
+
41
+ task :test => :check_dependencies
42
+
43
+ task :default => :test
44
+
45
+ require 'rake/rdoctask'
46
+ Rake::RDocTask.new do |rdoc|
47
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
48
+
49
+ rdoc.rdoc_dir = 'rdoc'
50
+ rdoc.title = "replica #{version}"
51
+ rdoc.rdoc_files.include('README*')
52
+ rdoc.rdoc_files.include('lib/**/*.rb')
53
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
data/lib/replica.rb ADDED
@@ -0,0 +1,116 @@
1
+ require 'activerecord'
2
+
3
+ module ActiveRecord # :nodoc:
4
+ class Base # :nodoc:
5
+ module Replica
6
+
7
+ # Executes queries using the slave database. Fails over to master if no slave is found.
8
+ # if you want to execute a block of code on the slave you can go:
9
+ # Account.with_slave do
10
+ # Account.first
11
+ # end
12
+ # the first account will be found on the slave DB
13
+ #
14
+ # this is the same as:
15
+ # Account.with_replica(:slave) do
16
+ # Account.first
17
+ # end
18
+ def with_slave(&block)
19
+ with_replica(:slave, &block)
20
+ end
21
+
22
+ # See with_slave
23
+ def with_master(&block)
24
+ with_replica(nil, &block)
25
+ end
26
+
27
+ # Name of the connection pool. Used by ConnectionHandler to retrieve the current connection pool.
28
+ def connection_pool_name # :nodoc:
29
+ replica = current_replica_name
30
+ if replica
31
+ "#{name}_#{replica}"
32
+ elsif self == ActiveRecord::Base
33
+ name
34
+ else
35
+ superclass.connection_pool_name
36
+ end
37
+ end
38
+
39
+ # Specify which database to use.
40
+ #
41
+ # Example:
42
+ # database.yml
43
+ # test_slave:
44
+ # adapter: mysql
45
+ # ...
46
+ #
47
+ # Account.with_replica(:slave) { Account.count }
48
+ #
49
+ def with_replica(replica_name, &block)
50
+ old_replica_name = current_replica_name
51
+ begin
52
+ self.current_replica_name = replica_name
53
+ rescue ActiveRecord::AdapterNotSpecified => e
54
+ self.current_replica_name = old_replica_name
55
+ logger.warn("Failed to establish replica connection: #{e.message} - defaulting to master")
56
+ end
57
+ yield
58
+ ensure
59
+ self.current_replica_name = old_replica_name
60
+ end
61
+
62
+ private
63
+
64
+ def current_replica_name
65
+ Thread.current[replica_key]
66
+ end
67
+
68
+ def current_replica_name=(new_replica_name)
69
+ Thread.current[replica_key] = new_replica_name
70
+
71
+ establish_replica_connection(new_replica_name) unless connected_to_replica?
72
+ end
73
+
74
+ def establish_replica_connection(replica_name)
75
+ name = replica_name ? "#{RAILS_ENV}_#{replica_name}" : RAILS_ENV
76
+ spec = configurations[name]
77
+ raise AdapterNotSpecified.new("No database defined by #{name} in database.yml") if spec.nil?
78
+
79
+ connection_handler.establish_connection(connection_pool_name, ConnectionSpecification.new(spec, "#{spec['adapter']}_connection"))
80
+ end
81
+
82
+ def connected_to_replica?
83
+ connection_handler.connection_pools.has_key?(connection_pool_name)
84
+ end
85
+
86
+ def replica_key
87
+ @replica_key ||= "#{name}_replica"
88
+ end
89
+
90
+ end
91
+ end
92
+ Base.extend(Base::Replica)
93
+
94
+ # The only difference here is that we use klass.connection_pool_name
95
+ # instead of klass.name as the pool key
96
+ module ConnectionAdapters # :nodoc:
97
+ class ConnectionHandler # :nodoc:
98
+
99
+ def retrieve_connection_pool(klass)
100
+ pool = @connection_pools[klass.connection_pool_name]
101
+ return pool if pool
102
+ return nil if ActiveRecord::Base == klass
103
+ retrieve_connection_pool klass.superclass
104
+ end
105
+
106
+ def remove_connection(klass)
107
+ pool = @connection_pools[klass.connection_pool_name]
108
+ @connection_pools.delete_if { |key, value| value == pool }
109
+ pool.disconnect! if pool
110
+ pool.spec.config if pool
111
+ end
112
+
113
+ end
114
+ end
115
+ end
116
+
data/test/database.yml ADDED
@@ -0,0 +1,19 @@
1
+ test:
2
+ adapter: mysql
3
+ encoding: utf8
4
+ database: replica_test
5
+ username: root
6
+ password:
7
+ socket: /tmp/mysql.sock
8
+ host: localhost
9
+
10
+ test_slave:
11
+ adapter: mysql
12
+ encoding: utf8
13
+ database: replica_test_slave
14
+ username: root
15
+ password:
16
+ socket: /tmp/mysql.sock
17
+ host: localhost
18
+
19
+
data/test/helper.rb ADDED
@@ -0,0 +1,32 @@
1
+ require 'rubygems'
2
+ require 'test/unit'
3
+ require 'shoulda'
4
+
5
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
7
+ require 'replica'
8
+ require 'models'
9
+
10
+ RAILS_ENV = "test"
11
+
12
+ ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/test.log")
13
+ ActiveRecord::Base.configurations = YAML::load(IO.read(File.dirname(__FILE__) + '/database.yml'))
14
+
15
+ ActiveRecord::Base.establish_connection('test_slave')
16
+ load(File.dirname(__FILE__) + "/schema.rb")
17
+
18
+ ActiveRecord::Base.establish_connection('test')
19
+ load(File.dirname(__FILE__) + "/schema.rb")
20
+
21
+
22
+ class Test::Unit::TestCase
23
+
24
+ def assert_using_master_db(klass)
25
+ assert_equal('replica_test', klass.connection.instance_variable_get(:@config)[:database])
26
+ end
27
+
28
+ def assert_using_slave_db(klass)
29
+ assert_equal('replica_test_slave', klass.connection.instance_variable_get(:@config)[:database])
30
+ end
31
+
32
+ end
data/test/models.rb ADDED
@@ -0,0 +1,7 @@
1
+ class Account < ActiveRecord::Base
2
+ # attributes: id, name, updated_at, created_at
3
+ end
4
+
5
+ class Ticket < ActiveRecord::Base
6
+ # attributes: id, title, account_id, updated_at, created_at
7
+ end
@@ -0,0 +1,91 @@
1
+ require 'helper'
2
+
3
+ class ReplicaTest < ActiveRecord::TestCase
4
+
5
+ context "without replica configuration" do
6
+
7
+ setup do
8
+ ActiveRecord::Base.configurations.delete('test_slave')
9
+ ActiveRecord::Base.connection_handler.connection_pools.clear
10
+ ActiveRecord::Base.establish_connection('test')
11
+ end
12
+
13
+ should "default to the master database" do
14
+ Account.create!
15
+
16
+ ActiveRecord::Base.with_slave { assert_using_master_db(Account) }
17
+ Account.with_slave { assert_using_master_db(Account) }
18
+ Ticket.with_slave { assert_using_master_db(Account) }
19
+ end
20
+
21
+ should "successfully execute queries" do
22
+ Account.create!
23
+ assert_using_master_db(Account)
24
+
25
+ assert_equal Account.count, ActiveRecord::Base.with_slave { Account.count }
26
+ assert_equal Account.count, Account.with_slave { Account.count }
27
+ end
28
+
29
+ end
30
+
31
+ context "with replica configuration" do
32
+
33
+ should "successfully execute queries" do
34
+ assert_using_master_db(Account)
35
+ Account.create!
36
+
37
+ assert_not_equal Account.count, ActiveRecord::Base.with_slave { Account.count }
38
+ assert_not_equal Account.count, Account.with_slave { Account.count }
39
+ assert_equal Account.count, Ticket.with_slave { Account.count }
40
+ end
41
+
42
+ should "support model specific with_slave blocks" do
43
+ assert_using_master_db(Account)
44
+ assert_using_master_db(Ticket)
45
+
46
+ Account.with_slave do
47
+ assert_using_slave_db(Account)
48
+ assert_using_master_db(Ticket)
49
+ end
50
+
51
+ assert_using_master_db(Account)
52
+ assert_using_master_db(Ticket)
53
+ end
54
+
55
+ should "support global with_slave blocks" do
56
+ assert_using_master_db(Account)
57
+ assert_using_master_db(Ticket)
58
+
59
+ ActiveRecord::Base.with_slave do
60
+ assert_using_slave_db(Account)
61
+ assert_using_slave_db(Ticket)
62
+ end
63
+
64
+ assert_using_master_db(Account)
65
+ assert_using_master_db(Ticket)
66
+ end
67
+
68
+ should_eventually "support nested with_* blocks" do
69
+
70
+ assert_using_master_db(Account)
71
+ assert_using_master_db(Ticket)
72
+
73
+ ActiveRecord::Base.with_slave do
74
+ assert_using_slave_db(Account)
75
+ assert_using_slave_db(Ticket)
76
+
77
+ Account.with_master do
78
+ assert_using_master_db(Account)
79
+ assert_using_slave_db(Ticket)
80
+ end
81
+
82
+ assert_using_slave_db(Account)
83
+ assert_using_slave_db(Ticket)
84
+ end
85
+
86
+ assert_using_master_db(Account)
87
+ assert_using_master_db(Ticket)
88
+ end
89
+
90
+ end
91
+ end
data/test/schema.rb ADDED
@@ -0,0 +1,16 @@
1
+ ActiveRecord::Schema.define(:version => 1) do
2
+
3
+ create_table "accounts", :force => true do |t|
4
+ t.string "name"
5
+ t.datetime "created_at"
6
+ t.datetime "updated_at"
7
+ end
8
+
9
+ create_table "tickets", :force => true do |t|
10
+ t.string "title"
11
+ t.integer "account_id"
12
+ t.datetime "created_at"
13
+ t.datetime "updated_at"
14
+ end
15
+
16
+ end
metadata ADDED
@@ -0,0 +1,80 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: replica
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Eric Chapweske
8
+ - Mick Staugaard
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-12-04 00:00:00 -08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: thoughtbot-shoulda
18
+ type: :development
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ version:
26
+ description: Easily run queries on replicated/slave databases.
27
+ email: eac@zendesk.com
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - LICENSE
34
+ - README.rdoc
35
+ files:
36
+ - .document
37
+ - .gitignore
38
+ - LICENSE
39
+ - README.rdoc
40
+ - Rakefile
41
+ - VERSION
42
+ - lib/replica.rb
43
+ - test/database.yml
44
+ - test/helper.rb
45
+ - test/models.rb
46
+ - test/replica_test.rb
47
+ - test/schema.rb
48
+ has_rdoc: true
49
+ homepage: http://github.com/eac/replica
50
+ licenses: []
51
+
52
+ post_install_message:
53
+ rdoc_options:
54
+ - --charset=UTF-8
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: "0"
62
+ version:
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: "0"
68
+ version:
69
+ requirements: []
70
+
71
+ rubyforge_project:
72
+ rubygems_version: 1.3.5
73
+ signing_key:
74
+ specification_version: 3
75
+ summary: Simple database switching for ActiveRecord.
76
+ test_files:
77
+ - test/helper.rb
78
+ - test/models.rb
79
+ - test/replica_test.rb
80
+ - test/schema.rb