pax_global_header00006660000000000000000000000064144317353550014524gustar00rootroot0000000000000052 comment=4cc8a8de14a82a236a29b59146477072a04203c7 mperham-connection_pool-8e3743b/000077500000000000000000000000001443173535500167045ustar00rootroot00000000000000mperham-connection_pool-8e3743b/.github/000077500000000000000000000000001443173535500202445ustar00rootroot00000000000000mperham-connection_pool-8e3743b/.github/dependabot.yml000066400000000000000000000001661443173535500230770ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" mperham-connection_pool-8e3743b/.github/workflows/000077500000000000000000000000001443173535500223015ustar00rootroot00000000000000mperham-connection_pool-8e3743b/.github/workflows/ci.yml000066400000000000000000000011231443173535500234140ustar00rootroot00000000000000name: 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", "jruby"] experimental: [false] include: - ruby: "truffleruby" experimental: true steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: ${{matrix.ruby}} bundler-cache: true - name: Run tests timeout-minutes: 5 run: ${{matrix.env}} bundle exec rake mperham-connection_pool-8e3743b/.gitignore000066400000000000000000000000411443173535500206670ustar00rootroot00000000000000*.gem .bundle Gemfile.lock pkg/* mperham-connection_pool-8e3743b/.standard.yml000066400000000000000000000000551443173535500213050ustar00rootroot00000000000000ruby_version: 2.5.0 fix: true parallel: true mperham-connection_pool-8e3743b/Changes.md000066400000000000000000000071401443173535500206000ustar00rootroot00000000000000# connection_pool Changelog 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-8e3743b/Gemfile000066400000000000000000000001611443173535500201750ustar00rootroot00000000000000source "https://rubygems.org" gemspec(development_group: :runtime) gem "standard", group: [:development, :test] mperham-connection_pool-8e3743b/LICENSE000066400000000000000000000020371443173535500177130ustar00rootroot00000000000000Copyright (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-8e3743b/README.md000066400000000000000000000103001443173535500201550ustar00rootroot00000000000000connection\_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. ## 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.with do |conn| cp.size # => 10 cp.available # => 9 end ``` 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-8e3743b/Rakefile000066400000000000000000000002071443173535500203500ustar00rootroot00000000000000require "bundler/gem_tasks" require "standard/rake" require "rake/testtask" Rake::TestTask.new task default: [:"standard:fix", :test] mperham-connection_pool-8e3743b/connection_pool.gemspec000066400000000000000000000017621443173535500234470ustar00rootroot00000000000000require "./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-8e3743b/lib/000077500000000000000000000000001443173535500174525ustar00rootroot00000000000000mperham-connection_pool-8e3743b/lib/connection_pool.rb000066400000000000000000000113441443173535500231720ustar00rootroot00000000000000require "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} 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" INSTANCES[self] = self if 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 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) end end def checkin(force: false) if ::Thread.current[@key] if ::Thread.current[@key_count] == 1 || force @available.push(::Thread.current[@key]) ::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 # 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 end require_relative "connection_pool/timed_stack" require_relative "connection_pool/wrapper" mperham-connection_pool-8e3743b/lib/connection_pool/000077500000000000000000000000001443173535500226425ustar00rootroot00000000000000mperham-connection_pool-8e3743b/lib/connection_pool/timed_stack.rb000066400000000000000000000105051443173535500254570ustar00rootroot00000000000000## # 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 @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 return fetch_connection(options) if connection_stored?(options) 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 ## # 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 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 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 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 connection_stored?(options) conn = fetch_connection(options) @shutdown_block.call(conn) end @created = 0 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 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-8e3743b/lib/connection_pool/version.rb000066400000000000000000000000551443173535500246540ustar00rootroot00000000000000class ConnectionPool VERSION = "2.4.1" end mperham-connection_pool-8e3743b/lib/connection_pool/wrapper.rb000066400000000000000000000024451443173535500246540ustar00rootroot00000000000000class 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-8e3743b/test/000077500000000000000000000000001443173535500176635ustar00rootroot00000000000000mperham-connection_pool-8e3743b/test/helper.rb000066400000000000000000000001751443173535500214720ustar00rootroot00000000000000gem "minitest" require "minitest/pride" require "minitest/autorun" $VERBOSE = 1 require_relative "../lib/connection_pool" mperham-connection_pool-8e3743b/test/test_connection_pool.rb000066400000000000000000000334671443173535500244540ustar00rootroot00000000000000require_relative "helper" class TestConnectionPool < Minitest::Test 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(method_id, *args) 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_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_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_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_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_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) { 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) { 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) { 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 end mperham-connection_pool-8e3743b/test/test_connection_pool_timed_stack.rb000066400000000000000000000051461443173535500270140ustar00rootroot00000000000000require_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_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_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_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 end