activerecord-bogacs 0.4.1 → 0.5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b0aec44c1b08bd47f913dcce2eba41e1d30b1fa0
4
- data.tar.gz: a506eb318972490b316604732aded93e5079e5d7
3
+ metadata.gz: 3da20a4a7e44c77643b1cf11b949e5fb818d43ca
4
+ data.tar.gz: 4bd02da78f04b586079c01ef3d3429d13821337e
5
5
  SHA512:
6
- metadata.gz: dc20088b6faeba8de52781af835d3cc078017bfcb7139b14e96d9f503fc3019340bbbe5fc0ae1ce8a000fb42a620a1fc3fda114b2dbe057f36d5e974ed892b28
7
- data.tar.gz: 4600c5ed962dfb2682cc9f5911292442ac90951af19c3511d069b97175a5114f4c4f05de46bbfad25f5ed821c44388946b6336b84c4b636f08b498f57fae68b7
6
+ metadata.gz: bc86fe344a38859c20e2c41c4017a5d5e3d1c802a2879cc80376c8f9f286d7ec9496851217f98197869558edfb08aca4c37eab0c873a4b1538a6f2d1aadbd39b
7
+ data.tar.gz: 8de7842694c6ad26a9a904c0c8656dc5b1a6924771e3c8a106a53a917f82928a59bfec09123317c93813d89c9a009e1e99c392fa2549d0e6a23e71dc6c805a65
@@ -5,7 +5,7 @@ jdk:
5
5
  - oraclejdk7
6
6
  - oraclejdk8
7
7
  rvm:
8
- - jruby-1.7.22
8
+ - jruby-1.7.24
9
9
  #- 2.1.2
10
10
  before_install:
11
11
  - ((jruby -v | grep 1.8.7) && jruby --1.9 -S gem update --system 2.1.11) || true
@@ -29,35 +29,35 @@ matrix:
29
29
  #allow_failures:
30
30
  #- rvm: jruby-head
31
31
  exclude:
32
- - rvm: jruby-1.7.22
32
+ - rvm: jruby-1.7.24
33
33
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=mysql AR_VERSION="~> 4.1.9" HIKARI_VERSION="2.3.12"
34
34
  jdk: oraclejdk7
35
- - rvm: jruby-1.7.22
35
+ - rvm: jruby-1.7.24
36
36
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=postgresql AR_VERSION="~> 4.1.9" HIKARI_VERSION="2.3.2-java6"
37
37
  jdk: oraclejdk8
38
- - rvm: jruby-1.7.22
38
+ - rvm: jruby-1.7.24
39
39
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=mysql AR_VERSION="~> 4.1.13" HIKARI_VERSION="2.0.1-java6"
40
40
  jdk: oraclejdk8
41
- - rvm: jruby-1.7.22
41
+ - rvm: jruby-1.7.24
42
42
  env: JRUBY_OPTS="--1.8 $JRUBY_OPTS" AR_ADAPTER=mysql AR_VERSION="~> 3.2.18" HIKARI_VERSION="2.2.5-java6"
43
43
  jdk: oraclejdk8
44
44
  include:
45
- - rvm: jruby-9.0.1.0
45
+ - rvm: jruby-9.0.5.0
46
46
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=mysql AR_VERSION="~> 4.1.13" HIKARI_VERSION="2.2.5-java6"
47
47
  jdk: oraclejdk7
48
- - rvm: jruby-9.0.1.0
48
+ - rvm: jruby-9.1.2.0
49
49
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=mysql AR_VERSION="~> 3.2.18" HIKARI_VERSION="2.3.9"
50
50
  jdk: oraclejdk8
51
51
  # AR 4.2
52
52
  - rvm: jruby-1.7.22
53
53
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=mysql AR_VERSION="~> 4.1.13" HIKARI_VERSION="2.2.5-java6"
54
54
  jdk: oraclejdk7
55
- - rvm: jruby-9.0.1.0
55
+ - rvm: jruby-9.1.2.0
56
56
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=mysql AR_VERSION="~> 4.2.4" HIKARI_VERSION="2.3.12"
57
57
  jdk: oraclejdk8
58
58
  - rvm: jruby-1.7.22
59
59
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=postgresql AR_VERSION="~> 4.2.4" HIKARI_VERSION="2.2.5-java6"
60
60
  jdk: oraclejdk8
61
- - rvm: jruby-9.0.1.0
61
+ - rvm: jruby-9.0.5.0
62
62
  env: JRUBY_OPTS="$JRUBY_OPTS" AR_ADAPTER=postgresql AR_VERSION="~> 4.1.13" HIKARI_VERSION="2.3.12"
63
- jdk: oraclejdk7
63
+ jdk: oraclejdk7
data/Gemfile CHANGED
@@ -21,7 +21,7 @@ if RUBY_VERSION.index('1.8') == 0
21
21
  gem 'atomic', '1.1.16' # concurrent-ruby gem only for Ruby version >= 1.9.3
22
22
  gem 'thread_safe', '~> 0.3'
23
23
  else
24
- gem 'concurrent-ruby', '1.0.0.pre4', :require => nil
24
+ gem 'concurrent-ruby', '~> 1.0.0', :require => nil
25
25
  end
26
26
 
27
27
  platform :jruby do
@@ -9,6 +9,8 @@ module ActiveRecord
9
9
  autoload :DefaultPool, 'active_record/bogacs/default_pool'
10
10
  autoload :FalsePool, 'active_record/bogacs/false_pool'
11
11
  autoload :ShareablePool, 'active_record/bogacs/shareable_pool'
12
+ autoload :Reaper, 'active_record/bogacs/reaper'
13
+ autoload :Validator, 'active_record/bogacs/validator'
12
14
  end
