glebpom-multi_db 0.2.2

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/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT license:
2
+ Copyright (c) 2008 Maximilian Schöfmann
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in
12
+ all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,238 @@
1
+ = multi_db
2
+
3
+ IMPORANT: version with some hacks for compatibility with ultrasphinx. Used in qik.com
4
+
5
+ =====-- This GEM was inspired by Rick Olson's "masochism"-Plugin
6
+
7
+ multi_db uses a connection proxy, which sends read queries to slave databases,
8
+ and all write queries to the master database (Read/Write Split).
9
+ Within transactions, while executing ActiveRecord Observers and
10
+ within "with_master" blocks (see below), even read queries are sent to the
11
+ master database.
12
+
13
+ === Important changes in 0.2.0
14
+
15
+ * As of this version, <tt>ActiveRecord::Base.connection</tt> does not return the
16
+ connection proxy by default anymore (therefore the jump to 0.2.0). Only models
17
+ inheriting from AR::B return the proxy, unless they are defined as master_models
18
+ (see below). If you want to access the connection proxy from AR::B directly,
19
+ use <tt>ActiveRecord::Base.connection_proxy</tt>.
20
+
21
+ * This version is the first attempt for thread-safety of this gem. <em>There might
22
+ still be some threading issues left!</em>. So please test your apps thoroughly
23
+ and report any issues you might encounter.
24
+
25
+ * <tt>CGI::Session::ActiveRecordStore::Session</tt> is now automatically registered
26
+ as a master model.
27
+
28
+ === Caveats
29
+
30
+ * works only with activerecord 2.1, 2.2 and 2.3
31
+
32
+ === Install
33
+
34
+ gem sources --add http://gems.github.com # only if you haven't already added github
35
+ gem install glebpom-multi_db
36
+
37
+ When using Rails, add this to your environment.rb:
38
+
39
+ config.gem 'glebpom-multi_db', :lib => 'multi_db', :source => 'http://gems.github.com'
40
+
41
+ === Setup
42
+
43
+ In your database.yml, add sections for the slaves, e.g.:
44
+
45
+ production: # that would be the master
46
+ adapter: mysql
47
+ database: myapp_production
48
+ username: root
49
+ password:
50
+ host: localhost
51
+
52
+ production_slave_database: # that would be a slave
53
+ adapter: mysql
54
+ database: myapp_production
55
+ username: root
56
+ password:
57
+ host: 10.0.0.2
58
+
59
+ production_slave_database_2: # another slave
60
+ ...
61
+ production_slave_database_in_india: # yet another one
62
+ ...
63
+
64
+ *NOTE*: multi_db identifies slave databases by looking for entries of the form
65
+ "<tt><environment>_slave_database<_optional_name></tt>". As a (useless) side effect you
66
+ get abstract classes named <tt>MultiDb::SlaveDatabaseInIndia</tt> etc.
67
+ The advantage of specifying the slaves explicitly, instead of the master, is that
68
+ you can use the same configuration file for scripts that don't use multi_db.
69
+ Also, when you decide to disable multi_db for some reason, you don't have to
70
+ swap hosts in your <tt>database.yml</tt> from master to slave (which is easy to forget...).
71
+
72
+ To enable the proxy globally, add this to your environment.rb, or some file in
73
+ config/initializers:
74
+
75
+ MultiDb::ConnectionProxy.setup!
76
+
77
+ If you only want to enable it for specific environments, add this to
78
+ the corresponding file in config/environments:
79
+
80
+ config.after_initialize do
81
+ MultiDb::ConnectionProxy.setup!
82
+ end
83
+
84
+ In the development and test environments, you can use identical configurations
85
+ for master and slave connections. This can help you finding (some of the) issues
86
+ your application might have with a replicated database setup without actually having
87
+ one on your development machine.
88
+
89
+ === Using with Phusion Passenger
90
+
91
+ With Passengers smart spawning method, child processes forked by the ApplicationSpawner
92
+ won't have the connection proxy set up properly.
93
+
94
+ To make it work, add this to your <tt>environment.rb</tt> or an initializer script
95
+ (e.g. <tt>config/initializers/connection_proxy.rb</tt>):
96
+
97
+ if defined?(PhusionPassenger)
98
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
99
+ if forked
100
+ # ... set MultiDb configuration options, if any ...
101
+ MultiDb::ConnectionProxy.setup!
102
+ end
103
+ end
104
+ else # not using passenger (e.g. development/testing)
105
+ # ... set MultiDb configuration options, if any ...
106
+ MultiDb::ConnectionProxy.setup!
107
+ end
108
+
109
+ Thanks to Nathan Esquenazi for testing this.
110
+
111
+ === Forcing the master for certain actions
112
+
113
+ Just add this to your controller:
114
+
115
+ around_filter(:only => :foo_action) { |c,a| ActiveRecord::Base.connection_proxy.with_master { a.call } }
116
+
117
+ === Forcing the master for certain models
118
+
119
+ In your environment.rb or an initializer, add this *before* the call to <tt>setup!</tt>:
120
+
121
+ MultiDb::ConnectionProxy.master_models = ['CGI::Session::ActiveRecordStore::Session', 'PaymentTransaction', ...]
122
+ MultiDb::ConnectionProxy.setup!
123
+
124
+ *NOTE*: You cannot safely add more master_models after calling <tt>setup!</tt>.
125
+
126
+ === Making one slave database sticky during a request
127
+
128
+ This can be useful to leverage database level query caching as all queries will
129
+ be sent to the same slave database during one web request.
130
+
131
+ To enable, add this to your environment.rb just before <tt>MultiDb::ConnectionProxy.setup!</tt>:
132
+
133
+ MultiDb::ConnectionProxy.sticky_slave = true
134
+
135
+ And add this to your ApplicationController:
136
+
137
+ after_filter { ActiveRecord::Base.connection_proxy.next_reader! }
138
+
139
+ *NOTE*: It's not possible to toggle this mode in a running process, as the dynamically
140
+ generated methods will have the initially defined "stickyness" built in.
141
+
142
+ === Usage outside of Rails
143
+
144
+ You can use multi_db together with other framworks or in standalone scripts.
145
+ Example:
146
+
147
+ require 'rubygems'
148
+ require 'active_record'
149
+ require 'multi_db'
150
+
151
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
152
+ ActiveRecord::Base.configurations = {
153
+ 'development' => {
154
+ 'adapter' => 'mysql',
155
+ 'host' => 'localhost',
156
+ 'username' => 'root',
157
+ 'database' => 'multi_db_test'
158
+ },
159
+ 'development_slave_database' => {
160
+ 'adapter' => 'mysql',
161
+ 'host' => 'localhost',
162
+ 'username' => 'root',
163
+ 'database' => 'multi_db_test'
164
+ }
165
+ }
166
+ ActiveRecord::Base.establish_connection :development
167
+ MultiDb::ConnectionProxy.setup!
168
+
169
+ class MyModel < ActiveRecord::Base
170
+ # ...
171
+ end
172
+
173
+ # ...
174
+
175
+ Note that the configurations hash should contain strings as keys instead of symbols.
176
+
177
+ === Differences to "masochism":
178
+
179
+ * Supports multiple slave databases (round robin)
180
+ * It sends everything except "select ..." queries to the master, instead of
181
+ sending only specific things to the master and anything "else" to the slave.
182
+ This avoids accidential writes to the master when there are API changes in
183
+ ActiveRecord which haven't been picked up by multi_db yet.
184
+ Note that this behaviour will also always send helper methods like "+quote+" or
185
+ "<tt>add_limit!</tt>" to the master connection object, which doesn't add any
186
+ more load on the master, as these methods don't communicate with the db server
187
+ itself.
188
+ * It uses its own query cache as the slave's cache isn't emptied when there are
189
+ changes on the master
190
+ * It supports immediate failover for slave connections
191
+ * It will wait some time before trying to query a failed slave database again
192
+ * It supports nesting "with_master"-blocks, without unexpectedly switching you
193
+ back to the slave again
194
+ * It schedules a reconnect of the master connection if statements fail there.
195
+ This might help with HA setups using virtual IPs (a test setup would be nice
196
+ to verify this)
197
+ * You specify slave databases in the configuration instead of specifying an extra
198
+ master database. This makes disabling or removing multi_db less dangerous
199
+ (Update: Recent versions of masochism support this, too).
200
+ * There are no <tt>set_to_master!</tt> and <tt>set_to_slave!</tt> methods, just
201
+ <tt>with_master(&block)</tt>
202
+ * All proxied methods are dynamically generated for better performance
203
+
204
+ === See also
205
+
206
+ ==== Masochism
207
+
208
+ The original plugin:
209
+
210
+ * http://github.com/technoweenie/masochism
211
+
212
+ ==== DataFabric
213
+
214
+ A solution by FiveRuns, also based on masochism but without the "nested with_master"-issue,
215
+ threadsafe and allows sharding of data.
216
+
217
+ * http://github.com/fiveruns/data_fabric
218
+
219
+ === Contributors
220
+
221
+ * Matt Conway http://github.com/wr0ngway
222
+ * Matthias Marshall http://github.com/webops
223
+
224
+ === Ideas
225
+
226
+ See: http://github.com/schoefmax/multi_db/wikis/home
227
+
228
+ === Running specs
229
+
230
+ If you haven't already, install the rspec gem, then create an empty database
231
+ called "multi_db_test" (you might want to tweak the spec/config/database.yml).
232
+ From the plugin directory, run:
233
+
234
+ spec spec
235
+
236
+
237
+ Copyright (c) 2008, Max Schoefmann <max (a) pragmatic-it de>
238
+ Released under the MIT license
data/lib/multi_db.rb ADDED
@@ -0,0 +1,6 @@
1
+ require 'tlattr_accessors'
2
+ require 'multi_db/scheduler'
3
+ require 'multi_db/active_record_extensions'
4
+ require 'multi_db/observer_extensions'
5
+ require 'multi_db/query_cache_compat'
6
+ require 'multi_db/connection_proxy'
@@ -0,0 +1,55 @@
1
+ module MultiDb
2
+ module ActiveRecordExtensions
3
+ def self.included(base)
4
+ base.send :include, InstanceMethods
5
+ base.send :extend, ClassMethods
6
+ base.alias_method_chain :reload, :master
7
+ base.cattr_accessor :connection_proxy
8
+ # handle subclasses which were defined by the framework or plugins
9
+ base.send(:subclasses).each do |child|
10
+ child.hijack_connection
11
+ end
12
+ end
13
+
14
+ module InstanceMethods
15
+ def reload_with_master(*args, &block)
16
+ self.connection_proxy.with_master { reload_without_master }
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+ # Make sure transactions always switch to the master
22
+ def transaction(&block)
23
+ if self.connection.kind_of?(ConnectionProxy)
24
+ super
25
+ else
26
+ self.connection_proxy.with_master { super }
27
+ end
28
+ end
29
+
30
+ # make caching always use the ConnectionProxy
31
+ def cache(&block)
32
+ if ActiveRecord::Base.configurations.blank?
33
+ yield
34
+ else
35
+ self.connection_proxy.cache(&block)
36
+ end
37
+ end
38
+
39
+ def inherited(child)
40
+ super
41
+ child.hijack_connection
42
+ end
43
+
44
+ def hijack_connection
45
+ return if ConnectionProxy.master_models.include?(self.to_s)
46
+ logger.info "[MULTIDB] hijacking connection for #{self.to_s}"
47
+ class << self
48
+ def connection
49
+ self.connection_proxy
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,186 @@
1
+ module MultiDb
2
+ class ConnectionProxy
3
+ include QueryCacheCompat
4
+ include ActiveRecord::ConnectionAdapters::QueryCache
5
+ extend ThreadLocalAccessors
6
+
7
+ # Safe methods are those that should either go to the slave ONLY or go
8
+ # to the current active connection.
9
+ SAFE_METHODS = [ :select_all, :select_one, :select_value, :select_values,
10
+ :select_rows, :select, :verify!, :raw_connection, :active?, :reconnect!,
11
+ :disconnect!, :reset_runtime, :log, :log_info ]
12
+
13
+ DEFAULT_MASTER_MODELS = ['CGI::Session::ActiveRecordStore::Session']
14
+
15
+ attr_accessor :master
16
+ tlattr_accessor :master_depth, :current, true
17
+
18
+ class << self
19
+
20
+ # defaults to RAILS_ENV if multi_db is used with Rails
21
+ # defaults to 'development' when used outside Rails
22
+ attr_accessor :environment
23
+
24
+ # a list of models that should always go directly to the master
25
+ #
26
+ # Example:
27
+ #
28
+ # MultiDb::ConnectionProxy.master_models = ['MySessionStore', 'PaymentTransaction']
29
+ attr_accessor :master_models
30
+
31
+ # decides if we should switch to the next reader automatically.
32
+ # If set to false, an after|before_filter in the ApplicationController
33
+ # has to do this.
34
+ # This will not affect failover if a master is unavailable.
35
+ attr_accessor :sticky_slave
36
+
37
+ # Replaces the connection of ActiveRecord::Base with a proxy and
38
+ # establishes the connections to the slaves.
39
+ def setup!
40
+ self.master_models ||= DEFAULT_MASTER_MODELS
41
+ self.environment ||= (defined?(RAILS_ENV) ? RAILS_ENV : 'development')
42
+ self.sticky_slave ||= false
43
+
44
+ master = ActiveRecord::Base
45
+ slaves = init_slaves
46
+ raise "No slaves databases defined for environment: #{self.environment}" if slaves.empty?
47
+ master.send :include, MultiDb::ActiveRecordExtensions
48
+ ActiveRecord::Observer.send :include, MultiDb::ObserverExtensions
49
+ master.connection_proxy = new(master, slaves)
50
+ master.logger.info("** multi_db with master and #{slaves.length} slave#{"s" if slaves.length > 1} loaded.")
51
+ end
52
+
53
+ protected
54
+
55
+ # Slave entries in the database.yml must be named like this
56
+ # development_slave_database:
57
+ # or
58
+ # development_slave_database1:
59
+ # or
60
+ # production_slave_database_someserver:
61
+ # These would be available later as MultiDb::SlaveDatabaseSomeserver
62
+ def init_slaves
63
+ returning([]) do |slaves|
64
+ ActiveRecord::Base.configurations.keys.each do |name|
65
+ if name.to_s =~ /#{self.environment}_(slave_database.*)/
66
+ MultiDb.module_eval %Q{
67
+ class #{$1.camelize} < ActiveRecord::Base
68
+ self.abstract_class = true
69
+ establish_connection :#{name}
70
+ end
71
+ }, __FILE__, __LINE__
72
+ slaves << "MultiDb::#{$1.camelize}".constantize
73
+ end
74
+ end
75
+ end
76
+ end
77
+
78
+ private :new
79
+
80
+ end
81
+
82
+ def initialize(master, slaves)
83
+ @slaves = Scheduler.new(slaves)
84
+ @master = master
85
+ @reconnect = false
86
+ @config = @master.connection.instance_variable_get("@config")
87
+ self.current = @slaves.current
88
+ self.master_depth = 0
89
+ end
90
+
91
+ def slave
92
+ @slaves.current
93
+ end
94
+
95
+ def with_master
96
+ self.current = @master
97
+ self.master_depth += 1
98
+ yield
99
+ ensure
100
+ self.master_depth -= 1
101
+ self.current = slave if master_depth.zero?
102
+ end
103
+
104
+ def transaction(start_db_transaction = true, &block)
105
+ with_master { @master.retrieve_connection.transaction(start_db_transaction, &block) }
106
+ end
107
+
108
+ # Calls the method on master/slave and dynamically creates a new
109
+ # method on success to speed up subsequent calls
110
+ def method_missing(method, *args, &block)
111
+ returning(send(target_method(method), method, *args, &block)) do
112
+ create_delegation_method!(method)
113
+ end
114
+ end
115
+
116
+ # Switches to the next slave database for read operations.
117
+ # Fails over to the master database if all slaves are unavailable.
118
+ def next_reader!
119
+ return unless master_depth.zero? # don't if in with_master block
120
+ self.current = @slaves.next
121
+ rescue Scheduler::NoMoreItems
122
+ logger.warn "[MULTIDB] All slaves are blacklisted. Reading from master"
123
+ self.current = @master
124
+ end
125
+
126
+ protected
127
+
128
+ def create_delegation_method!(method)
129
+ self.instance_eval %Q{
130
+ def #{method}(*args, &block)
131
+ #{'next_reader!' unless self.class.sticky_slave || unsafe?(method)}
132
+ #{target_method(method)}(:#{method}, *args, &block)
133
+ end
134
+ }, __FILE__, __LINE__
135
+ end
136
+
137
+ def target_method(method)
138
+ unsafe?(method) ? :send_to_master : :send_to_current
139
+ end
140
+
141
+ def send_to_master(method, *args, &block)
142
+ reconnect_master! if @reconnect
143
+ @master.retrieve_connection.send(method, *args, &block)
144
+ rescue => e
145
+ raise_master_error(e)
146
+ end
147
+
148
+ def send_to_current(method, *args, &block)
149
+ reconnect_master! if @reconnect && master?
150
+ current.retrieve_connection.send(method, *args, &block)
151
+ rescue NotImplementedError, NoMethodError
152
+ raise
153
+ rescue => e # TODO don't rescue everything
154
+ raise_master_error(e) if master?
155
+ logger.warn "[MULTIDB] Error reading from slave database"
156
+ logger.error %(#{e.message}\n#{e.backtrace.join("\n")})
157
+ @slaves.blacklist!(current)
158
+ next_reader!
159
+ retry
160
+ end
161
+
162
+ def reconnect_master!
163
+ @master.retrieve_connection.reconnect!
164
+ @reconnect = false
165
+ end
166
+
167
+ def raise_master_error(error)
168
+ logger.fatal "[MULTIDB] Error accessing master database. Scheduling reconnect"
169
+ @reconnect = true
170
+ raise error
171
+ end
172
+
173
+ def unsafe?(method)
174
+ !SAFE_METHODS.include?(method)
175
+ end
176
+
177
+ def master?
178
+ current == @master
179
+ end
180
+
181
+ def logger
182
+ ActiveRecord::Base.logger
183
+ end
184
+
185
+ end
186
+ end
@@ -0,0 +1,18 @@
1
+ module MultiDb
2
+ module ObserverExtensions
3
+ def self.included(base)
4
+ base.alias_method_chain :update, :masterdb
5
+ end
6
+
7
+ # Send observed_method(object) if the method exists.
8
+ def update_with_masterdb(observed_method, object) #:nodoc:
9
+ if object.class.connection.respond_to?(:with_master)
10
+ object.class.connection.with_master do
11
+ update_without_masterdb(observed_method, object)
12
+ end
13
+ else
14
+ update_without_masterdb(observed_method, object)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,21 @@
1
+ module MultiDb
2
+ # Implements the methods expected by the QueryCache module
3
+ module QueryCacheCompat
4
+ def select_all(*a, &b)
5
+ next_reader! unless ConnectionProxy.sticky_slave
6
+ send_to_current(:select_all, *a, &b)
7
+ end
8
+ def columns(*a, &b)
9
+ send_to_current(:columns, *a, &b)
10
+ end
11
+ def insert(*a, &b)
12
+ send_to_master(:insert, *a, &b)
13
+ end
14
+ def update(*a, &b)
15
+ send_to_master(:update, *a, &b)
16
+ end
17
+ def delete(*a, &b)
18
+ send_to_master(:delete, *a, &b)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,41 @@
1
+ module MultiDb
2
+ class Scheduler
3
+ class NoMoreItems < Exception; end
4
+ extend ThreadLocalAccessors
5
+
6
+ attr :items
7
+ delegate :[], :[]=, :to => :items
8
+ tlattr_accessor :current_index, true
9
+
10
+ def initialize(items, blacklist_timeout = 1.minute)
11
+ @n = items.length
12
+ @items = items
13
+ @blacklist = Array.new(@n, Time.at(0))
14
+ @blacklist_timeout = blacklist_timeout
15
+ self.current_index = 0
16
+ end
17
+
18
+ def blacklist!(item)
19
+ @blacklist[@items.index(item)] = Time.now
20
+ end
21
+
22
+ def current
23
+ @items[current_index]
24
+ end
25
+
26
+ def next
27
+ previous = current_index
28
+ until(@blacklist[next_index!] < Time.now - @blacklist_timeout) do
29
+ raise NoMoreItems, 'All items are blacklisted' if current_index == previous
30
+ end
31
+ current
32
+ end
33
+
34
+ protected
35
+
36
+ def next_index!
37
+ self.current_index = (current_index + 1) % @n
38
+ end
39
+
40
+ end
41
+ end
data/multi_db.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{multi_db}
5
+ s.version = "0.2.2"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Maximilian Sch\303\266fmann", "Gleb Pomykalov"]
9
+ s.date = %q{2009-09-22}
10
+ s.description = "Connection proxy for ActiveRecord for single master / multiple slave database deployments"
11
+ s.email = "gleb.pomykalov@qik.com"
12
+ s.extra_rdoc_files = ["LICENSE", "README.rdoc"]
13
+ s.files = ["lib/multi_db.rb", "lib/multi_db/active_record_extensions.rb", "lib/multi_db/connection_proxy.rb", "lib/multi_db/observer_extensions.rb", "lib/multi_db/query_cache_compat.rb", "lib/multi_db/scheduler.rb", "LICENSE", "README.rdoc", "spec/config/database.yml", "spec/connection_proxy_spec.rb", "spec/scheduler_spec.rb", "spec/spec_helper.rb", "multi_db.gemspec"]
14
+ s.has_rdoc = true
15
+ s.homepage = "http://github.com/glebpom/multi_db"
16
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "multi_db", "--main", "README.rdoc"]
17
+ s.require_paths = ["lib"]
18
+ s.rubyforge_project = "multi_db"
19
+ s.rubygems_version = %q{1.3.1}
20
+ s.summary = "Connection proxy for ActiveRecord for single master / multiple slave database deployments"
21
+
22
+ if s.respond_to? :specification_version then
23
+ s.specification_version = 2
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ s.add_runtime_dependency('activerecord', [">= 2.1.0"])
27
+ s.add_runtime_dependency('schoefmax-tlattr_accessors', [">= 0.0.3"])
28
+ else
29
+ s.add_dependency('activerecord', [">= 2.1.0"])
30
+ s.add_dependency('schoefmax-tlattr_accessors', [">= 0.0.3"])
31
+ end
32
+ else
33
+ s.add_dependency('activerecord', [">= 2.1.0"])
34
+ s.add_dependency('schoefmax-tlattr_accessors', [">= 0.0.3"])
35
+ end
36
+ end
@@ -0,0 +1,23 @@
1
+ test:
2
+ adapter: mysql
3
+ database: multi_db_test
4
+ username: root
5
+ password:
6
+ host: 127.0.0.1
7
+ pool: 5
8
+
9
+ test_slave_database_1:
10
+ adapter: mysql
11
+ database: multi_db_test
12
+ username: root
13
+ password:
14
+ host: 127.0.0.1
15
+ pool: 5
16
+
17
+ test_slave_database_2:
18
+ adapter: mysql
19
+ database: multi_db_test
20
+ username: root
21
+ password:
22
+ host: 127.0.0.1
23
+ pool: 5
@@ -0,0 +1,229 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require MULTI_DB_SPEC_DIR + '/../lib/multi_db/query_cache_compat'
3
+ require MULTI_DB_SPEC_DIR + '/../lib/multi_db/active_record_extensions'
4
+ require MULTI_DB_SPEC_DIR + '/../lib/multi_db/observer_extensions'
5
+ require MULTI_DB_SPEC_DIR + '/../lib/multi_db/scheduler'
6
+ require MULTI_DB_SPEC_DIR + '/../lib/multi_db/connection_proxy'
7
+
8
+ RAILS_ROOT = MULTI_DB_SPEC_DIR
9
+
10
+ describe MultiDb::ConnectionProxy do
11
+
12
+ before(:all) do
13
+ ActiveRecord::Base.configurations = MULTI_DB_SPEC_CONFIG
14
+ ActiveRecord::Base.establish_connection :test
15
+ ActiveRecord::Migration.verbose = false
16
+ ActiveRecord::Migration.create_table(:master_models, :force => true) {}
17
+ class MasterModel < ActiveRecord::Base; end
18
+ ActiveRecord::Migration.create_table(:foo_models, :force => true) {|t| t.string :bar}
19
+ class FooModel < ActiveRecord::Base; end
20
+ @sql = 'SELECT 1 + 1 FROM DUAL'
21
+ end
22
+
23
+ before(:each) do
24
+ MultiDb::ConnectionProxy.master_models = ['MasterModel']
25
+ MultiDb::ConnectionProxy.setup!
26
+ @proxy = ActiveRecord::Base.connection_proxy
27
+ @master = @proxy.master.retrieve_connection
28
+ @slave1 = MultiDb::SlaveDatabase1.retrieve_connection
29
+ @slave2 = MultiDb::SlaveDatabase2.retrieve_connection
30
+ end
31
+
32
+ after(:each) do
33
+ ActiveRecord::Base.send :alias_method, :reload, :reload_without_master
34
+ end
35
+
36
+ it 'AR::B should respond to #connection_proxy' do
37
+ ActiveRecord::Base.connection_proxy.should be_kind_of(MultiDb::ConnectionProxy)
38
+ end
39
+
40
+ it 'FooModel#connection should return an instance of MultiDb::ConnectionProxy' do
41
+ FooModel.connection.should be_kind_of(MultiDb::ConnectionProxy)
42
+ end
43
+
44
+ it 'MasterModel#connection should not return an instance of MultiDb::ConnectionProxy' do
45
+ MasterModel.connection.should_not be_kind_of(MultiDb::ConnectionProxy)
46
+ end
47
+
48
+ it "should generate classes for each entry in the database.yml" do
49
+ defined?(MultiDb::SlaveDatabase1).should_not be_nil
50
+ defined?(MultiDb::SlaveDatabase2).should_not be_nil
51
+ end
52
+
53
+ it 'should handle nested with_master-blocks correctly' do
54
+ @proxy.current.should_not == @proxy.master
55
+ @proxy.with_master do
56
+ @proxy.current.should == @proxy.master
57
+ @proxy.with_master do
58
+ @proxy.current.should == @proxy.master
59
+ @proxy.with_master do
60
+ @proxy.current.should == @proxy.master
61
+ end
62
+ @proxy.current.should == @proxy.master
63
+ end
64
+ @proxy.current.should == @proxy.master
65
+ end
66
+ @proxy.current.should_not == @proxy.master
67
+ end
68
+
69
+ it 'should perform transactions on the master' do
70
+ @master.should_receive(:select_all).exactly(1) # makes sure the first one goes to a slave
71
+ @proxy.select_all(@sql)
72
+ ActiveRecord::Base.transaction do
73
+ @proxy.select_all(@sql)
74
+ end
75
+ end
76
+
77
+ it 'should switch to the next reader on selects' do
78
+ @slave1.should_receive(:select_one).exactly(2)
79
+ @slave2.should_receive(:select_one).exactly(2)
80
+ 4.times { @proxy.select_one(@sql) }
81
+ end
82
+
83
+ it 'should not switch to the next reader when whithin a with_master-block' do
84
+ @master.should_receive(:select_one).twice
85
+ @slave1.should_not_receive(:select_one)
86
+ @slave2.should_not_receive(:select_one)
87
+ @proxy.with_master do
88
+ 2.times { @proxy.select_one(@sql) }
89
+ end
90
+ end
91
+
92
+ it 'should send dangerous methods to the master' do
93
+ meths = [:insert, :update, :delete, :execute]
94
+ meths.each do |meth|
95
+ @slave1.stub!(meth).and_raise(RuntimeError)
96
+ @master.should_receive(meth).and_return(true)
97
+ @proxy.send(meth, @sql)
98
+ end
99
+ end
100
+
101
+ it 'should dynamically generate safe methods' do
102
+ @proxy.should_not respond_to(:select_value)
103
+ @proxy.select_value(@sql)
104
+ @proxy.should respond_to(:select_value)
105
+ end
106
+
107
+ it 'should cache queries using select_all' do
108
+ ActiveRecord::Base.cache do
109
+ # next_reader will be called and switch to the SlaveDatabase2
110
+ @slave2.should_receive(:select_all).exactly(1)
111
+ @slave1.should_not_receive(:select_all)
112
+ @master.should_not_receive(:select_all)
113
+ 3.times { @proxy.select_all(@sql) }
114
+ end
115
+ end
116
+
117
+ it 'should invalidate the cache on insert, delete and update' do
118
+ ActiveRecord::Base.cache do
119
+ meths = [:insert, :update, :delete]
120
+ meths.each do |meth|
121
+ @master.should_receive(meth).and_return(true)
122
+ end
123
+ @slave2.should_receive(:select_all).twice
124
+ @slave1.should_receive(:select_all).once
125
+ 3.times do |i|
126
+ @proxy.select_all(@sql)
127
+ @proxy.send(meths[i])
128
+ end
129
+ end
130
+ end
131
+
132
+ it 'should retry the next slave when one fails and finally fall back to the master' do
133
+ @slave1.should_receive(:select_all).once.and_raise(RuntimeError)
134
+ @slave2.should_receive(:select_all).once.and_raise(RuntimeError)
135
+ @master.should_receive(:select_all).and_return(true)
136
+ @proxy.select_all(@sql)
137
+ end
138
+
139
+ it 'should try to reconnect the master connection after the master has failed' do
140
+ @master.should_receive(:update).and_raise(RuntimeError)
141
+ lambda { @proxy.update(@sql) }.should raise_error
142
+ @master.should_receive(:reconnect!).and_return(true)
143
+ @master.should_receive(:insert).and_return(1)
144
+ @proxy.insert(@sql)
145
+ end
146
+
147
+ it 'should reload models from the master' do
148
+ foo = FooModel.create!(:bar => 'baz')
149
+ foo.bar = "not_saved"
150
+ @slave1.should_not_receive(:select_all)
151
+ @slave2.should_not_receive(:select_all)
152
+ foo.reload
153
+ # we didn't stub @master#select_all here, check that we actually hit the db
154
+ foo.bar.should == 'baz'
155
+ end
156
+
157
+ describe 'with sticky_slave ' do
158
+
159
+ before { MultiDb::ConnectionProxy.sticky_slave = true }
160
+ after { MultiDb::ConnectionProxy.sticky_slave = false }
161
+
162
+ it 'should not switch to the next reader automatically' do
163
+ @slave1.should_receive(:select_all).exactly(3)
164
+ @slave2.should_receive(:select_all).exactly(0)
165
+ 3.times { @proxy.select_all(@sql) }
166
+ end
167
+
168
+ it '#next_reader! should switch to the next slave' do
169
+ @slave1.should_receive(:select_one).exactly(3)
170
+ @slave2.should_receive(:select_one).exactly(7)
171
+ 3.times { @proxy.select_one(@sql) }
172
+ @proxy.next_reader!
173
+ 7.times { @proxy.select_one(@sql) }
174
+ end
175
+
176
+ end
177
+
178
+ describe '(accessed from multiple threads)' do
179
+ # NOTE: We cannot put expectations on the connection objects itself
180
+ # for the threading specs, as connection pooling will cause
181
+ # different connections being returned for different threads.
182
+
183
+ it '#current and #next_reader! should be local to the thread' do
184
+ @proxy.current.should == MultiDb::SlaveDatabase1
185
+ @proxy.next_reader!.should == MultiDb::SlaveDatabase2
186
+ Thread.new do
187
+ @proxy.current.should == MultiDb::SlaveDatabase1
188
+ @proxy.next_reader!.should == MultiDb::SlaveDatabase2
189
+ @proxy.current.should == MultiDb::SlaveDatabase2
190
+ @proxy.next_reader!.should == MultiDb::SlaveDatabase1
191
+ @proxy.current.should == MultiDb::SlaveDatabase1
192
+ end
193
+ @proxy.current.should == MultiDb::SlaveDatabase2
194
+ end
195
+
196
+ it '#with_master should be local to the thread' do
197
+ @proxy.current.should_not == @proxy.master
198
+ @proxy.with_master do
199
+ @proxy.current.should == @proxy.master
200
+ Thread.new do
201
+ @proxy.current.should_not == @proxy.master
202
+ @proxy.with_master do
203
+ @proxy.current.should == @proxy.master
204
+ end
205
+ @proxy.current.should_not == @proxy.master
206
+ end
207
+ @proxy.current.should == @proxy.master
208
+ end
209
+ @proxy.current.should_not == @proxy.master
210
+ end
211
+
212
+ it 'should switch to the next reader even whithin with_master-block in different threads' do
213
+ # Because of connection pooling in AR 2.2, the second thread will cause
214
+ # a new connection being created behind the scenes. We therefore just test
215
+ # that these connections are beting retrieved for the right databases here.
216
+ @proxy.master.should_not_receive(:retrieve_connection).and_return(@master)
217
+ MultiDb::SlaveDatabase1.should_receive(:retrieve_connection).twice.and_return(@slave1)
218
+ MultiDb::SlaveDatabase2.should_receive(:retrieve_connection).once.and_return(@slave2)
219
+ @proxy.with_master do
220
+ Thread.new do
221
+ 3.times { @proxy.select_one(@sql) }
222
+ end.join
223
+ end
224
+ end
225
+
226
+ end
227
+
228
+ end
229
+
@@ -0,0 +1,58 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+ require MULTI_DB_SPEC_DIR + '/../lib/multi_db/scheduler'
3
+
4
+ describe MultiDb::Scheduler do
5
+
6
+ before do
7
+ @items = [5, 7, 4, 8]
8
+ @scheduler = MultiDb::Scheduler.new(@items.clone)
9
+ end
10
+
11
+ it "should return items in a round robin fashion" do
12
+ first = @items.shift
13
+ @scheduler.current.should == first
14
+ @items.each do |item|
15
+ @scheduler.next.should == item
16
+ end
17
+ @scheduler.next.should == first
18
+ end
19
+
20
+ it 'should not return blacklisted items' do
21
+ @scheduler.blacklist!(4)
22
+ @items.size.times do
23
+ @scheduler.next.should_not == 4
24
+ end
25
+ end
26
+
27
+ it 'should raise NoMoreItems if all are blacklisted' do
28
+ @items.each do |item|
29
+ @scheduler.blacklist!(item)
30
+ end
31
+ lambda {
32
+ @scheduler.next
33
+ }.should raise_error(MultiDb::Scheduler::NoMoreItems)
34
+ end
35
+
36
+ it 'should unblacklist items automatically' do
37
+ @scheduler = MultiDb::Scheduler.new(@items.clone, 1.second)
38
+ @scheduler.blacklist!(7)
39
+ sleep(1)
40
+ @scheduler.next.should == 7
41
+ end
42
+
43
+ describe '(accessed from multiple threads)' do
44
+
45
+ it '#current and #next should return the same item for the same thread' do
46
+ @scheduler.current.should == 5
47
+ @scheduler.next.should == 7
48
+ Thread.new do
49
+ @scheduler.current.should == 5
50
+ @scheduler.next.should == 7
51
+ end.join
52
+ @scheduler.next.should == 4
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+ gem 'activerecord', '2.2.2'
3
+ %w[tlattr_accessors active_record yaml erb spec].each {|lib| require lib}
4
+
5
+ RAILS_ENV = ENV['RAILS_ENV'] = 'test'
6
+
7
+ MULTI_DB_SPEC_DIR = File.dirname(__FILE__)
8
+ MULTI_DB_SPEC_CONFIG = YAML::load(File.open(MULTI_DB_SPEC_DIR + '/config/database.yml'))
9
+
10
+ ActiveRecord::Base.logger = Logger.new(MULTI_DB_SPEC_DIR + "/debug.log")
11
+ ActiveRecord::Base.configurations = MULTI_DB_SPEC_CONFIG
metadata ADDED
@@ -0,0 +1,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: glebpom-multi_db
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.2
5
+ platform: ruby
6
+ authors:
7
+ - "Maximilian Sch\xC3\xB6fmann"
8
+ - Gleb Pomykalov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2009-09-22 00:00:00 -07:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: activerecord
18
+ type: :runtime
19
+ version_requirement:
20
+ version_requirements: !ruby/object:Gem::Requirement
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: 2.1.0
25
+ version:
26
+ - !ruby/object:Gem::Dependency
27
+ name: schoefmax-tlattr_accessors
28
+ type: :runtime
29
+ version_requirement:
30
+ version_requirements: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - ">="
33
+ - !ruby/object:Gem::Version
34
+ version: 0.0.3
35
+ version:
36
+ description: Connection proxy for ActiveRecord for single master / multiple slave database deployments
37
+ email: gleb.pomykalov@qik.com
38
+ executables: []
39
+
40
+ extensions: []
41
+
42
+ extra_rdoc_files:
43
+ - LICENSE
44
+ - README.rdoc
45
+ files:
46
+ - lib/multi_db.rb
47
+ - lib/multi_db/active_record_extensions.rb
48
+ - lib/multi_db/connection_proxy.rb
49
+ - lib/multi_db/observer_extensions.rb
50
+ - lib/multi_db/query_cache_compat.rb
51
+ - lib/multi_db/scheduler.rb
52
+ - LICENSE
53
+ - README.rdoc
54
+ - spec/config/database.yml
55
+ - spec/connection_proxy_spec.rb
56
+ - spec/scheduler_spec.rb
57
+ - spec/spec_helper.rb
58
+ - multi_db.gemspec
59
+ has_rdoc: true
60
+ homepage: http://github.com/glebpom/multi_db
61
+ licenses:
62
+ post_install_message:
63
+ rdoc_options:
64
+ - --line-numbers
65
+ - --inline-source
66
+ - --title
67
+ - multi_db
68
+ - --main
69
+ - README.rdoc
70
+ require_paths:
71
+ - lib
72
+ required_ruby_version: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ version: "0"
77
+ version:
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: "1.2"
83
+ version:
84
+ requirements: []
85
+
86
+ rubyforge_project: multi_db
87
+ rubygems_version: 1.3.5
88
+ signing_key:
89
+ specification_version: 2
90
+ summary: Connection proxy for ActiveRecord for single master / multiple slave database deployments
91
+ test_files: []
92
+