activerecord-bogacs 0.4.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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