13
15
  autoload :SharedConnection, 'active_record/shared_connection'
14
16
  end
@@ -51,7 +51,7 @@ module ActiveRecord
51
51
  # stdlib's doesn't support waiting with a timeout.
52
52
  # @private
53
53
  class Queue
54
- def initialize(lock = Monitor.new)
54
+ def initialize(lock)
55
55
  @lock = lock
56
56
  @cond = @lock.new_cond
57
57
  @num_waiting = 0
@@ -177,36 +177,13 @@ module ActiveRecord
177
177
  end
178
178
  end
179
179
 
180
- # Every +frequency+ seconds, the reaper will call +reap+ on +pool+.
181
- # A reaper instantiated with a nil frequency will never reap the
182
- # connection pool.
183
- #
184
- # Configure the frequency by setting "reaping_frequency" in your
185
- # database yaml file.
186
- class Reaper
187
- attr_reader :pool, :frequency
188
-
189
- def initialize(pool, frequency)
190
- @pool = pool
191
- @frequency = frequency
192
- end
193
-
194
- def run
195
- return unless frequency
196
- Thread.new(frequency, pool) { |t, p|
197
- while true
198
- sleep t
199
- p.reap
200
- end
201
- }
202
- end
203
- end
204
-
205
180
  include PoolSupport
206
181
  include MonitorMixin # TODO consider avoiding ?!
207
182
 
183
+ require 'active_record/bogacs/reaper.rb'
184
+
208
185
  attr_accessor :automatic_reconnect, :checkout_timeout
209
- attr_reader :spec, :connections, :size, :reaper
186
+ attr_reader :spec, :connections, :size, :reaper, :validator
210
187
  attr_reader :initial_size
211
188
 
212
189
  # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
@@ -248,6 +225,14 @@ module ActiveRecord
248
225
  initial_size = (@size * initial_size).to_i if initial_size <= 1.0
249
226
  # NOTE: warn on onitial_size > size !
250
227
  prefill_initial_connections if ( @initial_size = initial_size.to_i ) > 0
228
+
229
+ if frequency = spec.config[:validate_frequency]
230
+ require 'active_record/bogacs/validator' unless self.class.const_defined?(:Validator)
231
+ @validator = Validator.new self, frequency, spec.config[:validate_timeout]
232
+ if @validator.run && @reaping
233
+ logger && logger.info("pool: validator configured alongside with reaper")
234
+ end
235
+ end
251
236
  end
252
237
 
253
238
  # Retrieve the connection associated with the current thread, or call
@@ -438,6 +423,16 @@ module ActiveRecord
438
423
  end
439
424
  # NOTE: active? and reset! are >= AR 2.3
440
425
 
426
+ def reaper?; (@reaper ||= nil) && @reaper.frequency end
427
+ def reaping?; reaper? && @reaper.running? end
428
+
429
+ def validator?; (@validator ||= nil) && @validator.frequency end
430
+ def validating?; validator? && @validator.running? end
431
+
432
+ #@@logger = nil
433
+ def logger; ::ActiveRecord::Base.logger end
434
+ #def logger=(logger); @@logger = logger end
435
+
441
436
  private
442
437
 
443
438
  # Acquire a connection by one of 1) immediately removing one
@@ -490,10 +485,6 @@ module ActiveRecord
490
485
  conns
491
486
  end
492
487
 
493
- #@@logger = nil
494
- def logger; ::ActiveRecord::Base.logger end
495
- #def logger=(logger); @@logger = logger end
496
-
497
488
  end
498
489
 
499
490
  end
@@ -18,7 +18,7 @@ module ActiveRecord
18
18
 
19
19
  @spec = spec
20
20
  @size = nil
21
- #@automatic_reconnect = true
21
+ @automatic_reconnect = nil
22
22
 
23
23
  @reserved_connections = ThreadSafe::Map.new #:initial_capacity => @size
24
24
  end
@@ -105,10 +105,7 @@ module ActiveRecord
105
105
  def verify_active_connections!
106
106
  synchronize do
107
107
  clear_stale_cached_connections!
108
- connections = @reserved_connections.values
109
- connections.each do |connection|
110
- connection.verify!
111
- end
108
+ @reserved_connections.values.each(&:verify!)
112
109
  end
113
110
  end if ActiveRecord::VERSION::MAJOR < 4
114
111
 
@@ -232,18 +229,19 @@ module ActiveRecord
232
229
  # conn
233
230
  #end
234
231
 
232
+ TIMEOUT_ERROR = /timeout|timed.?out/i
233
+
235
234
  def timeout_error?(error)
236
235
  full_error = error.inspect
237
- # sample on JRuby + Tomcat JDBC :
236
+ # Tomcat JDBC :
238
237
  # ActiveRecord::JDBCError(<The driver encountered an unknown error:
239
238
  # org.apache.tomcat.jdbc.pool.PoolExhaustedException:
240
239
  # [main] Timeout: Pool empty. Unable to fetch a connection in 2 seconds,
241
240
  # none available[size:10; busy:10; idle:0; lastwait:2500].>
242
241
  # )
243
- return true if full_error =~ /timeout/i
244
242
  # C3P0 :
245
243
  # java.sql.SQLException: An attempt by a client to checkout a Connection has timed out.
246
- return true if full_error =~ /timed.?out/i
244
+ return true if full_error =~ TIMEOUT_ERROR
247
245
  # NOTE: not sure what to do on MRI and friends (C-pools not tested)
248
246
  false
249
247
  end
@@ -50,6 +50,10 @@ module ActiveRecord
50
50
  conn.expire
51
51
  end
