postamt 0.9.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
+ SHA1:
3
+ metadata.gz: 34ca7a878f7cd7eb35c89e13148ff5bcc1ee9902
4
+ data.tar.gz: 019233a57d48dcddfefd6ea65025e4a98d59ee78
5
+ SHA512:
6
+ metadata.gz: 40034cd7b4e0016401f4a98893b2e70a48556a340b9fe541e7693369eb039ea02f1e1c104ec9a81bb2a0b6becbb0e53f06fa1af3145ce4b993eb9bd4c348258b
7
+ data.tar.gz: 2f7f090a657609cd192b44add07d8dd28e88773d299f6e8f792500f62f997d9917dfee86fa0192e2128f54381d6b126a108902a141919786169896c98cc87b55
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in pg_charmer.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Martin Schürrer
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,90 @@
1
+ # Postamt
2
+
3
+ Performs (some of) your read-only queries on a hot standby.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'pg_charmer'
10
+
11
+ ## Example usage
12
+
13
+ ```yaml
14
+ # database.yml
15
+ development:
16
+ adapter: postgresql
17
+ database: app
18
+ username: app
19
+ password:
20
+ host: master.db.internal
21
+ encoding: utf8
22
+ slave:
23
+ host: slave.db.internal
24
+ username: app_readonly
25
+ ```
26
+
27
+ ```ruby
28
+ class UserController < ApplicationController
29
+ use_db_connection :slave, for: ['User'], only: [:search]
30
+
31
+ def search
32
+ # SELECTs here are sent to slave
33
+ # User#save and User.create would be sent to master anyways.
34
+ # Everything in a transaction block too.
35
+ @users = User.where(...) # sent to slave
36
+ @something_else = SomethingElse.first # sent to master
37
+ end
38
+
39
+ def create
40
+ @user = User.new(params[:user])
41
+ @user.save! # sent to master
42
+ end
43
+
44
+ def invoice
45
+ transaction do
46
+ @user = User.where(...) # sent to master
47
+ @invoices = Invoice.create(...) # sent to master
48
+ end
49
+ end
50
+ end
51
+ ```
52
+
53
+ ```ruby
54
+ class ArchivedItem < ActiveRecord::Base
55
+ # default_connection can be overwritten with
56
+ # * Postamt.on(...) { ... },
57
+ # * ActiveRecord::Base.transaction { ... }, and
58
+ # * use_db_connection :other_connection, for: ['ArchivedItem'] in a controller.
59
+ self.default_connection = :slave
60
+ end
61
+
62
+ User.where(...) # sent to master
63
+ item = ArchivedItem.where(...) # sent to slave
64
+ item.title = "changed title"
65
+ item.save! # sent to master
66
+ item.reload # sent to slave, beware of replication lag here!
67
+
68
+ ActiveRecord::Base.transaction do
69
+ ArchivedItem.where(...) # sent to master, since we're in a transaction
70
+ User.where(...) # sent to master
71
+ end
72
+
73
+ Postamt.on(:master) do
74
+ ArchivedItem.where(...) # sent to master
75
+ User.where(...) # sent to master
76
+ end
77
+ ```
78
+
79
+ ```ruby
80
+ # If you don't want to test with a slave DB put this in config/environments/test.rb
81
+ Postamt.force_connection = :master
82
+ ```
83
+
84
+ ## Contributing
85
+
86
+ 1. Fork it
87
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
88
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
89
+ 4. Push to the branch (`git push origin my-new-feature`)
90
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/lib/postamt.rb ADDED
@@ -0,0 +1,118 @@
1
+ require 'action_controller'
2
+ require 'active_record'
3
+ require 'postamt/connection_handler'
4
+ require 'postamt/railtie'
5
+
6
+ module Postamt
7
+ mattr_accessor :default_connection
8
+ mattr_accessor :transaction_connection
9
+ mattr_accessor :force_connection
10
+
11
+ # Setup defaults
12
+ self.default_connection = :master
13
+ self.transaction_connection = :master
14
+ self.force_connection = nil
15
+
16
+ def self.on(connection)
17
+ self.connection_stack << connection
18
+ begin
19
+ yield
20
+ ensure
21
+ self.connection_stack.pop
22
+ end
23
+ end
24
+
25
+ def self.configurations
26
+ @configurations ||= begin
27
+ input = ActiveRecord::Base.configurations[Rails.env]
28
+ configs = input.select { |k, v| v.is_a? Hash }
29
+ master_config = input.reject { |k, v| v.is_a? Hash }
30
+ configs.each { |k, v| v.reverse_merge!(master_config) }
31
+ configs['master'] = master_config
32
+ configs
33
+ end
34
+ end
35
+
36
+ def self.connection_stack
37
+ Thread.current[:postamt_connection_stack] ||= []
38
+ end
39
+
40
+ # Used by use_db_connection. Cleared in an after_filter.
41
+ def self.overwritten_default_connections
42
+ Thread.current[:postamt_overwritten_default_connections] ||= {}
43
+ end
44
+
45
+ if Rails::VERSION::MAJOR == 4 and Rails::VERSION::MINOR == 0
46
+ Postamt::ConnectionSpecificationResolver = ActiveRecord::ConnectionAdapters::ConnectionSpecification::Resolver
47
+ elsif Rails::VERSION::MAJOR == 3 and Rails::VERSION::MINOR == 2
48
+ Postamt::ConnectionSpecificationResolver = ActiveRecord::Base::ConnectionSpecification::Resolver
49
+ else
50
+ abort "Postamt doesn't support Rails version #{Rails.version}"
51
+ end
52
+
53
+ # Called by Postamt::Railtie
54
+ def self.hook!
55
+ if Rails::VERSION::MAJOR == 4 and Rails::VERSION::MINOR == 0
56
+ ActiveRecord::Base.default_connection_handler = Postamt::ConnectionHandler.new
57
+ elsif Rails::VERSION::MAJOR == 3 and Rails::VERSION::MINOR == 2
58
+ ActiveRecord::Base.connection_handler = Postamt::ConnectionHandler.new
59
+ end
60
+
61
+ ActiveRecord::Base.instance_eval do
62
+ class_attribute :default_connection
63
+
64
+ # disable Model.establish_connection
65
+ def establish_connection(*args)
66
+ # This would be the only place Model.connection_handler.establish_connection is called.
67
+ nil
68
+ end
69
+
70
+ # a transaction runs on Postamt.transaction_connection or on the :on option
71
+ def transaction(options = {}, &block)
72
+ if connection = (options.delete(:on) || Postamt.transaction_connection)
73
+ Postamt.on(connection) { super }
74
+ else
75
+ super
76
+ end
77
+ end
78
+ end
79
+
80
+ ActiveRecord::Relation.class_eval do
81
+ # Also make sure that actions that don't instantiate the model and
82
+ # therefore don't call #save or #destroy run on master.
83
+ # update_column calls update_all, delete calls delete_all, so we don't
84
+ # have to monkey patch them.
85
+
86
+ def delete_all_with_postamt(conditions = nil)
87
+ Postamt.on(:master) { delete_all_without_postamt(conditions) }
88
+ end
89
+
90
+ def update_all_with_postamt(updates, conditions = nil, options = {})
91
+ Postamt.on(:master) { update_all_without_postamt(updates, conditions, options) }
92
+ end
93
+
94
+ # TODO: Switch to Module#prepend once we are Ruby-2.0.0-only
95
+ alias_method_chain :delete_all, :postamt
96
+ alias_method_chain :update_all, :postamt
97
+ end
98
+
99
+ ActionController::Base.instance_eval do
100
+ def use_db_connection(connection, args)
101
+ default_connections = {}
102
+ klass_names = args.delete(:for)
103
+ klass_names.each do |klass_name|
104
+ default_connections[klass_name] = connection
105
+ end
106
+
107
+ before_filter(args) do |controller|
108
+ Postamt.overwritten_default_connections.merge!(default_connections)
109
+ end
110
+ end
111
+
112
+ after_filter do
113
+ Postamt.overwritten_default_connections.clear
114
+ end
115
+ end
116
+ end
117
+ end
118
+
@@ -0,0 +1,124 @@
1
+ require 'atomic'
2
+ require 'thread_safe'
3
+
4
+ module Postamt
5
+ class ConnectionHandler
6
+ def initialize
7
+ @process_pid = Atomic.new(nil)
8
+ end
9
+
10
+ def connection_pools
11
+ # See https://github.com/rails/rails/commit/c3ca7ac09e960fa1287adc730e8ddc713e844c37
12
+ Hash[self.connection_pool_list.map { |pool| [pool.spec, pool] }]
13
+ end
14
+
15
+ def connection_pool_list
16
+ self.ensure_ready
17
+ @pools.values
18
+ end
19
+
20
+ # Returns true if there are any active connections among the connection
21
+ # pools that the ConnectionHandler is managing.
22
+ def active_connections?
23
+ self.ensure_ready
24
+ self.connection_pool_list.any?(&:active_connection?)
25
+ end
26
+
27
+ # Returns any connections in use by the current thread back to the pool,
28
+ # and also returns connections to the pool cached by threads that are no
29
+ # longer alive.
30
+ def clear_active_connections!
31
+ self.ensure_ready
32
+ self.connection_pool_list.each(&:release_connection)
33
+ end
34
+
35
+ # Clears the cache which maps classes.
36
+ def clear_reloadable_connections!
37
+ self.ensure_ready
38
+ self.connection_pool_list.each(&:clear_reloadable_connections!)
39
+ end
40
+
41
+ def clear_all_connections!
42
+ self.ensure_ready
43
+ self.connection_pool_list.each(&:disconnect!)
44
+ end
45
+
46
+ # Locate the connection of the nearest super class. This can be an
47
+ # active or defined connection: if it is the latter, it will be
48
+ # opened and set as the active connection for the class it was defined
49
+ # for (not necessarily the current class).
50
+ def retrieve_connection(klass) #:nodoc:
51
+ self.ensure_ready
52
+ pool = self.retrieve_connection_pool(klass)
53
+ (pool && pool.connection) or raise ActiveRecord::ConnectionNotEstablished
54
+ end
55
+
56
+ # Returns true if a connection that's accessible to this class has
57
+ # already been opened.
58
+ def connected?(klass)
59
+ return false if Process.pid != @process_pid.get
60
+ conn = self.retrieve_connection_pool(klass)
61
+ conn && conn.connected?
62
+ end
63
+
64
+ # Only called in ActiveRecord test code, so performance isn't an issue.
65
+ def remove_connection(owner)
66
+ self.clear_cache
67
+ # Don't return a ConnectionSpecification hash since we've disabled establish_connection anyway
68
+ return nil
69
+ end
70
+
71
+ # Called by ActiveRecord::ConnectionHandling#connection_pool.
72
+ def retrieve_connection_pool(klass)
73
+ self.ensure_ready
74
+ self.pool_for(klass)
75
+ end
76
+
77
+ protected
78
+
79
+ def prepare
80
+ @process_pid.set(Process.pid)
81
+ @pools = ThreadSafe::Cache.new(initial_capacity: 2)
82
+ end
83
+
84
+ def ensure_ready
85
+ if Process.pid != @process_pid.get
86
+ # We've been forked -> throw away connection pools
87
+ prepare
88
+ end
89
+ end
90
+
91
+ # Throw away all pools on next request
92
+ def clear_cache
93
+ @process_pid.set(nil)
94
+ end
95
+
96
+ def connection_for(klass)
97
+ Postamt.force_connection || Postamt.connection_stack.last || Postamt.overwritten_default_connections[klass.name] || klass.default_connection || Postamt.default_connection
98
+ end
99
+
100
+ def pool_for(klass)
101
+ # Sauspiel's reportable dependency makes Rails 3.2 request a connection early,
102
+ # before the App is initialized. Return nil in that case, Rails deals with that
103
+ return nil if ActiveRecord::Base.configurations[Rails.env].nil?
104
+
105
+ connection = connection_for(klass)
106
+ # Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
107
+ # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that
108
+ # #fetch is significantly slower than #[]. So in the nil case, no caching will
109
+ # take place, but that's ok since the nil case is not the common one that we wish
110
+ # to optimise for.
111
+ @pools[connection] ||= begin
112
+ Postamt.configurations[connection.to_s] ||= Postamt.configurations['master']
113
+ resolver = Postamt::ConnectionSpecificationResolver.new connection, Postamt.configurations
114
+ spec = resolver.spec
115
+
116
+ unless ActiveRecord::Base.respond_to?(spec.adapter_method)
117
+ raise ActiveRecord::AdapterNotFound, "database configuration specifies nonexistent #{spec.config[:adapter]} adapter"
118
+ end
119
+
120
+ ActiveRecord::ConnectionAdapters::ConnectionPool.new(spec)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,21 @@
1
+ module Postamt
2
+ class Railtie < Rails::Railtie
3
+ railtie_name "postamt"
4
+
5
+ initializer "postamt.hook", before: "active_record.initialize_database" do |app|
6
+ if (defined?($rails_rake_task) && $rails_rake_task)
7
+ # We mustn't hook into AR when db:migrate or db:test:load_schema
8
+ # run, but user-defined Rake tasks still need us
9
+ task_names = []
10
+ tasks_to_examine = Rake.application.top_level_tasks.map{ |task_name| Rake.application[task_name] }
11
+ until tasks_to_examine.empty?
12
+ task = tasks_to_examine.pop
13
+ task_names << task.name
14
+ tasks_to_examine += task.prerequisite_tasks
15
+ end
16
+ next if task_names.any? { |task_name| task_name.start_with? "db:" }
17
+ end
18
+ Postamt.hook!
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module Postamt
2
+ VERSION = "0.9.0"
3
+ end
data/postamt.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'postamt/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "postamt"
8
+ spec.version = Postamt::VERSION
9
+ spec.authors = ["Martin Schürrer"]
10
+ spec.email = ["martin@schuerrer.org"]
11
+ spec.description = %q{Choose per model and/or controller&action whether a read-only query should be sent to master or a hot standby. Or just use Postamt.on(:slave) { ... }. }
12
+ spec.summary = %q{Performs (some of) your read-only queries on a hot standby}
13
+ spec.homepage = "https://github.com/sauspiel/postamt"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/).reject { |f| f.start_with? 'testapp' }
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "thread_safe", "~> 0.1"
22
+ spec.add_dependency "railties", [">= 3.2.0", "< 4.1.0"]
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: postamt
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Schürrer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-04-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thread_safe
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ~>
25
+ - !ruby/object:Gem::Version
26
+ version: '0.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: railties
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: 3.2.0
34
+ - - <
35
+ - !ruby/object:Gem::Version
36
+ version: 4.1.0
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - '>='
42
+ - !ruby/object:Gem::Version
43
+ version: 3.2.0
44
+ - - <
45
+ - !ruby/object:Gem::Version
46
+ version: 4.1.0
47
+ - !ruby/object:Gem::Dependency
48
+ name: bundler
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: '1.3'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ version: '1.3'
61
+ - !ruby/object:Gem::Dependency
62
+ name: rake
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - '>='
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ type: :development
69
+ prerelease: false
70
+ version_requirements: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ description: 'Choose per model and/or controller&action whether a read-only query
76
+ should be sent to master or a hot standby. Or just use Postamt.on(:slave) { ...
77
+ }. '
78
+ email:
79
+ - martin@schuerrer.org
80
+ executables: []
81
+ extensions: []
82
+ extra_rdoc_files: []
83
+ files:
84
+ - .gitignore
85
+ - Gemfile
86
+ - LICENSE.txt
87
+ - README.md
88
+ - Rakefile
89
+ - lib/postamt.rb
90
+ - lib/postamt/connection_handler.rb
91
+ - lib/postamt/railtie.rb
92
+ - lib/postamt/version.rb
93
+ - postamt.gemspec
94
+ homepage: https://github.com/sauspiel/postamt
95
+ licenses:
96
+ - MIT
97
+ metadata: {}
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - '>='
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubyforge_project:
114
+ rubygems_version: 2.0.3
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Performs (some of) your read-only queries on a hot standby
118
+ test_files: []
119
+ has_rdoc: