activerecord-bogacs 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,259 @@
1
+ require 'thread_safe'
2
+ require 'active_record/bogacs/pool_support'
3
+
4
+ module ActiveRecord
5
+ module Bogacs
6
+ class FalsePool
7
+
8
+ include PoolSupport
9
+
10
+ include ThreadSafe::Util::CheapLockable
11
+ alias_method :synchronize, :cheap_synchronize
12
+
13
+ attr_accessor :automatic_reconnect
14
+
15
+ attr_reader :size, :spec
16
+
17
+ def initialize(spec)
18
+ @connected = nil
19
+
20
+ @spec = spec
21
+ @size = nil
22
+ #@automatic_reconnect = true
23
+
24
+ @reserved_connections = ThreadSafe::Cache.new #:initial_capacity => @size
25
+ end
26
+
27
+ # @private replacement for attr_reader :connections
28
+ def connections; @reserved_connections.values end
29
+
30
+ # @private attr_reader :reaper
31
+ def reaper; end
32
+
33
+ # @private
34
+ def checkout_timeout; end
35
+
36
+ # Retrieve the connection associated with the current thread, or call
37
+ # #checkout to obtain one if necessary.
38
+ #
39
+ # #connection can be called any number of times; the connection is
40
+ # held in a hash keyed by the thread id.
41
+ def connection
42
+ @reserved_connections[current_connection_id] ||= checkout
43
+ end
44
+
45
+ # Is there an open connection that is being used for the current thread?
46
+ def active_connection?
47
+ conn = @reserved_connections[current_connection_id]
48
+ conn ? conn.in_use? : false
49
+ end
50
+
51
+ # Signal that the thread is finished with the current connection.
52
+ # #release_connection releases the connection-thread association
53
+ # and returns the connection to the pool.
54
+ def release_connection(with_id = current_connection_id)
55
+ conn = @reserved_connections.delete(with_id)
56
+ checkin conn if conn
57
+ end
58
+
59
+ # If a connection already exists yield it to the block. If no connection
60
+ # exists checkout a connection, yield it to the block, and checkin the
61
+ # connection when finished.
62
+ def with_connection
63
+ connection_id = current_connection_id
64
+ fresh_connection = true unless active_connection?
65
+ yield connection
66
+ ensure
67
+ release_connection(connection_id) if fresh_connection
68
+ end
69
+
70
+ # Returns true if a connection has already been opened.
71
+ def connected?; @connected end
72
+
73
+ # Disconnects all connections in the pool, and clears the pool.
74
+ def disconnect!
75
+ synchronize do
76
+ @connected = false
77
+
78
+ connections = @reserved_connections.values
79
+ @reserved_connections.clear
80
+
81
+ connections.each do |conn|
82
+ checkin conn
83
+ conn.disconnect!
84
+ end
85
+ end
86
+ end
87
+
88
+ # Clears the cache which maps classes.
89
+ def clear_reloadable_connections!
90
+ synchronize do
91
+ @connected = false
92
+
93
+ connections = @reserved_connections.values
94
+ @reserved_connections.clear
95
+
96
+ connections.each do |conn|
97
+ checkin conn
98
+ conn.disconnect! if conn.requires_reloading?
99
+ end
100
+ end
101
+ end
102
+
103
+ # Verify active connections and remove and disconnect connections
104
+ # associated with stale threads.
105
+ # @private AR 3.2 compatibility
106
+ def verify_active_connections!
107
+ synchronize do
108
+ clear_stale_cached_connections!
109
+ connections = @reserved_connections.values
110
+ connections.each do |connection|
111
+ connection.verify!
112
+ end
113
+ end
114
+ end if ActiveRecord::VERSION::MAJOR < 4
115
+
116
+ # Return any checked-out connections back to the pool by threads that
117
+ # are no longer alive.
118
+ # @private AR 3.2 compatibility
119
+ def clear_stale_cached_connections!
120
+ keys = Thread.list.find_all { |t| t.alive? }.map(&:object_id)
121
+ keys = @reserved_connections.keys - keys
122
+ keys.each do |key|
123
+ if conn = @reserved_connections[key]
124
+ checkin conn, false # no release
125
+ @reserved_connections.delete(key)
126
+ end
127
+ end
128
+ end if ActiveRecord::VERSION::MAJOR < 4
129
+
130
+ # Check-out a database connection from the pool, indicating that you want
131
+ # to use it. You should call #checkin when you no longer need this.
132
+ #
133
+ # This is done by either returning and leasing existing connection, or by
134
+ # creating a new connection and leasing it.
135
+ #
136
+ # If all connections are leased and the pool is at capacity (meaning the
137
+ # number of currently leased connections is greater than or equal to the
138
+ # size limit set), an ActiveRecord::ConnectionTimeoutError exception will be raised.
139
+ #
140
+ # Returns: an AbstractAdapter object.
141
+ #
142
+ # Raises:
143
+ # - ConnectionTimeoutError: no connection can be obtained from the pool.
144
+ def checkout
145
+ #synchronize do
146
+ conn = checkout_new_connection # acquire_connection
147
+ conn.lease
148
+ conn.run_callbacks(:checkout) { conn.verify! } # checkout_and_verify(conn)
149
+ conn
150
+ #end
151
+ end
152
+
153
+ # Check-in a database connection back into the pool, indicating that you
154
+ # no longer need this connection.
155
+ #
156
+ # +conn+: an AbstractAdapter object, which was obtained by earlier by
157
+ # calling +checkout+ on this pool.
158
+ def checkin(conn, do_release = true)
159
+ release(conn) if do_release
160
+ #synchronize do
161
+ conn.run_callbacks(:checkin) { conn.expire }
162
+ #release conn
163
+ #@available.add conn
164
+ #end
165
+ end
166
+
167
+ # Remove a connection from the connection pool. The connection will
168
+ # remain open and active but will no longer be managed by this pool.
169
+ def remove(conn)
170
+ release(conn)
171
+ end
172
+
173
+ # @private
174
+ def reap
175
+ # we do not really manage the connection pool - nothing to do ...
176
+ end
177
+
178
+ private
179
+
180
+ # Acquire a connection by one of 1) immediately removing one
181
+ # from the queue of available connections, 2) creating a new
182
+ # connection if the pool is not at capacity, 3) waiting on the
183
+ # queue for a connection to become available.
184
+ #
185
+ # Raises:
186
+ # - ConnectionTimeoutError if a connection could not be acquired
187
+ def acquire_connection
188
+ # underlying pool will poll and block if "empty" (all checked-out)
189
+ #if conn = @available.poll
190
+ #conn
191
+ #elsif @connections.size < @size
192
+ #checkout_new_connection
193
+ #else
194
+ #reap
195
+ #@available.poll(@checkout_timeout)
196
+ #end
197
+ checkout_new_connection
198
+ end
199
+
200
+ def release(conn, owner = nil)
201
+ thread_id = owner.object_id unless owner.nil?
202
+
203
+ thread_id ||=
204
+ if @reserved_connections[conn_id = current_connection_id] == conn
205
+ conn_id
206
+ else
207
+ connections = @reserved_connections
208
+ connections.keys.find { |k| connections[k] == conn }
209
+ end
210
+
211
+ @reserved_connections.delete thread_id if thread_id
212
+ end
213
+
214
+ def checkout_new_connection
215
+ # NOTE: automatic reconnect seems to make no sense for us!
216
+ #raise ConnectionNotEstablished unless @automatic_reconnect
217
+
218
+ begin
219
+ conn = new_connection
220
+ rescue ConnectionTimeoutError => e
221
+ raise e
222
+ rescue => e
223
+ raise ConnectionTimeoutError, e.message if _timeout_error?(e)
224
+ raise e
225
+ end
226
+ conn.pool = self
227
+ synchronize { @connected = true } if @connected != true
228
+ conn
229
+ end
230
+
231
+ #def checkout_and_verify(conn)
232
+ # conn.run_callbacks(:checkout) { conn.verify! }
233
+ # conn
234
+ #end
235
+
236
+ # sample on JRuby + Tomcat JDBC :
237
+ # ActiveRecord::JDBCError(<The driver encountered an unknown error:
238
+ # org.apache.tomcat.jdbc.pool.PoolExhaustedException:
239
+ # [main] Timeout: Pool empty. Unable to fetch a connection in 2 seconds,
240
+ # none available[size:10; busy:10; idle:0; lastwait:2500].>
241
+ # )
242
+
243
+ def _timeout_error?(error)
244
+ error.inspect =~ /timeout/i
245
+ end
246
+
247
+ # def _timeout_error?(error); end # TODO: not sure what to do on MRI and friends
248
+ #
249
+ # def _timeout_error?(error)
250
+ # if error.is_a?(JDBCError)
251
+ # if sql_exception = error.sql_exception
252
+ # return true if sql_exception.to_s =~ /timeout/i
253
+ # end
254
+ # end
255
+ # end if defined? ArJdbc
256
+
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,21 @@
1
+ #require 'thread_safe'
2
+
3
+ module ActiveRecord
4
+ module Bogacs
5
+ module PoolSupport
6
+
7
+ def self.included(base)
8
+ #base.send :include, ThreadSafe::Util::CheapLockable
9
+ end
10
+
11
+ def new_connection
12
+ Base.send(spec.adapter_method, spec.config)
13
+ end
14
+
15
+ def current_connection_id
16
+ Base.connection_id ||= Thread.current.object_id # TODO
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,255 @@
1
+ require 'active_record/connection_adapters/abstract/connection_pool'
2
+ require 'thread'
3
+ require 'thread_safe'
4
+ require 'atomic'
5
+
6
+ # NOTE: needs explicit configuration - before connection gets established e.g.
7
+ #
8
+ # pool_class = ActiveRecord::ConnectionAdapters::ShareableConnectionPool
9
+ # ActiveRecord::ConnectionAdapters::ConnectionHandler.connection_pool_class = pool_class
10
+ #
11
+ module ActiveRecord
12
+ module Bogacs
13
+ class ShareablePool < ConnectionAdapters::ConnectionPool # TODO do not override?!
14
+ include ThreadSafe::Util::CheapLockable
15
+
16
+ DEFAULT_SHARED_POOL = 0.25 # only allow 25% of the pool size to be shared
17
+ MAX_THREAD_SHARING = 5 # not really a strict limit but should hold
18
+
19
+ attr_reader :shared_size
20
+
21
+ # @override
22
+ def initialize(spec)
23
+ super(spec) # ConnectionPool#initialize
24
+ shared_size = spec.config[:shared_pool]
25
+ shared_size = shared_size ? shared_size.to_f : DEFAULT_SHARED_POOL
26
+ # size 0.0 - 1.0 assumes percentage of the pool size
27
+ shared_size = ( @size * shared_size ).round if shared_size <= 1.0
28
+ @shared_size = shared_size.to_i
29
+ @shared_connections = ThreadSafe::Cache.new # :initial_capacity => @shared_size, :concurrency_level => 20
30
+ end
31
+
32
+ # @override
33
+ def connection
34
+ # TODO we assume here a single pool - multiple pool support not implemented!
35
+ Thread.current[:shared_pool_connection] || begin # super - simplified :
36
+ super # @reserved_connections.compute_if_absent(current_connection_id) { checkout }
37
+ end
38
+ end
39
+
40
+ # @override
41
+ def active_connection?
42
+ if shared_conn = Thread.current[:shared_pool_connection]
43
+ return shared_conn.in_use?
44
+ end
45
+ super_active_connection? current_connection_id
46
+ end
47
+
48
+ # @override called from ConnectionManagement middle-ware (when finished)
49
+ def release_connection(with_id = current_connection_id)
50
+ if reserved_conn = @reserved_connections[with_id]
51
+ if shared_count = @shared_connections[reserved_conn]
52
+ cheap_synchronize do # lock due #get_shared_connection ... not needed ?!
53
+ # NOTE: the other option is to not care about shared here at all ...
54
+ if shared_count.get == 0 # releasing a shared connection
55
+ release_shared_connection(reserved_conn)
56
+ #else return false
57
+ end
58
+ end
59
+ else # check back-in non-shared connections
60
+ checkin reserved_conn # (what super does)
61
+ end
62
+ end
63
+ end
64
+
65
+ # @override
66
+ def disconnect!
67
+ cheap_synchronize { @shared_connections.clear; super }
68
+ end
69
+
70
+ # @override
71
+ def clear_reloadable_connections!
72
+ cheap_synchronize { @shared_connections.clear; super }
73
+ end
74
+
75
+ # @override
76
+ # @note called from #reap thus the pool should work with reaper
77
+ def remove(conn)
78
+ cheap_synchronize { @shared_connections.delete(conn); super }
79
+ end
80
+
81
+ # # Return any checked-out connections back to the pool by threads that
82
+ # # are no longer alive.
83
+ # # @private AR 3.2 compatibility
84
+ # def clear_stale_cached_connections!
85
+ # keys = Thread.list.find_all { |t| t.alive? }.map(&:object_id)
86
+ # keys = @reserved_connections.keys - keys
87
+ # keys.each do |key|
88
+ # release_connection(key)
89
+ # @reserved_connections.delete(key)
90
+ # end
91
+ # end if ActiveRecord::VERSION::MAJOR < 4
92
+
93
+ # TODO take care of explicit connection.close (`pool.checkin self`) ?
94
+
95
+ # Custom API :
96
+
97
+ def release_shared_connection(connection)
98
+ if connection == Thread.current[:shared_pool_connection]
99
+ Thread.current[:shared_pool_connection] = nil
100
+ end
101
+
102
+ @shared_connections.delete(connection)
103
+ checkin connection
104
+ end
105
+
106
+ def with_shared_connection
107
+ # with_shared_connection call nested in the same thread
108
+ if connection = Thread.current[:shared_pool_connection]
109
+ emulated_checkout(connection)
110
+ return yield connection
111
+ end
112
+
113
+ start = Time.now if DEBUG
114
+ begin
115
+ connection_id = current_connection_id
116
+ # if there's a 'regular' connection on the thread use it as super
117
+ if super_active_connection?(connection_id) # for current thread
118
+ connection = self.connection # do not mark as shared
119
+ DEBUG && debug("with_shared_conn 10 got active = #{connection.to_s}")
120
+ # otherwise if we have a shared connection - use that one :
121
+ elsif connection = get_shared_connection
122
+ emulated_checkout(connection); shared = true
123
+ DEBUG && debug("with_shared_conn 20 got shared = #{connection.to_s}")
124
+ else
125
+ cheap_synchronize do
126
+ # check shared again as/if threads end up sync-ing up here :
127
+ if connection = get_shared_connection
128
+ emulated_checkout(connection)
129
+ DEBUG && debug("with_shared_conn 21 got shared = #{connection.to_s}")
130
+ end # here we acquire but a connection from the pool
131
+ # TODO the bottle-neck for concurrency doing sync { checkout } :
132
+ unless connection # here we acquire a connection from the pool
133
+ connection = self.checkout # might block if pool fully used
134
+ add_shared_connection(connection)
135
+ DEBUG && debug("with_shared_conn 30 acq shared = #{connection.to_s}")
136
+ end
137
+ end
138
+ shared = true
139
+ end
140
+
141
+ Thread.current[:shared_pool_connection] = connection if shared
142
+
143
+ DEBUG && debug("with_shared_conn obtaining a connection took #{(Time.now - start) * 1000}ms")
144
+ yield connection
145
+ ensure
146
+ Thread.current[:shared_pool_connection] = nil # if shared
147
+ rem_shared_connection(connection) if shared
148
+ end
149
+ end
150
+
151
+ private
152
+
153
+ def super_active_connection?(connection_id = current_connection_id)
154
+ (@reserved_connections.get(connection_id) || ( return false )).in_use?
155
+ end
156
+
157
+ def super_active_connection?(connection_id = current_connection_id)
158
+ synchronize do
159
+ (@reserved_connections[connection_id] || ( return false )).in_use?
160
+ end
161
+ end if ActiveRecord::VERSION::MAJOR < 4
162
+
163
+ def acquire_connection_no_wait?
164
+ synchronize do
165
+ @available.send(:can_remove_no_wait?) || @connections.size < @size
166
+ end
167
+ end
168
+
169
+ # get a (shared) connection that is least shared among threads (or nil)
170
+ # nil gets returned if it's 'better' to checkout a new one to be shared
171
+ # ... to better utilize shared connection reuse among multiple threads
172
+ def get_shared_connection # (lock = nil)
173
+ least_count = MAX_THREAD_SHARING + 1; least_shared = nil
174
+ shared_connections_size = @shared_connections.size
175
+
176
+ @shared_connections.each_pair do |connection, shared_count|
177
+ next if shared_count.get >= MAX_THREAD_SHARING
178
+ if ( shared_count = shared_count.get ) < least_count
179
+ DEBUG && debug(" get_shared_conn loop : #{connection.to_s} shared #{shared_count}-time(s)")
180
+ # ! DO NOT return connection if shared_count == 0
181
+ least_count = shared_count; least_shared = connection
182
+ end
183
+ end
184
+
185
+ if least_count > 0
186
+ if shared_connections_size < @shared_connections.size
187
+ DEBUG && debug(" get_shared_conn retry (shared connection added)")
188
+ return get_shared_connection # someone else added something re-try
189
+ end
190
+ if ( @shared_connections.size < @shared_size ) && acquire_connection_no_wait?
191
+ DEBUG && debug(" get_shared_conn return none - acquire from pool")
192
+ return nil # we should rather 'get' a new shared one from the pool
193
+ end
194
+ end
195
+
196
+ # we did as much as could without a lock - now sync due possible release
197
+ cheap_synchronize do # TODO although this likely might be avoided ...
198
+ # should try again if possibly the same connection got released :
199
+ unless least_count = @shared_connections[least_shared]
200
+ DEBUG && debug(" get_shared_conn retry (connection got released)")
201
+ return get_shared_connection
202
+ end
203
+ least_count.update { |v| v + 1 }
204
+ end if least_shared
205
+
206
+ DEBUG && debug(" get_shared_conn least shared = #{least_shared.to_s}")
207
+ least_shared # might be nil in that case we'll likely wait (as super)
208
+ end
209
+
210
+ def add_shared_connection(connection)
211
+ @shared_connections[connection] = Atomic.new(1)
212
+ end
213
+
214
+ def rem_shared_connection(connection)
215
+ if shared_count = @shared_connections[connection]
216
+ # shared_count.update { |v| v - 1 } # NOTE: likely fine without lock!
217
+ cheap_synchronize do # give it back to the pool
218
+ shared_count.update { |v| v - 1 } # might give it back if :
219
+ release_shared_connection(connection) if shared_count.get == 0
220
+ end
221
+ end
222
+ end
223
+
224
+ def emulated_checkin(connection)
225
+ # NOTE: not sure we'd like to run `run_callbacks :checkin {}` here ...
226
+ connection.expire
227
+ end
228
+
229
+ def emulated_checkout(connection)
230
+ # NOTE: not sure we'd like to run `run_callbacks :checkout {}` here ...
231
+ connection.lease; # connection.verify! auto-reconnect should do this
232
+ end
233
+
234
+ DEBUG = begin
235
+ debug = ENV['DB_POOL_DEBUG'].to_s
236
+ if debug.to_s == 'false' then false
237
+ elsif ! debug.empty?
238
+ log_dev = case debug
239
+ when 'STDOUT', 'stdout' then STDOUT
240
+ when 'STDERR', 'stderr' then STDERR
241
+ when 'true' then ActiveRecord::Base.logger
242
+ else File.expand_path(debug)
243
+ end
244
+ require 'logger'; Logger.new log_dev
245
+ else nil
246
+ end
247
+ end
248
+
249
+ private
250
+
251
+ def debug(msg); DEBUG.debug msg end
252
+
253
+ end
254
+ end
255
+ end