52
52
  end
53
+ rescue => e
54
+ remove conn
55
+ conn.disconnect! rescue nil
56
+ raise e
53
57
  end
54
58
 
55
59
  def _run_checkout_callbacks(conn)
@@ -62,6 +66,10 @@ module ActiveRecord
62
66
  conn.verify!
63
67
  end
64
68
  end
69
+ rescue => e
70
+ remove conn
71
+ conn.disconnect! rescue nil
72
+ raise e
65
73
  end
66
74
 
67
75
  else
@@ -0,0 +1,86 @@
1
+ require 'thread'
2
+
3
+ module ActiveRecord
4
+ module Bogacs
5
+
6
+ # Every +frequency+ seconds, the reaper will "reap" a pool it belongs to.
7
+ # Reaping means detecting stale cached connections in the pool - those that
8
+ # were checked-out by a thread but never checked back in after the thread
9
+ # finished/died.
10
+ #
11
+ # @note This version is fail safe - raised errors won't stop the reaping process.
12
+ # Instead the thread will be restarted and reaping continues at the next tick.
13
+ #
14
+ # Configure the frequency by setting `:reaping_frequency` in your database yaml file.
15
+ #
16
+ class Reaper
17
+
18
+ attr_reader :pool, :frequency
19
+ attr_accessor :retry_error
20
+
21
+ # Reaper.new(self, spec.config[:reaping_frequency]).run
22
+ # @private
23
+ def initialize(pool, frequency)
24
+ @pool = pool;
25
+ if frequency
26
+ frequency = frequency.to_f
27
+ @frequency = frequency > 0.0 ? frequency : false
28
+ else
29
+ @frequency = nil
30
+ end
31
+ @retry_error = 1.5; @running = nil
32
+ end
33
+
34
+ def run
35
+ return unless frequency
36
+ @running = true; start
37
+ end
38
+
39
+ def start(delay = nil)
40
+ Thread.new { exec(delay) }
41
+ end
42
+
43
+ def running?; @running end
44
+
45
+ private
46
+
47
+ def exec(delay = nil)
48
+ sleep delay if delay
49
+ while true
50
+ begin
51
+ sleep frequency
52
+ pool.reap
53
+ rescue => e
54
+ log = logger
55
+ if retry_delay = @retry_error
56
+ log && log.warn("[reaper] reaping failed: #{e.inspect} restarting after #{retry_delay}s")
57
+ start retry_delay
58
+ else
59
+ log && log.warn("[reaper] reaping failed: #{e.inspect} stopping reaper")
60
+ @running = false
61
+ end
62
+ break
63
+ end
64
+ end
65
+ end
66
+
67
+ def set_thread_name(name)
68
+ if ( thread = Thread.current ).respond_to?(:name)
69
+ thread.name = name; return
70
+ end
71
+ if defined? JRUBY_VERSION
72
+ thread = JRuby.reference(thread).getNativeThread
73
+ thread.setName("#{name} #{thread.getName}")
74
+ else
75
+ thread[:name] = name
76
+ end
77
+ end
78
+
79
+ def logger
80
+ @logger ||= ( pool.respond_to?(:logger) ? pool.logger : nil ) rescue nil
81
+ end
82
+
83
+ end
84
+
85
+ end
86
+ end
@@ -0,0 +1,182 @@
1
+ begin
2
+ require 'concurrent/executors'
3
+ require 'concurrent/timer_task'
4
+ rescue LoadError => e
5
+ warn "activerecord-bogacs' validator feature needs gem 'concurrent-ruby', please install or add it to your Gemfile"
6
+ raise e
7
+ end
8
+
9
+ require 'active_record/connection_adapters/adapter_compat'
10
+ require 'active_record/bogacs/thread_safe'
11
+
12
+ module ActiveRecord
13
+ module Bogacs
14
+
15
+ # Every +frequency+ seconds, the _validator_ will perform connection validation
16
+ # on a pool it operates.
17
+ #
18
+ # @note Do not use a reaper with the validator as reaping (stale connection detection
19
+ # and removal) is part of the validation process and will only likely slow things down
20
+ # as all pool connection updates needs to be synchronized.
21
+ #
22
+ # Configure the frequency by setting `:validate_frequency` in your AR configuration.
23
+ #
24
+ # We recommend not setting values too low as that would drain the pool's performance
25
+ # under heavy concurrent connection retrieval. Connections are also validated upon
26
+ # checkout - the validator is intended to detect long idle pooled connections "ahead
27
+ # of time" instead of upon retrieval.
28
+ #
29
+ class Validator
30
+
31
+ attr_reader :pool, :frequency, :timeout
32
+
33
+ # Validator.new(self, spec.config[:validate_frequency]).run
34
+ # @private
35
+ def initialize(pool, frequency = 60, timeout = nil)
36
+ @pool = pool; PoolAdaptor.adapt! pool
37
+ if frequency # validate every 60s by default
38
+ frequency = frequency.to_f
39
+ @frequency = frequency > 0.0 ? frequency : false
40
+ else
41
+ @frequency = nil
42
+ end
43
+ if timeout
44
+ timeout = timeout.to_f
45
+ @timeout = timeout > 0.0 ? timeout : 0
46
+ else
47
+ @timeout = @frequency
48
+ end
49
+ @running = nil
50
+ end
51
+
52
+ def run
53
+ return unless frequency
54
+ @running = true; start
55
+ end
56
+
57
+ TimerTask = ::Concurrent::TimerTask
58
+ private_constant :TimerTask rescue nil
59
+
60
+ def start
61
+ TimerTask.new(:execution_interval => frequency, :timeout_interval => timeout) do
62
+ validate_connections
63
+ end
64
+ end
65
+
66
+ def running?; @running end
67
+
68
+ def validate
69
+ start = Time.now
70
+ conns = connections
71
+ logger && logger.debug("[validator] found #{conns.size} candidates to validate")
72
+ invalid = 0
73
+ conns.each { |connection| invalid += 1 if validate_connection(connection) == false }
74
+ logger && logger.info("[validator] validated pool in #{Time.now - start}s (removed #{invalid} connections from pool)")
75
+ invalid
76
+ end
77
+
78
+ private
79
+
80
+ def connections
81
+ connections = pool.connections.dup
82
+ connections.map! do |conn|
83
+ if conn
84
+ owner = conn.owner
85
+ if conn.in_use? # we'll do a pool.sync-ed check ...
86
+ if owner && ! owner.alive? # stale-conn (reaping)
87
+ pool.remove conn # remove is synchronized
88
+ conn.disconnect! rescue nil
89
+ nil
90
+ elsif ! owner # NOTE: this is likely a nasty bug
91
+ logger && logger.warn("[validator] found in-use connection ##{conn.object_id} without owner - removing from pool")
92
+ pool.remove_without_owner conn # synchronized
93
+ conn.disconnect! rescue nil
94
+ nil
95
+ else
96
+ nil # owner.alive? ... do not touch
97
+ end
98
+ else
99
+ conn # conn not in-use - candidate for validation
100
+ end
101
+ end
102
+ end
103
+ connections.compact
104
+ end
105
+
106
+ def validate_connection(conn)
107
+ return nil if conn.in_use?
108
+ pool.synchronize do # make sure it won't get checked-out while validating
109
+ return nil if conn.in_use?
110
+ # NOTE: active? is assumed to behave e.g. connection_alive_timeout used
111
+ # on AR-JDBC active? might return false as the JDBC connection is lazy
112
+ # ... but that is just fine!
113
+ begin
114
+ return true if conn.active? # validate the connection - ping the DB
115
+ rescue => e
116
+ logger && logger.info("[validator] connection ##{conn.object_id} failed to validate: #{e.inspect}")
117
+ end
118
+
119
+ # TODO support last_use - only validate if certain amount since use passed
120
+
121
+ logger && logger.debug("[validator] found non-active connection ##{conn.object_id} - removing from pool")
122
+ pool.remove_without_owner conn # not active - remove
123
+ conn.disconnect! rescue nil
124
+ return false
125
+ end
126
+ end
127
+
128
+ #def synchronize(&block); pool.synchronize(&block) end
129
+
130
+ def logger
131
+ @logger ||= ( pool.respond_to?(:logger) ? pool.logger : nil ) rescue nil
132
+ end
133
+
134
+ module PoolAdaptor
135
+
136
+ def self.adapt!(pool)
137
+ unless pool.class.include?(PoolAdaptor)
138
+ pool.class.send :include, PoolAdaptor
139
+ end
140
+
141
+ return if pool.respond_to?(:thread_cached_conns)
142
+
143
+ if pool.instance_variable_get :@reserved_connections
144
+ class << pool
145
+ attr_reader :reserved_connections
146
+ alias_method :thread_cached_conns, :reserved_connections
147
+ end
148
+ elsif pool.instance_variable_get :@thread_cached_conns
149
+ class << pool
150
+ attr_reader :thread_cached_conns
151
+ end
152
+ else
153
+ raise NotImplementedError, "could not adapt pool: #{pool}"
154
+ end
155
+ end
156
+
157
+ def cached_conn_owner_id(conn)
158
+ thread_cached_conns.keys.each do |owner_id|
159
+ if thread_cached_conns[ owner_id ] == conn
160
+ return owner_id
161
+ end
162
+ end
163
+ nil
164
+ end
165
+
166
+ def remove_without_owner(conn)
167
+ remove conn # release(conn, nil) owner.object_id should do fine
168
+ release_without_owner conn
169
+ end
170
+
171
+ def release_without_owner(conn)
172
+ if owner_id = cached_conn_owner_id(conn)
173
+ thread_cached_conns.delete owner_id; return true
174
+ end
175
+ end
176
+
177
+ end
178
+
179
+ end
180
+
181
+ end
182
+ end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Bogacs
3
- VERSION = '0.4.1'
3
+ VERSION = '0.5.0'
4
4
  end
