seamless_database_pool 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.
@@ -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