activerecord-bogacs 0.1.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,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