5
5
  end
@@ -6,7 +6,43 @@ module ActiveRecord
6
6
 
7
7
  attr_accessor :pool unless method_defined? :pool
8
8
 
9
- unless method_defined? :owner
9
+ if method_defined? :owner # >= 4.2
10
+
11
+ attr_reader :last_use
12
+
13
+ if ActiveRecord::VERSION::MAJOR > 4
14
+
15
+ # @private added @last_use
16
+ def lease
17
+ if in_use?
18
+ msg = 'Cannot lease connection, '
19
+ if @owner == Thread.current
20
+ msg += 'it is already leased by the current thread.'
21
+ else
22
+ msg += "it is already in use by a different thread: #{@owner}. Current thread: #{Thread.current}."
23
+ end
24
+ raise ActiveRecordError, msg
25
+ end
26
+
27
+ @owner = Thread.current; @last_use = Time.now
28
+ end
29
+
30
+ else
31
+
32
+ # @private removed synchronization + added @last_use
33
+ def lease
34
+ if in_use?
35
+ if @owner == Thread.current
36
+ # NOTE: could do a warning if 4.2.x cases do not end up here ...
37
+ end
38
+ else
39
+ @owner = Thread.current; @last_use = Time.now
40
+ end
41
+ end
42
+
43
+ end
44
+
45
+ else
10
46
 
