pax_global_header00006660000000000000000000000064150553474700014524gustar00rootroot0000000000000052 comment=0dfef6419d707eec55311d9d50701abc8795fbda mperham-connection_pool-dcdc2db/000077500000000000000000000000001505534747000171725ustar00rootroot00000000000000mperham-connection_pool-dcdc2db/.github/000077500000000000000000000000001505534747000205325ustar00rootroot00000000000000mperham-connection_pool-dcdc2db/.github/dependabot.yml000066400000000000000000000001661505534747000233650ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" mperham-connection_pool-dcdc2db/.github/workflows/000077500000000000000000000000001505534747000225675ustar00rootroot00000000000000mperham-connection_pool-dcdc2db/.github/workflows/ci.yml000066400000000000000000000012551505534747000237100ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: ruby: ["2.5", "2.6", "2.7", "3.0", "3.1", "3.2", "3.3", "3.4", "jruby"] experimental: [false] include: - ruby: "truffleruby" experimental: true steps: - uses: actions/checkout@v5 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run standardrb run: bundle exec standardrb --no-fix - name: Run tests timeout-minutes: 5 run: ${{matrix.env}} bundle exec rake test mperham-connection_pool-dcdc2db/.gitignore000066400000000000000000000000411505534747000211550ustar00rootroot00000000000000*.gem .bundle Gemfile.lock pkg/* mperham-connection_pool-dcdc2db/.standard.yml000066400000000000000000000000551505534747000215730ustar00rootroot00000000000000ruby_version: 2.5.0 fix: true parallel: true mperham-connection_pool-dcdc2db/Changes.md000066400000000000000000000102361505534747000210660ustar00rootroot00000000000000# connection_pool Changelog 2.5.4 ------ - Add ability to remove a broken connection from the pool [#204, womblep] 2.5.3 ------ - Fix TruffleRuby/JRuby crash [#201] 2.5.2 ------ - Rollback inadvertant change to `auto_reload_after_fork` default. [#200] 2.5.1 ------ - Pass options to TimedStack in `checkout` [#195] - Optimize connection lookup [#196] - Fixes for use with Ractors 2.5.0 ------ - Reap idle connections [#187] ```ruby idle_timeout = 60 pool = ConnectionPool.new ... pool.reap(idle_timeout, &:close) ``` - `ConnectionPool#idle` returns the count of connections not in use [#187] 2.4.1 ------ - New `auto_reload_after_fork` config option to disable auto-drop [#177, shayonj] 2.4.0 ------ - Automatically drop all connections after fork [#166] 2.3.0 ------ - Minimum Ruby version is now 2.5.0 - Add pool size to TimeoutError message 2.2.5 ------ - Fix argument forwarding on Ruby 2.7 [#149] 2.2.4 ------ - Add `reload` to close all connections, recreating them afterwards [Andrew Marshall, #140] - Add `then` as a way to use a pool or a bare connection with the same code path [#138] 2.2.3 ------ - Pool now throws `ConnectionPool::TimeoutError` on timeout. [#130] - Use monotonic clock present in all modern Rubies [Tero Tasanen, #109] - Remove code hacks necessary for JRuby 1.7 - Expose wrapped pool from ConnectionPool::Wrapper [Thomas Lecavelier, #113] 2.2.2 ------ - Add pool `size` and `available` accessors for metrics and monitoring purposes [#97, robholland] 2.2.1 ------ - Allow CP::Wrapper to use an existing pool [#87, etiennebarrie] - Use monotonic time for more accurate timeouts [#84, jdantonio] 2.2.0 ------ - Rollback `Timeout` handling introduced in 2.1.1 and 2.1.2. It seems impossible to safely work around the issue. Please never, ever use `Timeout.timeout` in your code or you will see rare but mysterious bugs. [#75] 2.1.3 ------ - Don't increment created count until connection is successfully created. [mylesmegyesi, #73] 2.1.2 ------ - The connection\_pool will now close any connections which respond to `close` (Dalli) or `disconnect!` (Redis). This ensures discarded connections from the fix in 2.1.1 are torn down ASAP and don't linger open. 2.1.1 ------ - Work around a subtle race condition with code which uses `Timeout.timeout` and checks out a connection within the timeout block. This might cause connections to get into a bad state and raise very odd errors. [tamird, #67] 2.1.0 ------ - Refactoring to better support connection pool subclasses [drbrain, #55] - `with` should return value of the last expression [#59] 2.0.0 ----- - The connection pool is now lazy. Connections are created as needed and retained until the pool is shut down. [drbrain, #52] 1.2.0 ----- - Add `with(options)` and `checkout(options)`. [mattcamuto] Allows the caller to override the pool timeout. ```ruby @pool.with(:timeout => 2) do |conn| end ``` 1.1.0 ----- - New `#shutdown` method (simao) This method accepts a block and calls the block for each connection in the pool. After calling this method, trying to get a connection from the pool raises `PoolShuttingDownError`. 1.0.0 ----- - `#with_connection` is now gone in favor of `#with`. - We no longer pollute the top level namespace with our internal `TimedStack` class. 0.9.3 -------- - `#with_connection` is now deprecated in favor of `#with`. A warning will be issued in the 0.9 series and the method will be removed in 1.0. - We now reuse objects when possible. This means that under no contention, the same object will be checked out from the pool after subsequent calls to `ConnectionPool#with`. This change should have no impact on end user performance. If anything, it should be an improvement, depending on what objects you are pooling. 0.9.2 -------- - Fix reentrant checkout leading to early checkin. 0.9.1 -------- - Fix invalid superclass in version.rb 0.9.0 -------- - Move method\_missing magic into ConnectionPool::Wrapper (djanowski) - Remove BasicObject superclass (djanowski) 0.1.0 -------- - More precise timeouts and better error message - ConnectionPool now subclasses BasicObject so `method_missing` is more effective. mperham-connection_pool-dcdc2db/Gemfile000066400000000000000000000001611505534747000204630ustar00rootroot00000000000000source "https://rubygems.org" gemspec(development_group: :runtime) gem "standard", group: [:development, :test] mperham-connection_pool-dcdc2db/LICENSE000066400000000000000000000020371505534747000202010ustar00rootroot00000000000000Copyright (c) 2011 Mike Perham Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. mperham-connection_pool-dcdc2db/README.md000066400000000000000000000133641505534747000204600ustar00rootroot00000000000000connection\_pool ================= [![Build Status](https://github.com/mperham/connection_pool/actions/workflows/ci.yml/badge.svg)](https://github.com/mperham/connection_pool/actions/workflows/ci.yml) Generic connection pooling for Ruby. MongoDB has its own connection pool. ActiveRecord has its own connection pool. This is a generic connection pool that can be used with anything, e.g. Redis, Dalli and other Ruby network clients. Usage ----- Create a pool of objects to share amongst the fibers or threads in your Ruby application: ``` ruby $memcached = ConnectionPool.new(size: 5, timeout: 5) { Dalli::Client.new } ``` Then use the pool in your application: ``` ruby $memcached.with do |conn| conn.get('some-count') end ``` If all the objects in the connection pool are in use, `with` will block until one becomes available. If no object is available within `:timeout` seconds, `with` will raise a `ConnectionPool::TimeoutError` (a subclass of `Timeout::Error`). You can also use `ConnectionPool#then` to support _both_ a connection pool and a raw client. ```ruby # Compatible with a raw Redis::Client, and ConnectionPool Redis $redis.then { |r| r.set 'foo' 'bar' } ``` Optionally, you can specify a timeout override using the with-block semantics: ``` ruby $memcached.with(timeout: 2.0) do |conn| conn.get('some-count') end ``` This will only modify the resource-get timeout for this particular invocation. This is useful if you want to fail-fast on certain non-critical sections when a resource is not available, or conversely if you are comfortable blocking longer on a particular resource. This is not implemented in the `ConnectionPool::Wrapper` class. ## Migrating to a Connection Pool You can use `ConnectionPool::Wrapper` to wrap a single global connection, making it easier to migrate existing connection code over time: ``` ruby $redis = ConnectionPool::Wrapper.new(size: 5, timeout: 3) { Redis.new } $redis.sadd('foo', 1) $redis.smembers('foo') ``` The wrapper uses `method_missing` to checkout a connection, run the requested method and then immediately check the connection back into the pool. It's **not** high-performance so you'll want to port your performance sensitive code to use `with` as soon as possible. ``` ruby $redis.with do |conn| conn.sadd('foo', 1) conn.smembers('foo') end ``` Once you've ported your entire system to use `with`, you can simply remove `Wrapper` and use the simpler and faster `ConnectionPool`. ## Shutdown You can shut down a ConnectionPool instance once it should no longer be used. Further checkout attempts will immediately raise an error but existing checkouts will work. ```ruby cp = ConnectionPool.new { Redis.new } cp.shutdown { |c| c.close } ``` Shutting down a connection pool will block until all connections are checked in and closed. **Note that shutting down is completely optional**; Ruby's garbage collector will reclaim unreferenced pools under normal circumstances. ## Reload You can reload a ConnectionPool instance in the case it is desired to close all connections to the pool and, unlike `shutdown`, afterwards recreate connections so the pool may continue to be used. Reloading may be useful after forking the process. ```ruby cp = ConnectionPool.new { Redis.new } cp.reload { |conn| conn.quit } cp.with { |conn| conn.get('some-count') } ``` Like `shutdown`, this will block until all connections are checked in and closed. ## Reap You can reap idle connections in the ConnectionPool instance to close connections that were created but have not been used for a certain amount of time. This can be useful to run periodically in a separate thread especially if keeping the connection open is resource intensive. You can specify how many seconds the connections have to be idle for them to be reaped. Defaults to 60 seconds. ```ruby cp = ConnectionPool.new { Redis.new } cp.reap(300) { |conn| conn.close } # Reaps connections that have been idle for 300 seconds (5 minutes). ``` ### Reaper Thread You can start your own reaper thread to reap idle connections in the ConnectionPool instance on a regular interval. ```ruby cp = ConnectionPool.new { Redis.new } # Start a reaper thread to reap connections that have been idle for 300 seconds (5 minutes). Thread.new do loop do cp.reap(300) { |conn| conn.close } sleep 300 end end ``` ## Discarding Connections You can discard connections in the ConnectionPool instance to remove connections that are broken and can't be restarted. NOTE: the connection is not closed. It will just be removed from the pool so it won't be selected again. It can only be done inside the block passed to `with` or `with_timeout`. Takes an optional block that will be executed with the connection. ```ruby pool.with do |conn| begin conn.execute("SELECT 1") rescue SomeConnectionError pool.discard_current_connection # remove the connection from the pool raise end end ``` ## Current State There are several methods that return information about a pool. ```ruby cp = ConnectionPool.new(size: 10) { Redis.new } cp.size # => 10 cp.available # => 10 cp.idle # => 0 cp.with do |conn| cp.size # => 10 cp.available # => 9 cp.idle # => 0 end cp.idle # => 1 ``` Notes ----- - Connections are lazily created as needed. - There is no provision for repairing or checking the health of a connection; connections should be self-repairing. This is true of the Dalli and Redis clients. - **WARNING**: Don't ever use `Timeout.timeout` in your Ruby code or you will see occasional silent corruption and mysterious errors. The Timeout API is unsafe and cannot be used correctly, ever. Use proper socket timeout options as exposed by Net::HTTP, Redis, Dalli, etc. Author ------ Mike Perham, [@getajobmike](https://twitter.com/getajobmike), mperham-connection_pool-dcdc2db/Rakefile000066400000000000000000000002071505534747000206360ustar00rootroot00000000000000require "bundler/gem_tasks" require "standard/rake" require "rake/testtask" Rake::TestTask.new task default: [:"standard:fix", :test] mperham-connection_pool-dcdc2db/connection_pool.gemspec000066400000000000000000000017601505534747000237330ustar00rootroot00000000000000require "./lib/connection_pool/version" Gem::Specification.new do |s| s.name = "connection_pool" s.version = ConnectionPool::VERSION s.platform = Gem::Platform::RUBY s.authors = ["Mike Perham", "Damian Janowski"] s.email = ["mperham@gmail.com", "damian@educabilia.com"] s.homepage = "https://github.com/mperham/connection_pool" s.description = s.summary = "Generic connection pool for Ruby" s.files = ["Changes.md", "LICENSE", "README.md", "connection_pool.gemspec", "lib/connection_pool.rb", "lib/connection_pool/timed_stack.rb", "lib/connection_pool/version.rb", "lib/connection_pool/wrapper.rb"] s.executables = [] s.require_paths = ["lib"] s.license = "MIT" s.add_development_dependency "bundler" s.add_development_dependency "minitest", ">= 5.0.0" s.add_development_dependency "rake" s.required_ruby_version = ">= 2.5.0" s.metadata = {"changelog_uri" => "https://github.com/mperham/connection_pool/blob/main/Changes.md", "rubygems_mfa_required" => "true"} end mperham-connection_pool-dcdc2db/lib/000077500000000000000000000000001505534747000177405ustar00rootroot00000000000000mperham-connection_pool-dcdc2db/lib/connection_pool.rb000066400000000000000000000147771505534747000234750ustar00rootroot00000000000000require "timeout" require_relative "connection_pool/version" class ConnectionPool class Error < ::RuntimeError; end class PoolShuttingDownError < ::ConnectionPool::Error; end class TimeoutError < ::Timeout::Error; end end # Generic connection pool class for sharing a limited number of objects or network connections # among many threads. Note: pool elements are lazily created. # # Example usage with block (faster): # # @pool = ConnectionPool.new { Redis.new } # @pool.with do |redis| # redis.lpop('my-list') if redis.llen('my-list') > 0 # end # # Using optional timeout override (for that single invocation) # # @pool.with(timeout: 2.0) do |redis| # redis.lpop('my-list') if redis.llen('my-list') > 0 # end # # Example usage replacing an existing connection (slower): # # $redis = ConnectionPool.wrap { Redis.new } # # def do_work # $redis.lpop('my-list') if $redis.llen('my-list') > 0 # end # # Accepts the following options: # - :size - number of connections to pool, defaults to 5 # - :timeout - amount of time to wait for a connection if none currently available, defaults to 5 seconds # - :auto_reload_after_fork - automatically drop all connections after fork, defaults to true # class ConnectionPool DEFAULTS = {size: 5, timeout: 5, auto_reload_after_fork: true}.freeze def self.wrap(options, &block) Wrapper.new(options, &block) end if Process.respond_to?(:fork) INSTANCES = ObjectSpace::WeakMap.new private_constant :INSTANCES def self.after_fork INSTANCES.values.each do |pool| next unless pool.auto_reload_after_fork # We're on after fork, so we know all other threads are dead. # All we need to do is to ensure the main thread doesn't have a # checked out connection pool.checkin(force: true) pool.reload do |connection| # Unfortunately we don't know what method to call to close the connection, # so we try the most common one. connection.close if connection.respond_to?(:close) end end nil end if ::Process.respond_to?(:_fork) # MRI 3.1+ module ForkTracker def _fork pid = super if pid == 0 ConnectionPool.after_fork end pid end end Process.singleton_class.prepend(ForkTracker) end else INSTANCES = nil private_constant :INSTANCES def self.after_fork # noop end end def initialize(options = {}, &block) raise ArgumentError, "Connection pool requires a block" unless block options = DEFAULTS.merge(options) @size = Integer(options.fetch(:size)) @timeout = options.fetch(:timeout) @auto_reload_after_fork = options.fetch(:auto_reload_after_fork) @available = TimedStack.new(@size, &block) @key = :"pool-#{@available.object_id}" @key_count = :"pool-#{@available.object_id}-count" @discard_key = :"pool-#{@available.object_id}-discard" INSTANCES[self] = self if @auto_reload_after_fork && INSTANCES end def with(options = {}) Thread.handle_interrupt(Exception => :never) do conn = checkout(options) begin Thread.handle_interrupt(Exception => :immediate) do yield conn end ensure checkin end end end alias_method :then, :with ## # Marks the current thread's checked-out connection for discard. # # When a connection is marked for discard, it will not be returned to the pool # when checked in. Instead, the connection will be discarded. # This is useful when a connection has become invalid or corrupted # and should not be reused. # # Takes an optional block that will be called with the connection to be discarded. # The block should perform any necessary clean-up on the connection. # # @yield [conn] # @yieldparam conn [Object] The connection to be discarded. # @yieldreturn [void] # # # Note: This only affects the connection currently checked out by the calling thread. # The connection will be discarded when +checkin+ is called. # # @return [void] # # @example # pool.with do |conn| # begin # conn.execute("SELECT 1") # rescue SomeConnectionError # pool.discard_current_connection # Mark connection as bad # raise # end # end def discard_current_connection(&block) ::Thread.current[@discard_key] = block || proc { |conn| conn } end def checkout(options = {}) if ::Thread.current[@key] ::Thread.current[@key_count] += 1 ::Thread.current[@key] else ::Thread.current[@key_count] = 1 ::Thread.current[@key] = @available.pop(options[:timeout] || @timeout, options) end end def checkin(force: false) if ::Thread.current[@key] if ::Thread.current[@key_count] == 1 || force if ::Thread.current[@discard_key] begin @available.decrement_created ::Thread.current[@discard_key].call(::Thread.current[@key]) rescue nil ensure ::Thread.current[@discard_key] = nil end else @available.push(::Thread.current[@key]) end ::Thread.current[@key] = nil ::Thread.current[@key_count] = nil else ::Thread.current[@key_count] -= 1 end elsif !force raise ConnectionPool::Error, "no connections are checked out" end nil end ## # Shuts down the ConnectionPool by passing each connection to +block+ and # then removing it from the pool. Attempting to checkout a connection after # shutdown will raise +ConnectionPool::PoolShuttingDownError+. def shutdown(&block) @available.shutdown(&block) end ## # Reloads the ConnectionPool by passing each connection to +block+ and then # removing it the pool. Subsequent checkouts will create new connections as # needed. def reload(&block) @available.shutdown(reload: true, &block) end ## Reaps idle connections that have been idle for over +idle_seconds+. # +idle_seconds+ defaults to 60. def reap(idle_seconds = 60, &block) @available.reap(idle_seconds, &block) end # Size of this connection pool attr_reader :size # Automatically drop all connections after fork attr_reader :auto_reload_after_fork # Number of pool entries available for checkout at this instant. def available @available.length end # Number of pool entries created and idle in the pool. def idle @available.idle end end require_relative "connection_pool/timed_stack" require_relative "connection_pool/wrapper" mperham-connection_pool-dcdc2db/lib/connection_pool/000077500000000000000000000000001505534747000231305ustar00rootroot00000000000000mperham-connection_pool-dcdc2db/lib/connection_pool/timed_stack.rb000066400000000000000000000143521505534747000257510ustar00rootroot00000000000000## # The TimedStack manages a pool of homogeneous connections (or any resource # you wish to manage). Connections are created lazily up to a given maximum # number. # # Examples: # # ts = TimedStack.new(1) { MyConnection.new } # # # fetch a connection # conn = ts.pop # # # return a connection # ts.push conn # # conn = ts.pop # ts.pop timeout: 5 # #=> raises ConnectionPool::TimeoutError after 5 seconds class ConnectionPool::TimedStack attr_reader :max ## # Creates a new pool with +size+ connections that are created from the given # +block+. def initialize(size = 0, &block) @create_block = block @created = 0 @que = [] @max = size @mutex = Thread::Mutex.new @resource = Thread::ConditionVariable.new @shutdown_block = nil end ## # Returns +obj+ to the stack. +options+ is ignored in TimedStack but may be # used by subclasses that extend TimedStack. def push(obj, options = {}) @mutex.synchronize do if @shutdown_block @created -= 1 unless @created == 0 @shutdown_block.call(obj) else store_connection obj, options end @resource.broadcast end end alias_method :<<, :push ## # Retrieves a connection from the stack. If a connection is available it is # immediately returned. If no connection is available within the given # timeout a ConnectionPool::TimeoutError is raised. # # +:timeout+ is the only checked entry in +options+ and is preferred over # the +timeout+ argument (which will be removed in a future release). Other # options may be used by subclasses that extend TimedStack. def pop(timeout = 0.5, options = {}) options, timeout = timeout, 0.5 if Hash === timeout timeout = options.fetch :timeout, timeout deadline = current_time + timeout @mutex.synchronize do loop do raise ConnectionPool::PoolShuttingDownError if @shutdown_block if (conn = try_fetch_connection(options)) return conn end connection = try_create(options) return connection if connection to_wait = deadline - current_time raise ConnectionPool::TimeoutError, "Waited #{timeout} sec, #{length}/#{@max} available" if to_wait <= 0 @resource.wait(@mutex, to_wait) end end end ## # Shuts down the TimedStack by passing each connection to +block+ and then # removing it from the pool. Attempting to checkout a connection after # shutdown will raise +ConnectionPool::PoolShuttingDownError+ unless # +:reload+ is +true+. def shutdown(reload: false, &block) raise ArgumentError, "shutdown must receive a block" unless block @mutex.synchronize do @shutdown_block = block @resource.broadcast shutdown_connections @shutdown_block = nil if reload end end ## # Reaps connections that were checked in more than +idle_seconds+ ago. def reap(idle_seconds, &block) raise ArgumentError, "reap must receive a block" unless block raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric) raise ConnectionPool::PoolShuttingDownError if @shutdown_block idle.times do conn = @mutex.synchronize do raise ConnectionPool::PoolShuttingDownError if @shutdown_block reserve_idle_connection(idle_seconds) end break unless conn block.call(conn) end end ## # Returns +true+ if there are no available connections. def empty? (@created - @que.length) >= @max end ## # The number of connections available on the stack. def length @max - @created + @que.length end ## # The number of connections created and available on the stack. def idle @que.length end ## # Reduce the created count def decrement_created @created -= 1 unless @created == 0 end private def current_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must returns a connection from the stack if one exists. Allows # subclasses with expensive match/search algorithms to avoid double-handling # their stack. def try_fetch_connection(options = nil) connection_stored?(options) && fetch_connection(options) end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must returns true if a connection is available on the stack. def connection_stored?(options = nil) !@que.empty? end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must return a connection from the stack. def fetch_connection(options = nil) @que.pop&.first end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must shut down all connections on the stack. def shutdown_connections(options = nil) while (conn = try_fetch_connection(options)) @created -= 1 unless @created == 0 @shutdown_block.call(conn) end end ## # This is an extension point for TimedStack and is called with a mutex. # # This method returns the oldest idle connection if it has been idle for more than idle_seconds. # This requires that the stack is kept in order of checked in time (oldest first). def reserve_idle_connection(idle_seconds) return unless idle_connections?(idle_seconds) @created -= 1 unless @created == 0 @que.shift.first end ## # This is an extension point for TimedStack and is called with a mutex. # # Returns true if the first connection in the stack has been idle for more than idle_seconds def idle_connections?(idle_seconds) connection_stored? && (current_time - @que.first.last > idle_seconds) end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must return +obj+ to the stack. def store_connection(obj, options = nil) @que.push [obj, current_time] end ## # This is an extension point for TimedStack and is called with a mutex. # # This method must create a connection if and only if the total number of # connections allowed has not been met. def try_create(options = nil) unless @created == @max object = @create_block.call @created += 1 object end end end mperham-connection_pool-dcdc2db/lib/connection_pool/version.rb000066400000000000000000000000551505534747000251420ustar00rootroot00000000000000class ConnectionPool VERSION = "2.5.4" end mperham-connection_pool-dcdc2db/lib/connection_pool/wrapper.rb000066400000000000000000000024451505534747000251420ustar00rootroot00000000000000class ConnectionPool class Wrapper < ::BasicObject METHODS = [:with, :pool_shutdown, :wrapped_pool] def initialize(options = {}, &block) @pool = options.fetch(:pool) { ::ConnectionPool.new(options, &block) } end def wrapped_pool @pool end def with(&block) @pool.with(&block) end def pool_shutdown(&block) @pool.shutdown(&block) end def pool_size @pool.size end def pool_available @pool.available end def respond_to?(id, *args) METHODS.include?(id) || with { |c| c.respond_to?(id, *args) } end # rubocop:disable Style/MissingRespondToMissing if ::RUBY_VERSION >= "3.0.0" def method_missing(name, *args, **kwargs, &block) with do |connection| connection.send(name, *args, **kwargs, &block) end end elsif ::RUBY_VERSION >= "2.7.0" ruby2_keywords def method_missing(name, *args, &block) with do |connection| connection.send(name, *args, &block) end end else def method_missing(name, *args, &block) with do |connection| connection.send(name, *args, &block) end end end # rubocop:enable Style/MethodMissingSuper # rubocop:enable Style/MissingRespondToMissing end end mperham-connection_pool-dcdc2db/test/000077500000000000000000000000001505534747000201515ustar00rootroot00000000000000mperham-connection_pool-dcdc2db/test/helper.rb000066400000000000000000000004431505534747000217560ustar00rootroot00000000000000gem "minitest" require "minitest/pride" require "minitest/autorun" $VERBOSE = 1 require_relative "../lib/connection_pool" class ConnectionPool def self.reset_instances ov, $VERBOSE = $VERBOSE const_set(:INSTANCES, ObjectSpace::WeakMap.new) ensure $VERBOSE = ov end end mperham-connection_pool-dcdc2db/test/test_connection_pool.rb000066400000000000000000000455501505534747000247360ustar00rootroot00000000000000require_relative "helper" class TestConnectionPool < Minitest::Test def teardown # wipe the `:INSTANCES` const to avoid cross test contamination ConnectionPool.reset_instances end class NetworkConnection SLEEP_TIME = 0.1 def initialize @x = 0 end def do_something(*_args, increment: 1) @x += increment sleep SLEEP_TIME @x end def do_something_with_positional_hash(options) @x += options[:increment] || 1 sleep SLEEP_TIME @x end def fast @x += 1 end def do_something_with_block @x += yield sleep SLEEP_TIME @x end def respond_to?(method_id, *args) method_id == :do_magic || super end end class Recorder def initialize @calls = [] end attr_reader :calls def do_work(label) @calls << label end end def use_pool(pool, size) Array.new(size) { Thread.new do pool.with { sleep } end }.each do |thread| Thread.pass until thread.status == "sleep" end end def kill_threads(threads) threads.each do |thread| thread.kill thread.join end end def test_basic_multithreaded_usage pool_size = 5 pool = ConnectionPool.new(size: pool_size) { NetworkConnection.new } start = Time.new generations = 3 result = Array.new(pool_size * generations) { Thread.new do pool.with do |net| net.do_something end end }.map(&:value) finish = Time.new assert_equal((1..generations).cycle(pool_size).sort, result.sort) assert_operator(finish - start, :>, generations * NetworkConnection::SLEEP_TIME) end def test_timeout pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new } thread = Thread.new { pool.with do |net| net.do_something sleep 0.01 end } Thread.pass while thread.status == "run" assert_raises Timeout::Error do pool.with { |net| net.do_something } end thread.join pool.with do |conn| refute_nil conn end end def test_with pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } pool.with do Thread.new { assert_raises Timeout::Error do pool.checkout end }.join end assert Thread.new { pool.checkout }.join end def test_then pool = ConnectionPool.new { Object.new } assert_equal pool.method(:then), pool.method(:with) end def test_with_timeout pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } assert_raises Timeout::Error do Timeout.timeout(0.01) do pool.with do |obj| assert_equal 0, pool.available sleep 0.015 end end end assert_equal 1, pool.available end def test_invalid_size assert_raises ArgumentError, TypeError do ConnectionPool.new(timeout: 0, size: nil) { Object.new } end assert_raises ArgumentError, TypeError do ConnectionPool.new(timeout: 0, size: "") { Object.new } end end def test_handle_interrupt_ensures_checkin pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } def pool.checkout(options) sleep 0.015 super end action = lambda do Timeout.timeout(0.01) do pool.with do |obj| # Timeout::Error will be triggered by any non-trivial Ruby code # executed here since it couldn't be raised during checkout. # It looks like setting a local variable does not trigger # the Timeout check in MRI 2.2.1. obj.tap { obj.hash } end end end assert_raises Timeout::Error, &action assert_equal 1, pool.available end def test_explicit_return pool = ConnectionPool.new(timeout: 0, size: 1) { mock = Minitest::Mock.new def mock.disconnect! raise "should not disconnect upon explicit return" end mock } pool.with do |conn| return true end end def test_with_timeout_override pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new } t = Thread.new { pool.with do |net| net.do_something sleep 0.01 end } Thread.pass while t.status == "run" assert_raises Timeout::Error do pool.with { |net| net.do_something } end pool.with(timeout: 2 * NetworkConnection::SLEEP_TIME) do |conn| refute_nil conn end end def test_with_options pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } stack = pool.instance_variable_get(:@available) def stack.connection_stored?(opts) raise opts.to_s end options = {foo: 123} e = assert_raises do pool.with(options) {} end assert_equal e.message, options.to_s end def test_checkin pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new } conn = pool.checkout Thread.new { assert_raises Timeout::Error do pool.checkout end }.join pool.checkin assert_same conn, Thread.new { pool.checkout }.value end def test_discard pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new } pool.checkout Thread.new { assert_raises Timeout::Error do pool.checkout end }.join pool.discard_current_connection pool.checkin assert_equal 1, pool.size assert_equal 0, pool.idle assert_equal 1, pool.available end def test_discard_with_argument pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new } pool.checkout Thread.new { assert_raises Timeout::Error do pool.checkout end }.join pool.discard_current_connection { |conn| assert_kind_of NetworkConnection, conn } pool.checkin assert_equal 1, pool.size assert_equal 0, pool.idle assert_equal 1, pool.available end def test_discard_with_argument_and_error pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new } pool.checkout Thread.new { assert_raises Timeout::Error do pool.checkout end }.join pool.discard_current_connection { |conn| raise "boom" } pool.checkin assert_equal 1, pool.size assert_equal 0, pool.idle assert_equal 1, pool.available end def test_returns_value pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } assert_equal 1, pool.with { |o| 1 } end def test_checkin_never_checkout pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } e = assert_raises(ConnectionPool::Error) { pool.checkin } assert_equal "no connections are checked out", e.message end def test_checkin_no_current_checkout pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } pool.checkout pool.checkin assert_raises ConnectionPool::Error do pool.checkin end end def test_checkin_twice pool = ConnectionPool.new(timeout: 0, size: 1) { Object.new } pool.checkout pool.checkout pool.checkin Thread.new { assert_raises Timeout::Error do pool.checkout end }.join pool.checkin assert Thread.new { pool.checkout }.join end def test_checkout pool = ConnectionPool.new(size: 1) { NetworkConnection.new } conn = pool.checkout assert_kind_of NetworkConnection, conn assert_same conn, pool.checkout end def test_checkout_multithread pool = ConnectionPool.new(size: 2) { NetworkConnection.new } conn = pool.checkout t = Thread.new { pool.checkout } refute_same conn, t.value end def test_checkout_timeout pool = ConnectionPool.new(timeout: 0, size: 0) { Object.new } assert_raises Timeout::Error do pool.checkout end end def test_checkout_timeout_override pool = ConnectionPool.new(timeout: 0, size: 1) { NetworkConnection.new } thread = Thread.new { pool.with do |net| net.do_something sleep 0.01 end } Thread.pass while thread.status == "run" assert_raises Timeout::Error do pool.checkout end assert pool.checkout timeout: 2 * NetworkConnection::SLEEP_TIME end def test_passthru pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new } assert_equal 1, pool.do_something assert_equal 2, pool.do_something assert_equal 5, pool.do_something_with_block { 3 } assert_equal 6, pool.with { |net| net.fast } assert_equal 8, pool.do_something(increment: 2) assert_equal 10, pool.do_something_with_positional_hash({:increment => 2, :symbol_key => 3, "string_key" => 4}) end def test_passthru_respond_to pool = ConnectionPool.wrap(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new } assert pool.respond_to?(:with) assert pool.respond_to?(:do_something) assert pool.respond_to?(:do_magic) refute pool.respond_to?(:do_lots_of_magic) end def test_return_value pool = ConnectionPool.new(timeout: 2 * NetworkConnection::SLEEP_TIME, size: 1) { NetworkConnection.new } result = pool.with { |net| net.fast } assert_equal 1, result end def test_heavy_threading pool = ConnectionPool.new(timeout: 0.5, size: 3) { NetworkConnection.new } threads = Array.new(20) { Thread.new do pool.with do |net| sleep 0.01 end end } threads.map { |thread| thread.join } end def test_reuses_objects_when_pool_not_saturated pool = ConnectionPool.new(size: 5) { NetworkConnection.new } ids = 10.times.map { pool.with { |c| c.object_id } } assert_equal 1, ids.uniq.size end def test_nested_checkout recorder = Recorder.new pool = ConnectionPool.new(size: 1) { recorder } pool.with do |r_outer| @other = Thread.new { |t| pool.with do |r_other| r_other.do_work("other") end } pool.with do |r_inner| r_inner.do_work("inner") end Thread.pass r_outer.do_work("outer") end @other.join assert_equal ["inner", "outer", "other"], recorder.calls end def test_nested_discard recorder = Recorder.new pool = ConnectionPool.new(size: 1) { {recorder: recorder} } pool.with do |r_outer| @other = Thread.new { |t| pool.with do |r_other| r_other[:recorder].do_work("other") end } pool.with do |r_inner| @inner = r_inner r_inner[:recorder].do_work("inner") pool.discard_current_connection end Thread.pass r_outer[:recorder].do_work("outer") end @other.join assert_equal ["inner", "outer", "other"], recorder.calls refute_same @inner, pool.checkout end def test_shutdown_is_executed_for_all_connections recorders = [] pool = ConnectionPool.new(size: 3) { Recorder.new.tap { |r| recorders << r } } threads = use_pool pool, 3 pool.shutdown do |recorder| recorder.do_work("shutdown") end kill_threads(threads) assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls } end def test_checkout_after_reload_cannot_create_new_connections_beyond_size pool = ConnectionPool.new(size: 1) { Object.new } threads = use_pool pool, 1 pool.reload {} assert_raises ConnectionPool::TimeoutError do pool.checkout(timeout: 0) end ensure kill_threads(threads) if threads end def test_raises_error_after_shutting_down pool = ConnectionPool.new(size: 1) { true } pool.shutdown {} assert_raises ConnectionPool::PoolShuttingDownError do pool.checkout end end def test_runs_shutdown_block_asynchronously_if_connection_was_in_use recorders = [] pool = ConnectionPool.new(size: 3) { Recorder.new.tap { |r| recorders << r } } threads = use_pool pool, 2 pool.checkout pool.shutdown do |recorder| recorder.do_work("shutdown") end kill_threads(threads) assert_equal [["shutdown"], ["shutdown"], []], recorders.map { |r| r.calls } pool.checkin assert_equal [["shutdown"], ["shutdown"], ["shutdown"]], recorders.map { |r| r.calls } end def test_raises_an_error_if_shutdown_is_called_without_a_block pool = ConnectionPool.new(size: 1) {} assert_raises ArgumentError do pool.shutdown end end def test_shutdown_is_executed_for_all_connections_in_wrapped_pool recorders = [] wrapper = ConnectionPool::Wrapper.new(size: 3) { Recorder.new.tap { |r| recorders << r } } threads = use_pool wrapper, 3 wrapper.pool_shutdown do |recorder| recorder.do_work("shutdown") end kill_threads(threads) assert_equal [["shutdown"]] * 3, recorders.map { |r| r.calls } end def test_reap_removes_idle_connections recorders = [] pool = ConnectionPool.new(size: 1) do Recorder.new.tap { |r| recorders << r } end pool.with { |conn| conn } assert_equal 1, pool.idle pool.reap(0) { |recorder| recorder.do_work("reap") } assert_equal 0, pool.idle assert_equal [["reap"]], recorders.map(&:calls) end def test_reap_removes_all_idle_connections recorders = [] pool = ConnectionPool.new(size: 3) do Recorder.new.tap { |r| recorders << r } end threads = use_pool(pool, 3) kill_threads(threads) assert_equal 3, pool.idle pool.reap(0) { |recorder| recorder.do_work("reap") } assert_equal 0, pool.idle assert_equal [["reap"]] * 3, recorders.map(&:calls) end def test_reap_does_not_remove_connections_if_outside_idle_time pool = ConnectionPool.new(size: 1) { Object.new } pool.with { |conn| conn } pool.reap(1000) { |conn| flunk "should not reap active connection" } end def test_idle_returns_number_of_idle_connections pool = ConnectionPool.new(size: 1) { Object.new } assert_equal 0, pool.idle pool.checkout assert_equal 0, pool.idle pool.checkin assert_equal 1, pool.idle end def test_idle_with_multiple_connections pool = ConnectionPool.new(size: 3) { Object.new } assert_equal 0, pool.idle threads = use_pool(pool, 3) assert_equal 0, pool.idle kill_threads(threads) assert_equal 3, pool.idle end def test_reap_raises_error_after_shutting_down pool = ConnectionPool.new(size: 1) { true } pool.shutdown {} assert_raises ConnectionPool::PoolShuttingDownError do pool.reap(0) {} end end def test_wrapper_wrapped_pool wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new } assert_equal ConnectionPool, wrapper.wrapped_pool.class end def test_wrapper_method_missing wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new } assert_equal 1, wrapper.fast end def test_wrapper_respond_to_eh wrapper = ConnectionPool::Wrapper.new { NetworkConnection.new } assert_respond_to wrapper, :with assert_respond_to wrapper, :fast refute_respond_to wrapper, :"nonexistent method" end def test_wrapper_with wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { Object.new } wrapper.with do Thread.new { assert_raises Timeout::Error do wrapper.with { flunk "connection checked out :(" } end }.join end assert Thread.new { wrapper.with {} }.join end class ConnWithEval def eval(arg) "eval'ed #{arg}" end end def test_wrapper_kernel_methods wrapper = ConnectionPool::Wrapper.new(timeout: 0, size: 1) { ConnWithEval.new } assert_equal "eval'ed 1", wrapper.eval(1) end def test_wrapper_with_connection_pool recorder = Recorder.new pool = ConnectionPool.new(size: 1) { recorder } wrapper = ConnectionPool::Wrapper.new(pool: pool) pool.with { |r| r.do_work("with") } wrapper.do_work("wrapped") assert_equal ["with", "wrapped"], recorder.calls end def test_stats_without_active_connection pool = ConnectionPool.new(size: 2) { NetworkConnection.new } assert_equal(2, pool.size) assert_equal(2, pool.available) end def test_stats_with_active_connection pool = ConnectionPool.new(size: 2) { NetworkConnection.new } pool.with do assert_equal(1, pool.available) end end def test_stats_with_string_size pool = ConnectionPool.new(size: "2") { NetworkConnection.new } pool.with do assert_equal(2, pool.size) assert_equal(1, pool.available) end end def test_after_fork_callback skip("MRI feature") unless Process.respond_to?(:fork) GC.start # cleanup instances created by other tests pool = ConnectionPool.new(size: 2, auto_reload_after_fork: true) { NetworkConnection.new } prefork_connection = pool.with { |c| c } assert_equal(prefork_connection, pool.with { |c| c }) ConnectionPool.after_fork refute_equal(prefork_connection, pool.with { |c| c }) end def test_after_fork_callback_being_skipped skip("MRI feature") unless Process.respond_to?(:fork) GC.start # cleanup instances created by other tests pool = ConnectionPool.new(size: 2, auto_reload_after_fork: false) { NetworkConnection.new } prefork_connection = pool.with { |c| c } assert_equal(prefork_connection, pool.with { |c| c }) ConnectionPool.after_fork assert_equal(prefork_connection, pool.with { |c| c }) end def test_after_fork_callback_checkin skip("MRI feature") unless Process.respond_to?(:fork) GC.start # cleanup instances created by other tests pool = ConnectionPool.new(size: 2, auto_reload_after_fork: true) { NetworkConnection.new } prefork_connection = pool.checkout assert_equal(prefork_connection, pool.checkout) ConnectionPool.after_fork refute_equal(prefork_connection, pool.checkout) end def test_automatic_after_fork_callback skip("MRI 3.1 feature") unless Process.respond_to?(:_fork) GC.start # cleanup instances created by other tests pool = ConnectionPool.new(size: 2, auto_reload_after_fork: true) { NetworkConnection.new } prefork_connection = pool.with { |c| c } assert_equal(prefork_connection, pool.with { |c| c }) pid = fork do refute_equal(prefork_connection, pool.with { |c| c }) exit!(0) end assert_equal(prefork_connection, pool.with { |c| c }) _, status = Process.waitpid2(pid) assert_predicate(status, :success?) end def test_ractors begin Ractor rescue NameError skip("Ractor not available") end begin # TODO Ractor prints a bunch of warnings to the console, no idea # how to turn it off r = Ractor.new do ConnectionPool.new(auto_reload_after_fork: true) { Object.new } true end r.take # should not get here refute true rescue Ractor::RemoteError => re # expected assert re.cause assert_equal Ractor::IsolationError, re.cause.class assert_match(/ConnectionPool::INSTANCES/, re.cause.message) end r = Ractor.new do ConnectionPool.new(auto_reload_after_fork: false) { Object.new } true end assert_equal true, r.take end end mperham-connection_pool-dcdc2db/test/test_connection_pool_timed_stack.rb000066400000000000000000000177451505534747000273120ustar00rootroot00000000000000require_relative "helper" class TestConnectionPoolTimedStack < Minitest::Test def setup @stack = ConnectionPool::TimedStack.new { Object.new } end def test_empty_eh stack = ConnectionPool::TimedStack.new(1) { Object.new } refute_empty stack popped = stack.pop assert_empty stack stack.push popped refute_empty stack end def test_length stack = ConnectionPool::TimedStack.new(1) { Object.new } assert_equal 1, stack.length popped = stack.pop assert_equal 0, stack.length stack.push popped assert_equal 1, stack.length end def test_length_after_shutdown_reload_for_no_create_stack assert_equal 0, @stack.length @stack.push(Object.new) assert_equal 1, @stack.length @stack.shutdown(reload: true) {} assert_equal 0, @stack.length end def test_length_after_shutdown_reload_with_checked_out_conn stack = ConnectionPool::TimedStack.new(1) { Object.new } conn = stack.pop stack.shutdown(reload: true) {} assert_equal 0, stack.length stack.push(conn) assert_equal 1, stack.length end def test_idle stack = ConnectionPool::TimedStack.new(1) { Object.new } assert_equal 0, stack.idle popped = stack.pop assert_equal 0, stack.idle stack.push popped assert_equal 1, stack.idle end def test_object_creation_fails stack = ConnectionPool::TimedStack.new(2) { raise "failure" } begin stack.pop rescue => error assert_equal "failure", error.message end begin stack.pop rescue => error assert_equal "failure", error.message end refute_empty stack assert_equal 2, stack.length end def test_pop object = Object.new @stack.push object popped = @stack.pop assert_same object, popped end def test_pop_empty e = assert_raises(ConnectionPool::TimeoutError) { @stack.pop timeout: 0 } assert_equal "Waited 0 sec, 0/0 available", e.message end def test_pop_empty_2_0_compatibility e = assert_raises(Timeout::Error) { @stack.pop 0 } assert_equal "Waited 0 sec, 0/0 available", e.message end def test_pop_full stack = ConnectionPool::TimedStack.new(1) { Object.new } popped = stack.pop refute_nil popped assert_empty stack end def test_pop_full_with_extra_conn_pushed stack = ConnectionPool::TimedStack.new(1) { Object.new } popped = stack.pop stack.push(Object.new) stack.push(popped) assert_equal 2, stack.length stack.shutdown(reload: true) {} assert_equal 1, stack.length stack.pop assert_raises(ConnectionPool::TimeoutError) { stack.pop(0) } end def test_pop_wait thread = Thread.start { @stack.pop } Thread.pass while thread.status == "run" object = Object.new @stack.push object assert_same object, thread.value end def test_pop_shutdown @stack.shutdown {} assert_raises ConnectionPool::PoolShuttingDownError do @stack.pop end end def test_pop_shutdown_reload stack = ConnectionPool::TimedStack.new(1) { Object.new } object = stack.pop stack.push(object) stack.shutdown(reload: true) {} refute_equal object, stack.pop end def test_pop_raises_error_if_shutdown_reload_is_run_and_connection_is_still_in_use stack = ConnectionPool::TimedStack.new(1) { Object.new } stack.pop stack.shutdown(reload: true) {} assert_raises ConnectionPool::TimeoutError do stack.pop(0) end end def test_push stack = ConnectionPool::TimedStack.new(1) { Object.new } conn = stack.pop stack.push conn refute_empty stack end def test_push_shutdown called = [] @stack.shutdown do |object| called << object end @stack.push Object.new refute_empty called assert_empty @stack end def test_shutdown @stack.push Object.new called = [] @stack.shutdown do |object| called << object end refute_empty called assert_empty @stack end def test_shutdown_can_be_called_after_error 3.times { @stack.push Object.new } called = [] closing_error = "error in closing connection" raise_error = true shutdown_proc = ->(conn) do called << conn if raise_error raise_error = false raise closing_error end end assert_raises(closing_error) do @stack.shutdown(&shutdown_proc) end assert_equal 1, called.size @stack.shutdown(&shutdown_proc) assert_equal 3, called.size end def test_reap_can_be_called_after_error 3.times { @stack.push Object.new } called = [] closing_error = "error in closing connection" raise_error = true reap_proc = ->(conn) do called << conn if raise_error raise_error = false raise closing_error end end assert_raises(closing_error) do @stack.reap(0, &reap_proc) end assert_equal 1, called.size @stack.reap(0, &reap_proc) assert_equal 3, called.size end def test_reap @stack.push Object.new called = [] @stack.reap(0) do |object| called << object end refute_empty called assert_empty @stack end def test_reap_full_stack stack = ConnectionPool::TimedStack.new(1) { Object.new } stack.push stack.pop stack.reap(0) do |object| nil end # Can still pop from the stack after reaping all connections refute_nil stack.pop end def test_reap_large_idle_seconds @stack.push Object.new called = [] @stack.reap(100) do |object| called << object end assert_empty called refute_empty @stack end def test_reap_no_block assert_raises(ArgumentError) do @stack.reap(0) end end def test_reap_non_numeric_idle_seconds assert_raises(ArgumentError) do @stack.reap("0") { |object| object } end end def test_reap_with_multiple_connections stack = ConnectionPool::TimedStack.new(2) { Object.new } stubbed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) conn1 = stack.pop conn2 = stack.pop stack.stub :current_time, stubbed_time do stack.push conn1 end stack.stub :current_time, stubbed_time + 1 do stack.push conn2 end called = [] stack.stub :current_time, stubbed_time + 2 do stack.reap(1.5) do |object| called << object end end assert_equal [conn1], called refute_empty stack assert_equal 1, stack.idle end def test_reap_with_multiple_connections_and_zero_idle_seconds stack = ConnectionPool::TimedStack.new(2) { Object.new } stubbed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) conn1 = stack.pop conn2 = stack.pop stack.stub :current_time, stubbed_time do stack.push conn1 end stack.stub :current_time, stubbed_time + 1 do stack.push conn2 end called = [] stack.stub :current_time, stubbed_time + 2 do stack.reap(0) do |object| called << object end end assert_equal [conn1, conn2], called assert_equal 0, stack.idle end def test_reap_with_multiple_connections_and_idle_seconds_outside_range stack = ConnectionPool::TimedStack.new(2) { Object.new } stubbed_time = Process.clock_gettime(Process::CLOCK_MONOTONIC) conn1 = stack.pop conn2 = stack.pop stack.stub :current_time, stubbed_time do stack.push conn1 end stack.stub :current_time, stubbed_time + 1 do stack.push conn2 end called = [] stack.stub :current_time, stubbed_time + 2 do stack.reap(3) do |object| called << object end end assert_empty called assert_equal 2, stack.idle end def test_reap_does_not_loop_continuously stack = ConnectionPool::TimedStack.new(2) { Object.new } stack.push(Object.new) stack.push(Object.new) close_attempts = 0 stack.reap(0) do |conn| if close_attempts >= 2 flunk "Reap is stuck in a loop" end close_attempts += 1 stack.push(conn) end assert_equal 2, close_attempts end end mperham-connection_pool-dcdc2db/test/test_timed_stack_subclassing.rb000066400000000000000000000016721505534747000264270ustar00rootroot00000000000000# frozen_string_literal: true require_relative "helper" class TestTimedStackSubclassing < Minitest::Test def setup @klass = Class.new(ConnectionPool::TimedStack) end def test_try_fetch_connection obj = Object.new stack = @klass.new(1) { obj } assert_equal false, stack.send(:try_fetch_connection) assert_equal obj, stack.pop stack.push obj assert_equal obj, stack.send(:try_fetch_connection) end def test_override_try_fetch_connection obj = Object.new stack = @klass.new(1) { obj } stack.push stack.pop connection_stored_called = "cs_called" stack.define_singleton_method(:connection_stored?) { |*| raise connection_stored_called } e = assert_raises { stack.send(:try_fetch_connection) } assert_equal connection_stored_called, e.message stack.define_singleton_method(:try_fetch_connection) { fetch_connection } assert_equal obj, stack.send(:try_fetch_connection) end end