pax_global_header00006660000000000000000000000064151146161520014514gustar00rootroot0000000000000052 comment=78bc41e2a9fffbdd7242736233aeedfb64953dd8 mperham-connection_pool-0306752/000077500000000000000000000000001511461615200165235ustar00rootroot00000000000000mperham-connection_pool-0306752/.github/000077500000000000000000000000001511461615200200635ustar00rootroot00000000000000mperham-connection_pool-0306752/.github/dependabot.yml000066400000000000000000000001661511461615200227160ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" mperham-connection_pool-0306752/.github/workflows/000077500000000000000000000000001511461615200221205ustar00rootroot00000000000000mperham-connection_pool-0306752/.github/workflows/ci.yml000066400000000000000000000012011511461615200232300ustar00rootroot00000000000000name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest continue-on-error: ${{ matrix.experimental }} strategy: fail-fast: false matrix: ruby: ["3.2", "3.3", "3.4", "4.0", "jruby"] experimental: [false] include: - ruby: "truffleruby" experimental: true steps: - uses: actions/checkout@v6 - 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: 1 run: bundle exec rake test mperham-connection_pool-0306752/.gitignore000066400000000000000000000000411511461615200205060ustar00rootroot00000000000000*.gem .bundle Gemfile.lock pkg/* mperham-connection_pool-0306752/.standard.yml000066400000000000000000000000551511461615200211240ustar00rootroot00000000000000ruby_version: 3.2.0 fix: true parallel: true mperham-connection_pool-0306752/Changes.md000066400000000000000000000115331511461615200204200ustar00rootroot00000000000000# connection_pool Changelog 3.0.2 ------ - Support :name keyword for backwards compatibility [#210] 3.0.1 ------ - Add missing `fork.rb` to gemspec. 3.0.0 ------ - **BREAKING CHANGES** `ConnectionPool` and `ConnectionPool::TimedStack` now use keyword arguments rather than positional arguments everywhere. Expected impact is minimal as most people use the `with` API, which is unchanged. ```ruby pool = ConnectionPool.new(size: 5, timeout: 5) pool.checkout(1) # 2.x pool.reap(30) # 2.x pool.checkout(timeout: 1) # 3.x pool.reap(idle_seconds: 30) # 3.x ``` - Dropped support for Ruby <3.2.0 2.5.5 ------ - Support `ConnectionPool::TimedStack#pop(exception: false)` [#207] to avoid using exceptions as control flow. 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-0306752/Gemfile000066400000000000000000000002011511461615200200070ustar00rootroot00000000000000source "https://rubygems.org" gemspec gem "standard" gem "benchmark-ips" group :test do gem "maxitest" gem "simplecov" end mperham-connection_pool-0306752/LICENSE000066400000000000000000000020371511461615200175320ustar00rootroot00000000000000Copyright (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-0306752/README.md000066400000000000000000000126161511461615200200100ustar00rootroot00000000000000connection\_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: ``` ruby $memcached.with(timeout: 2.0) do |conn| conn.get('some-count') end ``` This will only modify the 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. ## 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 remove `::Wrapper` and use `ConnectionPool` directly. ## 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 if it is necessary to close all existing connections and continue to use the pool. ConnectionPool will automatically reload if the process is forked. Use `auto_reload_after_fork: false` if you don't want this behavior. ```ruby cp = ConnectionPool.new(auto_reload_after_fork: false) { Redis.new } cp.reload { |conn| conn.quit } # reload manually cp.with { |conn| conn.get('some-count') } ``` Like `shutdown`, `reload` will block until all connections are checked in and closed. ## Reap You can call `reap` periodically on the ConnectionPool instance to close connections that were created but have not been used for a certain amount of time. This can be useful in environments where connections are expensive. You can specify how many seconds the connections have to be idle for them to be reaped, defaulting to 60 seconds. ```ruby cp = ConnectionPool.new { Redis.new } # Start a reaper thread to reap connections that have been # idle more than 300 seconds (5 minutes) Thread.new do loop do cp.reap(idle_seconds: 300, &:close) sleep 30 end end ``` ## Discarding Connections You can discard connections in the ConnectionPool instance to remove connections that are broken and can't be repaired. It can only be done inside the block passed to `with`. 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(&:close) # 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 ``` ## Upgrading from ConnectionPool 2 * Support for Ruby <3.2 has been removed. * ConnectionPool's APIs now consistently use keyword arguments everywhere. Positional arguments must be converted to keywords: ```ruby pool = ConnectionPool.new(size: 5, timeout: 5) pool.checkout(1) # 2.x pool.reap(30) # 2.x pool.checkout(timeout: 1) # 3.x pool.reap(idle_seconds: 30) # 3.x ``` ## Notes - Connections are lazily created as needed. - **WARNING**: Avoid `Timeout.timeout` in your Ruby code or you can see occasional silent corruption and mysterious errors. The Timeout API is unsafe and dangerous to use. Use proper socket timeout options as exposed by Net::HTTP, Redis, Dalli, etc. ## Author Mike Perham, [@getajobmike](https://ruby.social/@getajobmike), mperham-connection_pool-0306752/Rakefile000066400000000000000000000003001511461615200201610ustar00rootroot00000000000000require "bundler/gem_tasks" require "standard/rake" require "rake/testtask" Rake::TestTask.new task default: [:"standard:fix", :test] task :bench do require_relative "test/benchmarks" end mperham-connection_pool-0306752/connection_pool.gemspec000066400000000000000000000025031511461615200232600ustar00rootroot00000000000000require "./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/fork.rb", "lib/connection_pool/wrapper.rb"] s.executables = [] s.require_paths = ["lib"] s.license = "MIT" s.required_ruby_version = ">= 3.2.0" s.add_development_dependency "bundler" s.add_development_dependency "maxitest" s.add_development_dependency "rake" s.metadata = { "bug_tracker_uri" => "https://github.com/mperham/connection_pool/issues", "documentation_uri" => "https://github.com/mperham/connection_pool/wiki", "changelog_uri" => "https://github.com/mperham/connection_pool/blob/main/Changes.md", "source_code_uri" => "https://github.com/mperham/connection_pool", "homepage_uri" => "https://github.com/mperham/connection_pool", "rubygems_mfa_required" => "true" } end mperham-connection_pool-0306752/lib/000077500000000000000000000000001511461615200172715ustar00rootroot00000000000000mperham-connection_pool-0306752/lib/connection_pool.rb000066400000000000000000000125371511461615200230160ustar00rootroot00000000000000require "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 def self.wrap(**, &) Wrapper.new(**, &) end attr_reader :size def initialize(timeout: 5, size: 5, auto_reload_after_fork: true, name: nil, &) raise ArgumentError, "Connection pool requires a block" unless block_given? @size = Integer(size) @timeout = Float(timeout) @available = TimedStack.new(size: @size, &) @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(**) # We need to manage exception handling manually here in order # to work correctly with `Timeout.timeout` and `Thread#raise`. # Otherwise an interrupted Thread can leak connections. Thread.handle_interrupt(Exception => :never) do conn = checkout(**) 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(timeout: @timeout, **) if ::Thread.current[@key] ::Thread.current[@key_count] += 1 ::Thread.current[@key] else conn = @available.pop(timeout:, **) ::Thread.current[@key] = conn ::Thread.current[@key_count] = 1 conn 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(&) @available.shutdown(&) 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(&) @available.shutdown(reload: true, &) end ## Reaps idle connections that have been idle for over +idle_seconds+. # +idle_seconds+ defaults to 60. def reap(idle_seconds: 60, &) @available.reap(idle_seconds:, &) end # 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" require_relative "connection_pool/fork" mperham-connection_pool-0306752/lib/connection_pool/000077500000000000000000000000001511461615200224615ustar00rootroot00000000000000mperham-connection_pool-0306752/lib/connection_pool/fork.rb000066400000000000000000000017501511461615200237520ustar00rootroot00000000000000class ConnectionPool if Process.respond_to?(:fork) INSTANCES = ObjectSpace::WeakMap.new private_constant :INSTANCES def self.after_fork INSTANCES.each_value do |pool| # We're in after_fork, so we know all other threads are dead. # All we need to do is 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 module ForkTracker def _fork pid = super if pid == 0 ConnectionPool.after_fork end pid end end Process.singleton_class.prepend(ForkTracker) else # JRuby, et al INSTANCES = nil private_constant :INSTANCES def self.after_fork # noop end end end mperham-connection_pool-0306752/lib/connection_pool/timed_stack.rb000066400000000000000000000147461511461615200253110ustar00rootroot00000000000000## # 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(size: 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. Additional kwargs are ignored in TimedStack but may be # used by subclasses that extend TimedStack. def push(obj, **) @mutex.synchronize do if @shutdown_block @created -= 1 unless @created == 0 @shutdown_block.call(obj) else store_connection obj, ** 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. # # @option options [Float] :timeout (0.5) Wait this many seconds for an available entry # @option options [Class] :exception (ConnectionPool::TimeoutError) Exception class to raise # if an entry was not available within the timeout period. Use `exception: false` to return nil. # # Other options may be used by subclasses that extend TimedStack. def pop(timeout: 0.5, exception: ConnectionPool::TimeoutError, **) deadline = current_time + timeout @mutex.synchronize do loop do raise ConnectionPool::PoolShuttingDownError if @shutdown_block if (conn = try_fetch_connection(**)) return conn end connection = try_create(**) return connection if connection to_wait = deadline - current_time if to_wait <= 0 if exception raise exception, "Waited #{timeout} sec, #{length}/#{@max} available" else return nil end end @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:) raise ArgumentError, "reap must receive a block" unless block_given? raise ArgumentError, "idle_seconds must be a number" unless idle_seconds.is_a?(Numeric) raise ConnectionPool::PoolShuttingDownError if @shutdown_block count = idle count.times do conn = @mutex.synchronize do raise ConnectionPool::PoolShuttingDownError if @shutdown_block reserve_idle_connection(idle_seconds) end break unless conn yield 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(**) connection_stored?(**) && fetch_connection(**) 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?(**) !@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(**) @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(**) while (conn = try_fetch_connection(**)) @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 # Most active elements are at the tail of the array. # Most idle will be at the head so `shift` rather than `pop`. @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) return unless connection_stored? # Most idle will be at the head so `first` age = (current_time - @que.first.last) age > 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, **) @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(**) unless @created == @max object = @create_block.call @created += 1 object end end end mperham-connection_pool-0306752/lib/connection_pool/version.rb000066400000000000000000000000551511461615200244730ustar00rootroot00000000000000class ConnectionPool VERSION = "3.0.2" end mperham-connection_pool-0306752/lib/connection_pool/wrapper.rb000066400000000000000000000014501511461615200244660ustar00rootroot00000000000000class ConnectionPool class Wrapper < ::BasicObject METHODS = [:with, :pool_shutdown, :wrapped_pool] def initialize(**options, &) @pool = options.fetch(:pool) { ::ConnectionPool.new(**options, &) } end def wrapped_pool @pool end def with(**, &) @pool.with(**, &) end def pool_shutdown(&) @pool.shutdown(&) end def pool_size @pool.size end def pool_available @pool.available end def respond_to?(id, *, **) METHODS.include?(id) || with { |c| c.respond_to?(id, *, **) } end def respond_to_missing?(id, *, **) with { |c| c.respond_to?(id, *, **) } end def method_missing(name, *, **, &) with do |connection| connection.send(name, *, **, &) end end end end mperham-connection_pool-0306752/test/000077500000000000000000000000001511461615200175025ustar00rootroot00000000000000mperham-connection_pool-0306752/test/benchmarks.rb000066400000000000000000000004121511461615200221410ustar00rootroot00000000000000# bundle exec ruby test/benchmarks.rb require "benchmark/ips" require "connection_pool" puts "ConnectionPool #{ConnectionPool::VERSION}" CP = ConnectionPool.new { Object.new } Benchmark.ips do |x| x.report("ConnectionPool#with") do CP.with { |x| } end end mperham-connection_pool-0306752/test/helper.rb000066400000000000000000000014311511461615200213050ustar00rootroot00000000000000require "bundler/setup" Bundler.require(:default, :test) require "minitest/pride" require "maxitest/autorun" require "maxitest/threads" # require "maxitest/timeout" # Maxitest.timeout = 0.5 # $VERBOSE = 1 # $TESTING = true # disable minitest/parallel threads # ENV["MT_CPU"] = "0" # ENV["N"] = "0" # Disable any stupid backtrace cleansers # ENV["BACKTRACE"] = "1" if ENV["COVERAGE"] require "simplecov" SimpleCov.start do enable_coverage :branch add_filter "/test/" minimum_coverage 90 end end require_relative "../lib/connection_pool" class ConnectionPool def self.reset_instances silence_warnings do const_set(:INSTANCES, ObjectSpace::WeakMap.new) end end end def silence_warnings old, $VERBOSE = $VERBOSE, nil yield ensure $VERBOSE = old end mperham-connection_pool-0306752/test/test_connection_pool.rb000066400000000000000000000462471511461615200242730ustar00rootroot00000000000000require_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.02 def initialize @x = 0 end def pass Thread.pass end def do_something(*_args, increment: 1) @x += increment sleep 0.02 @x end def do_something_with_positional_hash(options) @x += options[:increment] || 1 pass @x end def fast @x += 1 end def do_something_with_block @x += yield pass @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(&:kill) threads.each(&:join) end def test_basic_multithreaded_usage pool_size = 5 pool = ConnectionPool.new(size: pool_size) { NetworkConnection.new } generations = 3 result = Array.new(pool_size * generations) { Thread.new do pool.with do |net| net.do_something end end }.map(&:value) assert_equal((1..generations).cycle(pool_size).sort, result.sort) 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 # Timeout.timeout creates a watcher thread and does not provide a way to # shut it down so we have to disable maxitest's extra thread paranoia or # else it will trigger a test failure. skip_maxitest_extra_threads end def skip_maxitest_extra_threads @maxitest_threads_before = Thread.list 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, timeout: 0.01) { {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 %w[shutdown shutdown shutdown], recorders.map { |r| r.calls }.flatten end def test_checkout_after_reload_cannot_create_new_connections_beyond_size pool = ConnectionPool.new(size: 1, name: "bob") { 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(idle_seconds: 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(idle_seconds: 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(idle_seconds: 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(idle_seconds: 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 assert_raises Timeout::Error do wrapper.with(timeout: 0.1) { 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_pool_usage begin Ractor rescue NameError skip("Ractor not available") end silence_warnings do obj = "mike" r = Ractor.new(obj) do |copy| # verify we can create a pool in a Ractor and that we can pool = ConnectionPool.new(auto_reload_after_fork: false) { copy } checkedout = nil pool.with { |y| checkedout = y } checkedout end result = (RUBY_VERSION < "4") ? r.take : r.value assert_equal obj, result # same string but different String instance refute_equal obj.object_id, result.object_id # was copied across Ractor boundary end end end mperham-connection_pool-0306752/test/test_connection_pool_timed_stack.rb000066400000000000000000000205041511461615200266260ustar00rootroot00000000000000require_relative "helper" class TestConnectionPoolTimedStack < Minitest::Test def setup @stack = ConnectionPool::TimedStack.new { Object.new } end def test_empty_eh stack = ConnectionPool::TimedStack.new(size: 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(size: 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(size: 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(size: 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(size: 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 assert_nil @stack.pop(timeout: 0, exception: false) end def test_pop_empty_custom_exception e = assert_raises(RuntimeError) { @stack.pop(timeout: 0, exception: RuntimeError) } assert_equal "Waited 0 sec, 0/0 available", e.message end def test_pop_full stack = ConnectionPool::TimedStack.new(size: 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(size: 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(timeout: 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(size: 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(size: 1) { Object.new } stack.pop stack.shutdown(reload: true) {} assert_raises ConnectionPool::TimeoutError do stack.pop(timeout: 0) end end def test_push stack = ConnectionPool::TimedStack.new(size: 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(idle_seconds: 0, &reap_proc) end assert_equal 1, called.size @stack.reap(idle_seconds: 0, &reap_proc) assert_equal 3, called.size end def test_reap @stack.push Object.new called = [] @stack.reap(idle_seconds: 0) do |object| called << object end refute_empty called assert_empty @stack end def test_reap_full_stack stack = ConnectionPool::TimedStack.new(size: 1) { Object.new } stack.push stack.pop stack.reap(idle_seconds: 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(idle_seconds: 100) do |object| called << object end assert_empty called refute_empty @stack end def test_reap_no_block assert_raises(ArgumentError) do @stack.reap(idle_seconds: 0) end end def test_reap_non_numeric_idle_seconds assert_raises(ArgumentError) do @stack.reap(idle_seconds: "0") { |object| object } end end def test_reap_with_multiple_connections stack = ConnectionPool::TimedStack.new(size: 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(idle_seconds: 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(size: 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(idle_seconds: 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(size: 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(idle_seconds: 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(size: 2) { Object.new } stack.push(Object.new) stack.push(Object.new) close_attempts = 0 stack.reap(idle_seconds: 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-0306752/test/test_timed_stack_subclassing.rb000066400000000000000000000017061511461615200257560ustar00rootroot00000000000000# 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(size: 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(size: 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