11
47
  attr_reader :owner
12
48
 
@@ -29,6 +29,35 @@ module ActiveRecord
29
29
  assert_equal 0, @pool.connections.size
30
30
  end
31
31
 
32
+ def test_connection_removed_connection_on_checkout_failure
33
+ connection = nil
34
+
35
+ Thread.new {
36
+ pool.with_connection do |conn|
37
+ connection = conn
38
+ end
39
+ }.join
40
+
41
+ assert connection
42
+ assert_equal 1, pool.connections.size
43
+
44
+ bad_connection = connection
45
+ def bad_connection.verify!
46
+ raise ThreadError, 'corrupted-connection'
47
+ end
48
+
49
+ begin
50
+ connection = pool.connection
51
+ rescue ThreadError
52
+ else; warn 'verify! error not raised'
53
+ end
54
+ assert_equal 0, pool.connections.size # gets removed from pool
55
+
56
+ connection = pool.connection
57
+ assert bad_connection != connection, 'bad connection returned on checkout'
58
+
59
+ end if ActiveRecord::VERSION::STRING > '4.2'
60
+
32
61
  def self.startup; puts "running with ActiveRecord: #{ActiveRecord::VERSION::STRING}" end
33
62
 
34
63
  end
@@ -0,0 +1,86 @@
1
+ require File.expand_path('../../test_helper', File.dirname(__FILE__))
2
+ require 'stringio'
3
+
4
+ module ActiveRecord
5
+ module Bogacs
6
+ class ReaperTest < Test::Unit::TestCase
7
+
8
+ def self.startup
9
+ super; require 'active_record/bogacs/reaper'
10
+ Reaper.const_set :Thread, AbortiveThread
11
+ end
12
+
13
+ def config; AR_CONFIG end
14
+
15
+ def setup
16
+ super
17
+ ActiveRecord::Base.establish_connection config.merge :reaping_frequency => 0.25
18
+ @pool = DefaultPool.new ActiveRecord::Base.connection_pool.spec
19
+ end
20
+
21
+ def teardown; @pool.disconnect! if (@pool ||= nil) end
22
+
23
+ def test_null_reaper
24
+ ActiveRecord::Base.establish_connection config.merge :reaping_frequency => false
25
+ pool = DefaultPool.new ActiveRecord::Base.connection_pool.spec
26
+
27
+ assert ! pool.reaper?
28
+ sleep 0.1
29
+ assert ! pool.reaping?
30
+ end
31
+
32
+ def test_parse_frequency
33
+ ActiveRecord::Base.establish_connection config.merge :reaping_frequency => '0'
34
+ pool = DefaultPool.new ActiveRecord::Base.connection_pool.spec
35
+
36
+ assert ! pool.reaper?
37
+ sleep 0.1
38
+ assert ! pool.reaping?
39
+
40
+ assert ! Reaper.new(pool, '').frequency
41
+ assert_equal 5, Reaper.new(pool, '5').frequency
42
+ assert_equal 5.5, Reaper.new(pool, '5.5').frequency
43
+ end
44
+
45
+ def test_reaper?
46
+ assert @pool.reaper?
47
+ sleep 0.1
48
+ assert @pool.reaping?
49
+ end
50
+
51
+ def test_reap_error_restart
52
+ logger = Logger.new str = StringIO.new
53
+ @pool.reaper.instance_variable_set :@logger, logger
54
+ def @pool.reap; raise RuntimeError, 'test_reap_error' end
55
+
56
+ assert @pool.reaper?
57
+ sleep 0.3
58
+ assert_true @pool.reaping?
59
+ assert_match /WARN.*reaping failed:.* test_reap_error.* restarting after/, str.string
60
+ end
61
+
62
+ def test_reap_error_stop
63
+ logger = Logger.new str = StringIO.new
64
+ @pool.reaper.instance_variable_set :@logger, logger
65
+ @pool.reaper.retry_error = false
66
+ def @pool.reap; raise 'test_reap_error2' end
67
+
68
+ assert @pool.reaper?
69
+ sleep 0.3
70
+ assert_false @pool.reaping?
71
+ assert_match /WARN.*reaping failed:.* test_reap_error2.* stopping/, str.string
72
+ end
73
+
74
+ private
75
+
76
+ class AbortiveThread < Thread
77
+
78
+ def self.new(*args, &block)
79
+ super.tap { |thread| thread.abort_on_exception = true }
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,248 @@
1
+ require File.expand_path('../../test_helper', File.dirname(__FILE__))
2
+ require 'stringio'
3
+
4
+ module ActiveRecord
5
+ module Bogacs
6
+ class ValidatorTest < Test::Unit::TestCase
7
+
8
+ def self.startup
9
+ ConnectionAdapters::ConnectionHandler.connection_pool_class = DefaultPool
10
+ end
11
+
12
+ def self.shutdown
13
+ ConnectionAdapters::ConnectionHandler.connection_pool_class = nil
14
+ end
15
+
16
+ def config; AR_CONFIG end
17
+
18
+ def teardown; @pool.disconnect! if (@pool ||= nil) end
19
+
20
+ def test_null_validator
21
+ pool = new_pool :validate_frequency => nil
22
+
23
+ assert ! pool.validator?
24
+ sleep 0.05
25
+ assert ! pool.validating?
26
+ end
27
+
28
+ def test_parse_frequency
29
+ pool = new_pool :validate_frequency => '0'
30
+
31
+ assert ! pool.validator?
32
+ sleep 0.05
33
+ assert ! pool.validating?
34
+
35
+ assert ! Validator.new(pool, '').frequency
36
+ assert_equal 50, Validator.new(pool, '50').frequency
37
+ assert_equal 5.5, Validator.new(pool, '5.5').frequency
38
+ end
39
+
40
+ def test_validator?
41
+ assert pool.validator?
42
+ sleep 0.1
43
+ assert pool.validating?
44
+ end
45
+
46
+ require 'concurrent/atomic/atomic_fixnum.rb'
47
+ AtomicFixnum = ::Concurrent::AtomicFixnum
48
+
49
+ require 'concurrent/atomic/semaphore.rb'
50
+ Semaphore = ::Concurrent::Semaphore
51
+
52
+ def test_selects_non_used_connections
53
+ assert_equal [], validator.send(:connections)
54
+
55
+ count = AtomicFixnum.new
56
+ semaphore = Semaphore.new(2); semaphore.drain_permits
57
+ Thread.new {
58
+ pool.with_connection { |conn| assert conn; count.increment; semaphore.acquire }
59
+ }
60
+ Thread.new {
61
+ pool.with_connection { |conn| assert conn; count.increment; semaphore.acquire }
62
+ }
63
+ while count.value < 2; sleep 0.01 end
64
+
65
+ released_conn = nil
66
+ Thread.new {
67
+ pool.with_connection { |conn| assert released_conn = conn }
68
+ }.join
69
+
70
+
71
+ assert_equal 3, pool.connections.size
72
+ assert_equal 1, validator.send(:connections).size
73
+ assert_equal [ released_conn ], validator.send(:connections)
74
+
75
+ semaphore.release 2
76
+ end
77
+
78
+ def test_auto_removes_stale_connection_from_pool_when_collecting_connections_to_validate
79
+ conn = connection
80
+
81
+ assert_equal [], validator.send(:connections)
82
+
83
+ count = AtomicFixnum.new
84
+ semaphore = Semaphore.new(2); semaphore.drain_permits
85
+ Thread.new {
86
+ pool.with_connection { |conn| assert conn; count.increment; semaphore.acquire }
87
+ }
88
+ stale_conn = nil
89
+ Thread.new {
90
+ assert stale_conn = pool.connection; count.increment; semaphore.acquire
91
+ }
92
+ while count.value < 2; sleep 0.01 end
93
+
94
+ returned_conn = nil
95
+ Thread.new {
96
+ pool.with_connection { |conn| assert returned_conn = conn }
97
+ }.join
98
+
99
+ assert_equal 4, pool.connections.size
100
+ validate_candidates = validator.send(:connections)
101
+ assert_equal [ returned_conn ], validate_candidates
102
+ assert_equal 4, pool.connections.size
103
+
104
+ semaphore.release(2); sleep 0.05
105
+
106
+ validate_candidates = validator.send(:connections)
107
+ assert_equal 2, validate_candidates.size
108
+ assert validate_candidates.include?(returned_conn)
109
+ assert ! validate_candidates.include?(stale_conn)
110
+ assert_equal 3, pool.connections.size
111
+ assert ! pool.connections.map(&:object_id).include?(stale_conn.object_id)
112
+ end
113
+
114
+ def test_validate_connection
115
+ conn = connection; pool.remove conn
116
+ conn.expire; assert ! conn.in_use?
117
+ # lazy on AR-JDBC :
118
+ conn.tables; assert conn.active?
119
+
120
+ def conn.active_called?; @_active_called ||= false end
121
+ def conn.active?; @_active_called = true; super end
122
+
123
+ result = validator.send :validate_connection, conn
124
+ assert_true result
125
+
126
+ assert conn.active_called?
127
+ end
128
+
129
+ def test_validate_connection_non_valid
130
+ conn = connection; pool.remove conn
131
+ conn.expire; assert ! conn.in_use?
132
+
133
+ def conn.active?; false end
134
+
135
+ result = validator.send :validate_connection, conn
136
+ assert_false result
137
+ end
138
+
139
+ def test_validate_connection_in_use
140
+ conn = connection
141
+ assert conn.in_use?
142
+ def conn.active?; raise 'active? should not be called for a used connection' end
143
+
144
+ result = validator.send :validate_connection, conn
145
+ assert_nil result
146
+ end
147
+
148
+ def test_validate_connection_removes_invalid_connection_from_pool
149
+ conn = connection
150
+ puts pool.connections.map(&:object_id).inspect
151
+ Thread.new { pool.with_connection { |conn| assert conn } }.join
152
+ puts pool.connections.map(&:object_id).inspect
153
+ assert_equal 2, pool.connections.size
154
+
155
+ conn.expire; assert ! conn.in_use?
156
+
157
+ def conn.active?; false end
158
+
159
+ result = validator.send :validate_connection, conn
160
+ assert_false result
161
+
162
+ assert_equal 1, pool.connections.size
163
+ assert ! pool.connections.include?(conn)
164
+ end
165
+
166
+ def test_validate_connection_considers_raising_connection_invalid
167
+ conn = connection; threads = []
168
+ threads << Thread.new { pool.with_connection { |conn| assert conn; thread_work(conn) } }
169
+ threads << Thread.new { pool.with_connection { |conn| assert conn; thread_work(conn) } }
170
+ threads.each(&:join)
171
+ assert_equal 3, pool.connections.size
172
+
173
+ conn.expire; assert ! conn.in_use?
174
+ assert pool.connections.include?(conn)
175
+
176
+ def conn.active?; raise 'a failing active? check' end
177
+ def conn.disconnect!; raise 'failing disconnect!' end
178
+
179
+ result = validator.send :validate_connection, conn
180
+ assert_false result
181
+
182
+ assert_equal 2, pool.connections.size
183
+ assert ! pool.connections.include?(conn)
184
+ end
185
+
186
+
187
+ def test_validate_returns_invalid_connection_count
188
+ conn = connection; threads = []; invalid_conns = []
189
+ threads << Thread.new { pool.with_connection { |conn| thread_work(conn, 0.05) } }
190
+ threads << Thread.new { pool.with_connection { |conn| thread_work(conn, 0.05); conn.expire; invalid_conns << conn } }
191
+ threads << Thread.new { pool.with_connection { |conn| thread_work(conn, 0.05) } }
192
+ threads << Thread.new { pool.with_connection { |conn| thread_work(conn, 0.05); conn.expire; invalid_conns << conn } }
193
+ threads.each(&:join)
194
+ assert_equal 5, pool.connections.size
195
+
196
+ invalid_conns.each { |conn| def conn.active?; false end }
197
+
198
+ result = validator.validate
199
+ assert_equal 2, result
200
+ assert_equal 3, pool.connections.size
201
+
202
+ conn.expire
203
+ result = validator.validate
204
+ assert_equal 0, result
205
+ assert_equal 3, pool.connections.size
206
+ end
207
+
208
+ private
209
+
210
+ def connection
211
+ pool; ActiveRecord::Base.connection
212
+ end
213
+
214
+ def validator; pool.validator end
215
+
216
+ def pool
217
+ # self.startup: connection_pool_class = DefaultPool
218
+ @pool ||= (establish_connection; Base.connection_pool)
219
+ end
220
+
221
+ DEFAULT_OPTS = { :size => 5, :validate_frequency => 1 }
222
+
223
+ def establish_connection(opts = DEFAULT_OPTS)
224
+ ActiveRecord::Base.establish_connection config.merge opts
225
+ end
226
+
227
+ def new_pool(opts = DEFAULT_OPTS)
228
+ establish_connection config.merge opts
229
+ DefaultPool.new Base.connection_pool.spec
230
+ end
231
+
232
+ def thread_work(conn, sleep = 0.1)
233
+ Thread.current.abort_on_exception = true
234
+ conn.tables; sleep(sleep) if sleep
235
+ end
236
+
237
+ class TimerTaskStub
238
+
239
+ # :execution_interval => frequency, :timeout_interval => timeout
240
+ def self.new(opts, &block)
241
+ raise 'noop'
242
+ end
243
+
244
+ end
245
+
246
+ end
247
+ end
248
+ end
@@ -20,7 +20,8 @@ module ActiveRecord
20
20
  connection.close
