seamless_database_pool 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,108 @@
1
+ # This module allows setting the read pool connection type. Generally you will use one of
2
+ #
3
+ # - use_random_read_connection
4
+ # - use_persistent_read_connection
5
+ # - use_master_connection
6
+ #
7
+ # Each of these methods can take an optional block. If they are called with a block, they
8
+ # will set the read connection type only within the block. Otherwise they will set the default
9
+ # read connection type. If none is ever called, the read connection type will be :master.
10
+
11
+ module SeamlessDatabasePool
12
+
13
+ READ_CONNECTION_METHODS = [:master, :persistent, :random]
14
+
15
+ # Call this method to use a random connection from the read pool for every select statement.
16
+ # This method is good if your replication is very fast. Otherwise there is a chance you could
17
+ # get inconsistent results from one request to the next. This can result in mysterious failures
18
+ # if your code selects a value in one statement and then uses in another statement. You can wind
19
+ # up trying to use a value from one server that hasn't been replicated to another one yet.
20
+ # This method is best if you have few processes which generate a lot of queries and you have
21
+ # fast replication.
22
+ def self.use_random_read_connection
23
+ if block_given?
24
+ set_read_only_connection_type(:random){yield}
25
+ else
26
+ Thread.current[:read_only_connection] = :random
27
+ end
28
+ end
29
+
30
+ # Call this method to pick a random connection from the read pool and use it for all subsequent
31
+ # select statements. This provides consistency from one select statement to the next. This
32
+ # method should always be called with a block otherwise you can end up with an imbalanced read
33
+ # pool. This method is best if you have lots of processes which have a relatively few select
34
+ # statements or a slow replication mechanism. Generally this is the best method to use for web
35
+ # applications.
36
+ def self.use_persistent_read_connection
37
+ if block_given?
38
+ set_read_only_connection_type(:persistent){yield}
39
+ else
40
+ Thread.current[:read_only_connection] = {}
41
+ end
42
+ end
43
+
44
+ # Call this method to use the master connection for all subsequent select statements. This
45
+ # method is most useful when you are doing lots of updates since it guarantees consistency
46
+ # if you do a select immediately after an update or insert.
47
+ #
48
+ # The master connection will also be used for selects inside any transaction blocks. It will
49
+ # also be used if you pass :readonly => false to any ActiveRecord.find method.
50
+ def self.use_master_connection
51
+ if block_given?
52
+ set_read_only_connection_type(:master){yield}
53
+ else
54
+ Thread.current[:read_only_connection] = :master
55
+ end
56
+ end
57
+
58
+ # Set the read only connection type to either :master, :random, or :persistent.
59
+ def self.set_read_only_connection_type (connection_type)
60
+ saved_connection = Thread.current[:read_only_connection]
61
+ retval = nil
62
+ begin
63
+ connection_type = {} if connection_type == :persistent
64
+ Thread.current[:read_only_connection] = connection_type
65
+ retval = yield if block_given?
66
+ ensure
67
+ Thread.current[:read_only_connection] = saved_connection
68
+ end
69
+ return retval
70
+ end
71
+
72
+ # Get the read only connection type currently in use. Will be one of :master, :random, or :persistent.
73
+ def self.read_only_connection_type (default = :master)
74
+ connection_type = Thread.current[:read_only_connection] || default
75
+ connection_type = :persistent if connection_type.kind_of?(Hash)
76
+ return connection_type
77
+ end
78
+
79
+ # Get a read only connection from a connection pool.
80
+ def self.read_only_connection (pool_connection)
81
+ return pool_connection.master_connection if pool_connection.using_master_connection?
82
+ connection_type = Thread.current[:read_only_connection]
83
+
84
+ if connection_type.kind_of?(Hash)
85
+ connection = connection_type[pool_connection]
86
+ unless connection
87
+ connection = pool_connection.random_read_connection
88
+ connection_type[pool_connection] = connection
89
+ end
90
+ return connection
91
+ elsif connection_type == :random
92
+ return pool_connection.random_read_connection
93
+ else
94
+ return pool_connection.master_connection
95
+ end
96
+ end
97
+
98
+ # This method is provided as a way to change the persistent connection when it fails and a new one is substituted.
99
+ def self.set_persistent_read_connection (pool_connection, read_connection)
100
+ connection_type = Thread.current[:read_only_connection]
101
+ connection_type[pool_connection] = read_connection if connection_type.kind_of?(Hash)
102
+ end
103
+
104
+ def self.clear_read_only_connection
105
+ Thread.current[:read_only_connection] = nil
106
+ end
107
+
108
+ end
@@ -0,0 +1,22 @@
1
+ module SeamlessDatabasePool
2
+ # This module is mixed into connection adapters to allow the reconnect! method to timeout if the
3
+ # IP address becomes unreachable. The default timeout is 1 second, but you can change it by setting
4
+ # the connect_timeout parameter in the adapter configuration.
5
+ module ConnectTimeout
6
+ attr_accessor :connect_timeout
7
+
8
+ def self.included (base)
9
+ base.alias_method_chain :reconnect!, :connect_timeout
10
+ end
11
+
12
+ def reconnect_with_connect_timeout!
13
+ begin
14
+ timeout(connect_timeout || 1) do
15
+ reconnect_without_connect_timeout!
16
+ end
17
+ rescue TimeoutError
18
+ raise "reconnect timed out"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,63 @@
1
+ module SeamlessDatabasePool
2
+ # This module is included for testing. Mix it into each of your database pool connections
3
+ # and it will keep track of how often each connection calls update, insert, execute,
4
+ # or select.
5
+ module ConnectionStatistics
6
+ def self.included (base)
7
+ base.alias_method_chain(:update, :connection_statistics)
8
+ base.alias_method_chain(:insert, :connection_statistics)
9
+ base.alias_method_chain(:execute, :connection_statistics)
10
+ base.alias_method_chain(:select, :connection_statistics)
11
+ end
12
+
13
+ # Get the connection statistics
14
+ def connection_statistics
15
+ @connection_statistics ||= {}
16
+ end
17
+
18
+ def reset_connection_statistics
19
+ @connection_statistics = {}
20
+ end
21
+
22
+ def update_with_connection_statistics (sql, name = nil)
23
+ increment_connection_statistic(:update) do
24
+ update_without_connection_statistics(sql, name)
25
+ end
26
+ end
27
+
28
+ def insert_with_connection_statistics (sql, name = nil)
29
+ increment_connection_statistic(:insert) do
30
+ insert_without_connection_statistics(sql, name)
31
+ end
32
+ end
33
+
34
+ def execute_with_connection_statistics (sql, name = nil)
35
+ increment_connection_statistic(:execute) do
36
+ execute_without_connection_statistics(sql, name)
37
+ end
38
+ end
39
+
40
+ protected
41
+
42
+ def select_with_connection_statistics (sql, name = nil)
43
+ increment_connection_statistic(:select) do
44
+ select_without_connection_statistics(sql, name)
45
+ end
46
+ end
47
+
48
+ def increment_connection_statistic (method)
49
+ if @counting_pool_statistics
50
+ yield
51
+ else
52
+ begin
53
+ @counting_pool_statistics = true
54
+ stat = connection_statistics[method] || 0
55
+ @connection_statistics[method] = stat + 1
56
+ yield
57
+ ensure
58
+ @counting_pool_statistics = false
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,91 @@
1
+ module SeamlessDatabasePool
2
+ # This module provides a simple method of declaring which read pool connection type should
3
+ # be used for various ActionController actions. To use it, you must first mix it into
4
+ # you controller and then call use_database_pool to configure the connection types. Generally
5
+ # you should just do this in ApplicationController and call use_database_pool in your controllers
6
+ # when you need different connection types.
7
+ #
8
+ # Example:
9
+ #
10
+ # ApplicationController < ActionController::Base
11
+ # include SeamlessDatabasePool
12
+ # use_database_pool :all => :persistent, [:save, :delete] => :master
13
+ # ...
14
+
15
+ module ControllerFilter
16
+ def self.included (base)
17
+ unless base.respond_to?(:use_database_pool)
18
+ base.extend(ClassMethods)
19
+ base.class_eval do
20
+ alias_method_chain :perform_action, :seamless_database_pool
21
+ alias_method_chain :redirect_to, :seamless_database_pool
22
+ end
23
+ end
24
+ end
25
+
26
+ module ClassMethods
27
+
28
+ def seamless_database_pool_options
29
+ return @seamless_database_pool_options if @seamless_database_pool_options
30
+ @seamless_database_pool_options = superclass.seamless_database_pool_options.dup if superclass.respond_to?(:seamless_database_pool_options)
31
+ @seamless_database_pool_options ||= {}
32
+ end
33
+
34
+ # Call this method to set up the connection types that will be used for your actions.
35
+ # The configuration is given as a hash where the key is the action name and the value is
36
+ # the connection type (:master, :persistent, or :random). You can specify :all as the action
37
+ # to define a default connection type. You can also specify the action names in an array
38
+ # to easily map multiple actions to one connection type.
39
+ #
40
+ # The configuration is inherited from parent controller classes, so if you have default
41
+ # behavior, you should simply specify it in ApplicationController to have it available
42
+ # globally.
43
+ def use_database_pool (options)
44
+ remapped_options = seamless_database_pool_options
45
+ options.each_pair do |actions, connection_method|
46
+ unless SeamlessDatabasePool::READ_CONNECTION_METHODS.include?(connection_method)
47
+ raise "Invalid read pool method: #{connection_method}; should be one of #{SeamlessDatabasePool::READ_CONNECTION_METHODS.inspect}"
48
+ end
49
+ actions = [actions] unless actions.kind_of?(Array)
50
+ actions.each do |action|
51
+ remapped_options[action.to_sym] = connection_method
52
+ end
53
+ end
54
+ @seamless_database_pool_options = remapped_options
55
+ end
56
+ end
57
+
58
+ # Force the master connection to be used on the next request. This is very useful for the Post-Redirect pattern
59
+ # where you post a request to your save action and then redirect the user back to the edit action. By calling
60
+ # this method, you won't have to worry if the replication engine is slower than the redirect. Normally you
61
+ # won't need to call this method yourself as it is automatically called when you perform a redirect from within
62
+ # a master connection block. It is made available just in case you have special needs that don't quite fit
63
+ # into this module's default logic.
64
+ def use_master_db_connection_on_next_request
65
+ session[:next_request_db_connection] = :master if session
66
+ end
67
+
68
+ def seamless_database_pool_options
69
+ self.class.seamless_database_pool_options
70
+ end
71
+
72
+ def perform_action_with_seamless_database_pool
73
+ read_pool_method = session.delete(:next_request_db_connection) if session
74
+ read_pool_method ||= seamless_database_pool_options[action_name.to_sym] || seamless_database_pool_options[:all]
75
+ if read_pool_method
76
+ SeamlessDatabasePool.set_read_only_connection_type(read_pool_method) do
77
+ perform_action_without_seamless_database_pool
78
+ end
79
+ else
80
+ perform_action_without_seamless_database_pool
81
+ end
82
+ end
83
+
84
+ def redirect_to_with_seamless_database_pool (options = {}, response_status = {})
85
+ if SeamlessDatabasePool.read_only_connection_type(nil) == :master
86
+ use_master_db_connection_on_next_request
87
+ end
88
+ redirect_to_without_seamless_database_pool(options, response_status)
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,77 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+
3
+ describe "SeamlessDatabasePool::ConnectionStatistics" do
4
+
5
+ module SeamlessDatabasePool
6
+ class ConnectionStatisticsTester
7
+ def insert (sql, name = nil)
8
+ "INSERT #{sql}/#{name}"
9
+ end
10
+
11
+ def update (sql, name = nil)
12
+ execute(sql)
13
+ "UPDATE #{sql}/#{name}"
14
+ end
15
+
16
+ def execute (sql, name = nil)
17
+ "EXECUTE #{sql}/#{name}"
18
+ end
19
+
20
+ protected
21
+
22
+ def select (sql, name = nil)
23
+ "SELECT #{sql}/#{name}"
24
+ end
25
+
26
+ include ConnectionStatistics
27
+ end
28
+ end
29
+
30
+ it "should increment statistics on update" do
31
+ connection = SeamlessDatabasePool::ConnectionStatisticsTester.new
32
+ connection.update('SQL', 'name').should == "UPDATE SQL/name"
33
+ connection.connection_statistics.should == {:update => 1}
34
+ connection.update('SQL 2').should == "UPDATE SQL 2/"
35
+ connection.connection_statistics.should == {:update => 2}
36
+ end
37
+
38
+ it "should increment statistics on insert" do
39
+ connection = SeamlessDatabasePool::ConnectionStatisticsTester.new
40
+ connection.insert('SQL', 'name').should == "INSERT SQL/name"
41
+ connection.connection_statistics.should == {:insert => 1}
42
+ connection.insert('SQL 2').should == "INSERT SQL 2/"
43
+ connection.connection_statistics.should == {:insert => 2}
44
+ end
45
+
46
+ it "should increment statistics on execute" do
47
+ connection = SeamlessDatabasePool::ConnectionStatisticsTester.new
48
+ connection.execute('SQL', 'name').should == "EXECUTE SQL/name"
49
+ connection.connection_statistics.should == {:execute => 1}
50
+ connection.execute('SQL 2').should == "EXECUTE SQL 2/"
51
+ connection.connection_statistics.should == {:execute => 2}
52
+ end
53
+
54
+ it "should increment statistics on select" do
55
+ connection = SeamlessDatabasePool::ConnectionStatisticsTester.new
56
+ connection.send(:select, 'SQL', 'name').should == "SELECT SQL/name"
57
+ connection.connection_statistics.should == {:select => 1}
58
+ connection.send(:select, 'SQL 2').should == "SELECT SQL 2/"
59
+ connection.connection_statistics.should == {:select => 2}
60
+ end
61
+
62
+ it "should increment counts only once within a block" do
63
+ connection = SeamlessDatabasePool::ConnectionStatisticsTester.new
64
+ connection.should_receive(:execute).with('SQL')
65
+ connection.update('SQL')
66
+ connection.connection_statistics.should == {:update => 1}
67
+ end
68
+
69
+ it "should be able to clear the statistics" do
70
+ connection = SeamlessDatabasePool::ConnectionStatisticsTester.new
71
+ connection.update('SQL')
72
+ connection.connection_statistics.should == {:update => 1}
73
+ connection.reset_connection_statistics
74
+ connection.connection_statistics.should == {}
75
+ end
76
+
77
+ end
@@ -0,0 +1,134 @@
1
+ require File.expand_path(File.join(File.dirname(__FILE__), 'spec_helper'))
2
+
3
+ describe "SeamlessDatabasePool::ControllerFilter" do
4
+
5
+ module SeamlessDatabasePool
6
+ class TestApplicationController
7
+ attr_reader :action_name, :session
8
+
9
+ def initialize(action, session = {})
10
+ @action_name = action
11
+ @session = session
12
+ end
13
+
14
+ def perform_action
15
+ send action_name
16
+ end
17
+
18
+ def redirect_to (options = {}, response_status = {})
19
+ options
20
+ end
21
+
22
+ def base_action
23
+ SeamlessDatabasePool.read_only_connection_type
24
+ end
25
+ end
26
+
27
+ class TestBaseController < TestApplicationController
28
+ include SeamlessDatabasePool::ControllerFilter
29
+
30
+ use_database_pool :read => :persistent
31
+
32
+ def read
33
+ SeamlessDatabasePool.read_only_connection_type
34
+ end
35
+
36
+ def other
37
+ SeamlessDatabasePool.read_only_connection_type
38
+ end
39
+ end
40
+
41
+ class TestOtherController < TestBaseController
42
+ use_database_pool :all => :random, [:edit, :save, :redirect_master_action] => :master
43
+
44
+ def edit
45
+ SeamlessDatabasePool.read_only_connection_type
46
+ end
47
+
48
+ def save
49
+ SeamlessDatabasePool.read_only_connection_type
50
+ end
51
+
52
+ def redirect_master_action
53
+ redirect_to(:action => :read)
54
+ end
55
+
56
+ def redirect_read_action
57
+ redirect_to(:action => :read)
58
+ end
59
+ end
60
+ end
61
+
62
+ it "should work with nothing set" do
63
+ controller = SeamlessDatabasePool::TestApplicationController.new('base_action')
64
+ controller.perform_action.should == :master
65
+ end
66
+
67
+ it "should allow setting a connection type for a single action" do
68
+ controller = SeamlessDatabasePool::TestBaseController.new('read')
69
+ controller.perform_action.should == :persistent
70
+ end
71
+
72
+ it "should allow setting a connection type for actions" do
73
+ controller = SeamlessDatabasePool::TestOtherController.new('edit')
74
+ controller.perform_action.should == :master
75
+ controller = SeamlessDatabasePool::TestOtherController.new('save')
76
+ controller.perform_action.should == :master
77
+ end
78
+
79
+ it "should allow setting a connection type for all actions" do
80
+ controller = SeamlessDatabasePool::TestOtherController.new('other')
81
+ controller.perform_action.should == :random
82
+ end
83
+
84
+ it "should inherit the superclass' options" do
85
+ controller = SeamlessDatabasePool::TestOtherController.new('read')
86
+ controller.perform_action.should == :persistent
87
+ end
88
+
89
+ it "should be able to force using the master connection on the next request" do
90
+ session = {}
91
+
92
+ # First request
93
+ controller = SeamlessDatabasePool::TestOtherController.new('read', session)
94
+ controller.perform_action.should == :persistent
95
+ controller.use_master_db_connection_on_next_request
96
+
97
+ # Second request
98
+ controller = SeamlessDatabasePool::TestOtherController.new('read', session)
99
+ controller.perform_action.should == :master
100
+
101
+ # Third request
102
+ controller = SeamlessDatabasePool::TestOtherController.new('read', session)
103
+ controller.perform_action.should == :persistent
104
+ end
105
+
106
+ it "should not break trying to force the master connection if sessions are not enabled" do
107
+ controller = SeamlessDatabasePool::TestOtherController.new('read', nil)
108
+ controller.perform_action.should == :persistent
109
+ controller.use_master_db_connection_on_next_request
110
+
111
+ # Second request
112
+ controller = SeamlessDatabasePool::TestOtherController.new('read', nil)
113
+ controller.perform_action.should == :persistent
114
+ end
115
+
116
+ it "should force the master connection on the next request for a redirect in master connection block" do
117
+ session = {}
118
+ controller = SeamlessDatabasePool::TestOtherController.new('redirect_master_action', session)
119
+ controller.perform_action.should == {:action => :read}
120
+
121
+ controller = SeamlessDatabasePool::TestOtherController.new('read', session)
122
+ controller.perform_action.should == :master
123
+ end
124
+
125
+ it "should not force the master connection on the next request for a redirect not in master connection block" do
126
+ session = {}
127
+ controller = SeamlessDatabasePool::TestOtherController.new('redirect_read_action', session)
128
+ controller.perform_action.should == {:action => :read}
129
+
130
+ controller = SeamlessDatabasePool::TestOtherController.new('read', session)
131
+ controller.perform_action.should == :persistent
132
+ end
133
+
134
+ end