revolutionhealth-masochism 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +18 -0
- data/README.markdown +154 -0
- data/lib/active_reload/connection_proxy.rb +141 -0
- data/lib/active_reload/master_filter.rb +12 -0
- data/lib/masochism.rb +12 -0
- metadata +58 -0
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2007 Rick Olson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
4
|
+
this software and associated documentation files (the "Software"), to deal in
|
5
|
+
the Software without restriction, including without limitation the rights to
|
6
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
7
|
+
the Software, and to permit persons to whom the Software is furnished to do so,
|
8
|
+
subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
11
|
+
copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
15
|
+
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
16
|
+
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,154 @@
|
|
1
|
+
masochism
|
2
|
+
=========
|
3
|
+
|
4
|
+
<div style="width:240px; padding:2px; border:1px solid silver; float:right; margin:0 0 1em 2em; background:white">
|
5
|
+
<img src="http://farm1.static.flickr.com/111/295426387_a39c5c8954_m.jpg" alt="Scream" />
|
6
|
+
<p style="text-align:center">photo by <a href="http://flickr.com/people/alphadesigner/" title="Flickr: ArtWerk">ArtWerk</a></p>
|
7
|
+
</div>
|
8
|
+
|
9
|
+
The masochism plugin provides an easy solution for Ruby on Rails applications to work in a
|
10
|
+
replicated database environment. It works by replacing the `connection` object accessed by
|
11
|
+
ActiveRecord models by ConnectionProxy that chooses between master and slave when
|
12
|
+
executing queries. Generally all writes go to master.
|
13
|
+
|
14
|
+
|
15
|
+
Quick setup
|
16
|
+
-----------
|
17
|
+
|
18
|
+
First, setup your database.yml:
|
19
|
+
|
20
|
+
# default configuration (slave)
|
21
|
+
production: &defaults
|
22
|
+
adapter: mysql
|
23
|
+
database: app_production
|
24
|
+
username: webapp
|
25
|
+
password: ********
|
26
|
+
host: localhost
|
27
|
+
|
28
|
+
# setup for masochism (master)
|
29
|
+
master_database:
|
30
|
+
<<: *defaults
|
31
|
+
host: master.example.com
|
32
|
+
|
33
|
+
To enable masochism, this is required:
|
34
|
+
|
35
|
+
# enable masochism
|
36
|
+
ActiveReload::ConnectionProxy.setup!
|
37
|
+
|
38
|
+
Example usage:
|
39
|
+
|
40
|
+
# in environment.rb
|
41
|
+
config.after_initialize do
|
42
|
+
if Rails.env.production?
|
43
|
+
ActiveReload::ConnectionProxy::setup!
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
Considerations
|
49
|
+
--------------
|
50
|
+
|
51
|
+
### Thinking Sphinx
|
52
|
+
|
53
|
+
Thinking Sphinx inspects the `connection` object to determine the database adapter.
|
54
|
+
Because masochism works by putting the connection proxy in its place, TS will be confused
|
55
|
+
about `ActiveReload::ConnectionProxy` and abort. A possible workaround is to monkeypatch TS right to **hardcode** our adapter after masochism has been enabled:
|
56
|
+
|
57
|
+
# ConnectionProxy from masochism confuses TS
|
58
|
+
ThinkingSphinx::Index.class_eval do
|
59
|
+
def adapter() :mysql end
|
60
|
+
end
|
61
|
+
|
62
|
+
ThinkingSphinx::AbstractAdapter.class_eval do
|
63
|
+
def self.detect(model)
|
64
|
+
ThinkingSphinx::MysqlAdapter
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
### Litespeed web server or Phusion Passenger (mod\_rails)
|
69
|
+
|
70
|
+
If you are using the Litespeed web server or Passenger (mod\_rails), child processes are initialized on creation,
|
71
|
+
which means any setup done in an environment file will be effectively ignored. [A brief
|
72
|
+
discussion of the problem is posted here](http://litespeedtech.com/support/wiki/doku.php?id=litespeed_wiki:rails:memcache).
|
73
|
+
|
74
|
+
One solution for Litespeed/Passenger users is to check the connection at your first request and do
|
75
|
+
the `setup!` call if your connection hasn't been initialized, like:
|
76
|
+
|
77
|
+
# in ApplicationController
|
78
|
+
prepend_before_filter do |controller|
|
79
|
+
unless ActiveRecord::Base.connection.is_a? ActiveReload::ConnectionProxy
|
80
|
+
ActiveReload::ConnectionProxy.setup!
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
Advanced
|
86
|
+
--------
|
87
|
+
|
88
|
+
The ActiveReload::MasterDatabase model uses a 'master\_database' setting that can either be
|
89
|
+
defined for all of your environments, or for each environment as a nested declaration.
|
90
|
+
|
91
|
+
The ActiveReload::SlaveDatabase model uses a 'slave\_database' setting that can only be
|
92
|
+
defined per environment.
|
93
|
+
|
94
|
+
Example:
|
95
|
+
|
96
|
+
login: &login
|
97
|
+
adapter: postgresql
|
98
|
+
host: localhost
|
99
|
+
port: 5432
|
100
|
+
|
101
|
+
production:
|
102
|
+
database: production_slave_database_name
|
103
|
+
<<: *login
|
104
|
+
|
105
|
+
master_database:
|
106
|
+
database: production_master_database_name
|
107
|
+
<<: *login
|
108
|
+
|
109
|
+
staging:
|
110
|
+
database: staging_database_name
|
111
|
+
host: slave-db-pool.local
|
112
|
+
<<: *login
|
113
|
+
master_database:
|
114
|
+
database: staging_database_name
|
115
|
+
host: master-db-server.local
|
116
|
+
<<: *login
|
117
|
+
|
118
|
+
qa:
|
119
|
+
database: qa_master_database_name
|
120
|
+
host: qa-master
|
121
|
+
<<: *login
|
122
|
+
slave_database:
|
123
|
+
database: qa_slave_database_name
|
124
|
+
host: qa-slave
|
125
|
+
<<: *login
|
126
|
+
|
127
|
+
development: # Does not use masochism
|
128
|
+
database: development_database_name
|
129
|
+
<<: *login
|
130
|
+
|
131
|
+
If you want a model to always use the Master database, you can inherit
|
132
|
+
`ActiveReload::MasterDatabase`. Any models with their own database connection will not be
|
133
|
+
affected.
|
134
|
+
|
135
|
+
### More control at setup
|
136
|
+
|
137
|
+
By default, masochism `setup!` is a shorthand for this:
|
138
|
+
|
139
|
+
ActiveReload::ConnectionProxy.setup_for ActiveReload::MasterDatabase, ActiveRecord::Base
|
140
|
+
|
141
|
+
The first argument is the model that has the master database connection established; the
|
142
|
+
second argument is the model whose `connection` gets hijacked by ConnectionProxy. But we
|
143
|
+
don't have to touch `ActiveRecord::Base` at all:
|
144
|
+
|
145
|
+
# set up MyMaster's connection as the master database connection for User:
|
146
|
+
ActiveReload::ConnectionProxy.setup_for MyMaster, User
|
147
|
+
|
148
|
+
### The controller filter
|
149
|
+
|
150
|
+
If you have any actions you know require the master database for both reads and writes,
|
151
|
+
simply do the following:
|
152
|
+
|
153
|
+
# in a controller:
|
154
|
+
around_filter ActiveReload::MasterFilter, :only => [:show, :edit, :update]
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module ActiveReload
|
2
|
+
class MasterDatabase < ActiveRecord::Base
|
3
|
+
self.abstract_class = true
|
4
|
+
establish_connection configurations[Rails.env]['master_database'] || configurations['master_database'] || Rails.env
|
5
|
+
end
|
6
|
+
|
7
|
+
class SlaveDatabase < ActiveRecord::Base
|
8
|
+
self.abstract_class = true
|
9
|
+
def self.name
|
10
|
+
ActiveRecord::Base.name
|
11
|
+
end
|
12
|
+
establish_connection configurations[Rails.env]['slave_database'] || Rails.env
|
13
|
+
end
|
14
|
+
|
15
|
+
class ConnectionProxy
|
16
|
+
|
17
|
+
def initialize(master_class, slave_class)
|
18
|
+
@master = master_class
|
19
|
+
@slave = slave_class
|
20
|
+
@current = :slave
|
21
|
+
end
|
22
|
+
|
23
|
+
def master
|
24
|
+
@slave.connection_handler.retrieve_connection(@master)
|
25
|
+
end
|
26
|
+
|
27
|
+
def slave
|
28
|
+
@slave.retrieve_connection
|
29
|
+
end
|
30
|
+
|
31
|
+
def current
|
32
|
+
send @current
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.setup!
|
36
|
+
if slave_defined?
|
37
|
+
setup_for ActiveReload::MasterDatabase, ActiveReload::SlaveDatabase
|
38
|
+
else
|
39
|
+
setup_for ActiveReload::MasterDatabase
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.slave_defined?
|
44
|
+
ActiveRecord::Base.configurations[Rails.env]['slave_database']
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.setup_for(master, slave = nil)
|
48
|
+
slave ||= ActiveRecord::Base
|
49
|
+
slave.send :include, ActiveRecordConnectionMethods
|
50
|
+
ActiveRecord::Observer.send :include, ActiveReload::ObserverExtensions
|
51
|
+
slave.connection_proxy = new(master, slave)
|
52
|
+
end
|
53
|
+
|
54
|
+
def with_master(to_slave = true)
|
55
|
+
set_to_master!
|
56
|
+
yield
|
57
|
+
ensure
|
58
|
+
set_to_slave! if to_slave
|
59
|
+
end
|
60
|
+
|
61
|
+
def set_to_master!
|
62
|
+
unless @current == :master
|
63
|
+
# @slave.logger.info caller.inspect
|
64
|
+
@slave.logger.info "Switching to Master"
|
65
|
+
@current = :master
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def set_to_slave!
|
70
|
+
unless @current == :slave
|
71
|
+
# @master.logger.info caller.inspect
|
72
|
+
@master.logger.info "Switching to Slave"
|
73
|
+
@current = :slave
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
delegate :insert, :update, :delete, :create_table, :rename_table, :drop_table, :add_column, :remove_column,
|
78
|
+
:change_column, :change_column_default, :rename_column, :add_index, :remove_index, :initialize_schema_migrations_table, :select_values,
|
79
|
+
:dump_schema_information, :execute, :columns, :to => :master
|
80
|
+
|
81
|
+
def transaction(start_db_transaction = true, &block)
|
82
|
+
with_master(start_db_transaction) do
|
83
|
+
master.transaction(start_db_transaction, &block)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def method_missing(method, *args, &block)
|
88
|
+
val = current.send(method, *args, &block)
|
89
|
+
val
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
module ActiveRecordConnectionMethods
|
94
|
+
def self.included(base)
|
95
|
+
base.alias_method_chain :reload, :master
|
96
|
+
|
97
|
+
class << base
|
98
|
+
def connection_proxy=(proxy)
|
99
|
+
@@connection_proxy = proxy
|
100
|
+
end
|
101
|
+
|
102
|
+
# hijack the original method
|
103
|
+
#only return if current is defined
|
104
|
+
#calling retrieve_connection thru current also trigger code that
|
105
|
+
#raises a ConnectionError if the db is not created, this fixes the
|
106
|
+
#problem with running rake db:create in rails 2.2.2
|
107
|
+
def connection
|
108
|
+
@@connection_proxy if @@connection_proxy.current
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def reload_with_master(*args, &block)
|
114
|
+
if connection.class.name == "ActiveReload::ConnectionProxy"
|
115
|
+
connection.with_master { reload_without_master }
|
116
|
+
else
|
117
|
+
reload_without_master
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# extend observer to always use the master database
|
123
|
+
# observers only get triggered on writes, so shouldn't be a performance hit
|
124
|
+
# removes a race condition if you are using conditionals in the observer
|
125
|
+
module ObserverExtensions
|
126
|
+
def self.included(base)
|
127
|
+
base.alias_method_chain :update, :masterdb
|
128
|
+
end
|
129
|
+
|
130
|
+
# Send observed_method(object) if the method exists.
|
131
|
+
def update_with_masterdb(observed_method, object) #:nodoc:
|
132
|
+
if object.class.connection.respond_to?(:with_master)
|
133
|
+
object.class.connection.with_master do
|
134
|
+
update_without_masterdb(observed_method, object)
|
135
|
+
end
|
136
|
+
else
|
137
|
+
update_without_masterdb(observed_method, object)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module ActiveReload
|
2
|
+
# MasterFilter should be used as an around filter in your controllers that require certain actions to use the Master DB for reads as well as writes
|
3
|
+
class MasterFilter
|
4
|
+
def self.filter(controller, &block)
|
5
|
+
if ActiveRecord::Base.connection.respond_to?(:with_master)
|
6
|
+
ActiveRecord::Base.connection.with_master(&block)
|
7
|
+
else
|
8
|
+
yield block
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/masochism.rb
ADDED
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
namespace :db do
|
4
|
+
task :create => :environment do
|
5
|
+
create_database(ActiveRecord::Base.configurations[RAILS_ENV]["master_database"])
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
require 'active_reload/connection_proxy'
|
10
|
+
require 'active_reload/master_filter'
|
11
|
+
|
12
|
+
#ActiveReload::ConnectionProxy.setup!
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: revolutionhealth-masochism
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Revolution Health
|
8
|
+
autorequire: masochism
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2009-01-23 00:00:00 -08:00
|
13
|
+
default_executable:
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: ActiveRecord connection proxy for master/slave connections
|
17
|
+
email: rails@revolutionhealth.com
|
18
|
+
executables: []
|
19
|
+
|
20
|
+
extensions: []
|
21
|
+
|
22
|
+
extra_rdoc_files:
|
23
|
+
- LICENSE
|
24
|
+
- README.markdown
|
25
|
+
files:
|
26
|
+
- README.markdown
|
27
|
+
- lib/active_reload/connection_proxy.rb
|
28
|
+
- lib/active_reload/master_filter.rb
|
29
|
+
- lib/masochism.rb
|
30
|
+
- LICENSE
|
31
|
+
has_rdoc: true
|
32
|
+
homepage: http://github.com/revolutionhealth/masochism/
|
33
|
+
post_install_message:
|
34
|
+
rdoc_options: []
|
35
|
+
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: "0"
|
43
|
+
version:
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
version: "0"
|
49
|
+
version:
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
rubyforge_project:
|
53
|
+
rubygems_version: 1.2.0
|
54
|
+
signing_key:
|
55
|
+
specification_version: 1
|
56
|
+
summary: ActiveRecord connection proxy for master/slave connections
|
57
|
+
test_files: []
|
58
|
+
|