21
21
  assert ! connection.in_use?
22
22
 
23
- assert pool.connection.in_use?
23
+ assert_equal connection, pool.connection
24
+ # assert pool.connection.in_use?
24
25
  end
25
26
 
26
27
  def test_released_connection_moves_between_threads
@@ -35,9 +35,9 @@ ActiveRecord::Base.logger = Logger.new(STDOUT)
35
35
  # ActiveRecord::ConnectionAdapters::ConnectionHandler.connection_pool_class = pool_class
36
36
  #end
37
37
 
38
- config = { :'adapter' => ENV['AR_ADAPTER'] || 'sqlite3' }
39
- config[:'username'] = ENV['AR_USERNAME'] if ENV['AR_USERNAME']
40
- config[:'password'] = ENV['AR_PASSWORD'] if ENV['AR_PASSWORD']
38
+ config = { :adapter => ENV['AR_ADAPTER'] || 'sqlite3' }
39
+ config[:username] = ENV['AR_USERNAME'] if ENV['AR_USERNAME']
40
+ config[:password] = ENV['AR_PASSWORD'] if ENV['AR_PASSWORD']
41
41
  if url = ENV['AR_URL'] || ENV['JDBC_URL']
42
42
  config[:'url'] = url
43
43
  else
@@ -51,7 +51,7 @@ config[:'connect_timeout'] = connect_timeout
51
51
  prepared_statements = ENV['AR_PREPARED_STATEMENTS'] # || true
52
52
  config[:'prepared_statements'] = prepared_statements if prepared_statements
53
53
  #jdbc_properties = { 'logUnclosedConnections' => true, 'loginTimeout' => 5 }
54
- #config['properties'] = jdbc_properties
54
+ #config[:'properties'] = jdbc_properties
55
55
 
56
56
  checkout_timeout = ENV['AR_POOL_CHECKOUT_TIMEOUT'] || checkout_timeout
57
57
  config[:'checkout_timeout'] = checkout_timeout.to_f if checkout_timeout
@@ -404,6 +404,9 @@ module ActiveRecord
404
404
  if ar_jdbc_config[:password]
405
405
  hikari_config.addDataSourceProperty 'password', ar_jdbc_config[:password]
406
406
  end
407
+ ( ar_jdbc_config[:properties] || {} ).each do |name, val|
408
+ hikari_config.addDataSourceProperty name.to_s, val.to_s
409
+ end
407
410
  when /postgres/i
408
411
  hikari_config.setDataSourceClassName 'org.postgresql.ds.PGSimpleDataSource'
409
412
  hikari_config.addDataSourceProperty 'serverName', ar_jdbc_config[:host] || 'localhost'
@@ -415,12 +418,16 @@ module ActiveRecord
415
418
  if ar_jdbc_config[:password]
416
419
  hikari_config.addDataSourceProperty 'password', ar_jdbc_config[:password]
417
420
  end
421
+ ( ar_jdbc_config[:properties] || {} ).each do |name, val|
422
+ hikari_config.addDataSourceProperty name.to_s, val.to_s
423
+ end
418
424
  else
419
425
  hikari_config.setDriverClassName driver
420
426
  hikari_config.setJdbcUrl ar_jdbc_config[:url]
421
427
  hikari_config.setUsername ar_jdbc_config[:username] if ar_jdbc_config[:username]
422
428
  hikari_config.setPassword ar_jdbc_config[:password] if ar_jdbc_config[:password]
423
429
  end
430
+ hikari_config.setJdbcUrl ar_jdbc_config[:url] if ar_jdbc_config[:url]
424
431
 
425
432
  # TODO: we shall handle raw properties ?!
426
433
  #if ar_jdbc_config[:properties]
metadata CHANGED
@@ -1,57 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-bogacs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Karol Bucek
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-02 00:00:00.000000000 Z
11
+ date: 2016-06-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
15
- version_requirements: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - '>='
18
- - !ruby/object:Gem::Version
19
- version: '0.9'
20
14
  requirement: !ruby/object:Gem::Requirement
21
15
  requirements:
22
- - - '>='
16
+ - - ">="
23
17
  - !ruby/object:Gem::Version
24
18
  version: '0.9'
19
+ name: concurrent-ruby
25
20
  prerelease: false
26
21
  type: :development
27
- - !ruby/object:Gem::Dependency
28
- name: rake
29
22
  version_requirements: !ruby/object:Gem::Requirement
30
23
  requirements:
31
- - - ~>
24
+ - - ">="
32
25
  - !ruby/object:Gem::Version
33
- version: '10.3'
26
+ version: '0.9'
27
+ - !ruby/object:Gem::Dependency
34
28
  requirement: !ruby/object:Gem::Requirement
35
29
  requirements:
36
- - - ~>
30
+ - - "~>"
37
31
  - !ruby/object:Gem::Version
38
32
  version: '10.3'
33
+ name: rake
39
34
  prerelease: false
40
35
  type: :development
41
- - !ruby/object:Gem::Dependency
42
- name: test-unit
43
36
  version_requirements: !ruby/object:Gem::Requirement
44
37
  requirements:
45
- - - ~>
38
+ - - "~>"
46
39
  - !ruby/object:Gem::Version
47
- version: '2.5'
40
+ version: '10.3'
41
+ - !ruby/object:Gem::Dependency
48
42
  requirement: !ruby/object:Gem::Requirement
49
43
  requirements:
50
- - - ~>
44
+ - - "~>"
51
45
  - !ruby/object:Gem::Version
52
46
  version: '2.5'
47
+ name: test-unit
53
48
  prerelease: false
54
49
  type: :development
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.5'
55
55
  description: Improved ActiveRecord::ConnectionAdapters::ConnectionPool alternatives
56
56
  email:
57
57
  - self@kares.org
@@ -59,8 +59,8 @@ executables: []
59
59
  extensions: []
60
60
  extra_rdoc_files: []
61
61
  files:
62
- - .gitignore
63
- - .travis.yml
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
64
  - Gemfile
65
65
  - LICENSE.txt
66
66
  - README.md
@@ -70,17 +70,21 @@ files:
70
70
  - lib/active_record/bogacs/default_pool.rb
71
71
  - lib/active_record/bogacs/false_pool.rb
72
72
  - lib/active_record/bogacs/pool_support.rb
73
+ - lib/active_record/bogacs/reaper.rb
73
74
  - lib/active_record/bogacs/shareable_pool.rb
74
75
  - lib/active_record/bogacs/thread_safe.rb
75
76
  - lib/active_record/bogacs/thread_safe/synchronized.rb
77
+ - lib/active_record/bogacs/validator.rb
76
78
  - lib/active_record/bogacs/version.rb
77
79
  - lib/active_record/connection_adapters/adapter_compat.rb
78
80
  - lib/active_record/shared_connection.rb
79
81
  - test/active_record/bogacs/default_pool_test.rb
80
82
  - test/active_record/bogacs/false_pool_test.rb
83
+ - test/active_record/bogacs/reaper_test.rb
81
84
  - test/active_record/bogacs/shareable_pool/connection_pool_test.rb
82
85
  - test/active_record/bogacs/shareable_pool/connection_sharing_test.rb
83
86
  - test/active_record/bogacs/shareable_pool_helper.rb
87
+ - test/active_record/bogacs/validator_test.rb
84
88
  - test/active_record/builtin_pool_test.rb
85
89
  - test/active_record/connection_pool_test_methods.rb
86
90
  - test/test_helper.rb
@@ -94,12 +98,12 @@ require_paths:
94
98
  - lib
95
99
  required_ruby_version: !ruby/object:Gem::Requirement
96
100
  requirements:
97
- - - '>='
101
+ - - ">="
98
102
  - !ruby/object:Gem::Version
99
103
  version: '0'
100
104
  required_rubygems_version: !ruby/object:Gem::Requirement
101
105
  requirements:
102
- - - '>='
106
+ - - ">="
103
107
  - !ruby/object:Gem::Version
104
108
  version: '0'
105
109
  requirements: []
@@ -114,9 +118,11 @@ summary: 'Bogacs contains several pool implementations that can be used as a rep
114
118
  test_files:
115
119
  - test/active_record/bogacs/default_pool_test.rb
116
120
  - test/active_record/bogacs/false_pool_test.rb
121
+ - test/active_record/bogacs/reaper_test.rb
117
122
  - test/active_record/bogacs/shareable_pool/connection_pool_test.rb
118
123
  - test/active_record/bogacs/shareable_pool/connection_sharing_test.rb
119
124
  - test/active_record/bogacs/shareable_pool_helper.rb
125
+ - test/active_record/bogacs/validator_test.rb
120
126
  - test/active_record/builtin_pool_test.rb
121
127
  - test/active_record/connection_pool_test_methods.rb
122
128
  - test/test_helper.rb