├── .rspec ├── lib ├── pond │ └── version.rb └── pond.rb ├── Gemfile ├── spec ├── spec_helper.rb ├── wrapper_spec.rb ├── config_spec.rb └── checkout_spec.rb ├── .gitignore ├── Rakefile ├── CHANGELOG.md ├── tasks └── stress.rake ├── pond.gemspec ├── LICENSE.txt └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --order random 3 | -------------------------------------------------------------------------------- /lib/pond/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Pond 4 | VERSION = '0.5.0' 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in pond.gemspec 4 | gemspec 5 | 6 | gem 'rubysl', '~> 2.0', :platform => :rbx 7 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pond' 4 | 5 | RSpec.configure do |config| 6 | config.expect_with(:rspec) { |c| c.syntax = [:expect, :should] } 7 | end 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | 3 | require 'rspec/core/rake_task' 4 | 5 | RSpec::Core::RakeTask.new :default do |spec| 6 | spec.pattern = './spec/**/*_spec.rb' 7 | end 8 | 9 | Dir[File.dirname(__FILE__) + '/tasks/**/*.rake'].sort.each &method(:load) 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### 0.5.0 (2020-09-04) 2 | 3 | * Silence warnings on Ruby 2.7 (boof) 4 | 5 | ### 0.4.0 (2019-12-31) 6 | 7 | * Add support for Ruby 2.7. (gekola) 8 | 9 | ### 0.3.0 (2018-03-27) 10 | 11 | * Support scoped checkouts. 12 | 13 | * Use frozen string literals. 14 | 15 | ### 0.2.0 (2016-02-05) 16 | 17 | * Add an option for a detach_if callable, which can contain logic to 18 | determine whether to remove an object from the pool. 19 | 20 | ### 0.1.0 (2014-02-14) 21 | 22 | * Initial release. 23 | -------------------------------------------------------------------------------- /tasks/stress.rake: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'pond' 4 | 5 | desc "Stress test the Pond gem to check for concurrency issues." 6 | task :stress do 7 | detach_if = proc do |obj| 8 | raise "Bad Detach!" if rand < 0.05 9 | obj != "Good!" 10 | end 11 | 12 | pond = Pond.new(detach_if: detach_if) do 13 | raise "Bad Instantiation!" if rand < 0.05 14 | "Good!".dup 15 | end 16 | 17 | threads = 18 | 20.times.map do 19 | Thread.new do 20 | 10_000.times do 21 | begin 22 | pond.checkout do |o| 23 | raise "Uh-oh!" unless o == "Good!" 24 | o.replace "Bad!" if rand < 0.05 25 | end 26 | rescue => e 27 | raise e unless ["Bad Detach!", "Bad Instantiation!"].include?(e.message) 28 | end 29 | end 30 | end 31 | end 32 | 33 | threads.each(&:join) 34 | 35 | puts "Stress test succeeded!" 36 | end 37 | -------------------------------------------------------------------------------- /pond.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'pond/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = 'pond' 8 | spec.version = Pond::VERSION 9 | spec.authors = ["Chris Hanks"] 10 | spec.email = ["christopher.m.hanks@gmail.com"] 11 | spec.description = %q{A simple, generic, thread-safe pool for connections or whatever else} 12 | spec.summary = %q{A simple, generic, thread-safe pool} 13 | spec.homepage = 'https://github.com/chanks/pond' 14 | spec.license = 'MIT' 15 | 16 | spec.files = `git ls-files`.split($/) 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ['lib'] 20 | 21 | spec.add_development_dependency 'bundler', '>= 1.3' 22 | spec.add_development_dependency 'rspec', '>= 2.14' 23 | spec.add_development_dependency 'rake' 24 | end 25 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Chris Hanks 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /spec/wrapper_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Pond::Wrapper do 6 | class Wrapped 7 | # JRuby implements BasicObject#object_id, so we need a minor workaround. 8 | def id 9 | object_id 10 | end 11 | 12 | def pipelined(&block) 13 | yield 14 | end 15 | end 16 | 17 | before do 18 | @wrapper = Pond.wrap { Wrapped.new } 19 | @pond = @wrapper.pond 20 | end 21 | 22 | it "should proxy method calls to checked out objects" do 23 | @pond.size.should == 0 24 | 25 | @wrapper.class.should == Wrapped 26 | @wrapper.respond_to?(:pipelined).should == true 27 | id = @wrapper.id 28 | 29 | @pond.size.should == 1 30 | @pond.allocated.should == {nil => {}} 31 | @pond.available.map(&:id).should == [id] 32 | end 33 | 34 | it "should return the same object within a block passed to one of its methods" do 35 | q1, q2 = Queue.new, Queue.new 36 | id1, id2 = nil, nil 37 | 38 | @wrapper.pipelined do 39 | id1 = @wrapper.id 40 | 41 | t = Thread.new do 42 | @wrapper.pipelined do 43 | q1.push nil 44 | q2.pop 45 | 46 | id2 = @wrapper.id 47 | id2.should == @wrapper.id 48 | @wrapper 49 | end 50 | end 51 | 52 | q1.pop 53 | 54 | @wrapper.id.should == id1 55 | 56 | @pond.allocated[nil].keys.should == [Thread.current, t] 57 | @pond.available.should == [] 58 | 59 | q2.push nil 60 | t.join 61 | 62 | @wrapper.id.should == id1 63 | @wrapper.id.should == id1 64 | end 65 | 66 | @pond.allocated.should == {nil => {}} 67 | @pond.available.map(&:id).should == [id2, id1] 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /spec/config_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Pond, "configuration" do 6 | it "should eagerly instantiate objects if the option is given" do 7 | int = 0 8 | pond = Pond.new(:eager => true){int += 1} 9 | pond.available.should == (1..10).to_a 10 | end 11 | 12 | it "should have its collection type gettable and settable" do 13 | pond = Pond.new { Object.new } 14 | pond.collection.should == :queue 15 | pond.collection = :stack 16 | pond.collection.should == :stack 17 | 18 | pond = Pond.new(:collection => :stack) { Object.new } 19 | pond.collection.should == :stack 20 | pond.collection = :queue 21 | pond.collection.should == :queue 22 | 23 | procs = [ 24 | proc{pond.collection = nil}, 25 | proc{Pond.new(:collection => nil) { Object.new }}, 26 | proc{pond.collection = :blah}, 27 | proc{Pond.new(:collection => :blah) { Object.new }} 28 | ] 29 | 30 | procs.each { |p| p.should raise_error RuntimeError, /Bad value for Pond collection:/ } 31 | end 32 | 33 | it "should have its timeout gettable and settable" do 34 | pond = Pond.new { Object.new } 35 | pond.timeout.should == 1 36 | pond.timeout = 4 37 | pond.timeout.should == 4 38 | 39 | pond = Pond.new(:timeout => 3.7) { Object.new } 40 | pond.timeout.should == 3.7 41 | pond.timeout = 1.9 42 | pond.timeout.should == 1.9 43 | 44 | procs = [ 45 | proc{pond.timeout = nil}, 46 | proc{Pond.new(:timeout => nil) { Object.new }}, 47 | proc{pond.timeout = :blah}, 48 | proc{Pond.new(:timeout => :blah) { Object.new }} 49 | ] 50 | 51 | procs.each { |p| p.should raise_error RuntimeError, /Bad value for Pond timeout:/ } 52 | end 53 | 54 | it "should have its maximum_size gettable and settable" do 55 | pond = Pond.new { Object.new } 56 | pond.maximum_size.should == 10 57 | pond.maximum_size = 7 58 | pond.maximum_size.should == 7 59 | pond.maximum_size = 0 60 | pond.maximum_size.should == 0 61 | pond.maximum_size = 2 62 | pond.maximum_size.should == 2 63 | 64 | procs = [ 65 | proc{pond.maximum_size = nil}, 66 | proc{Pond.new(:maximum_size => nil) { Object.new }}, 67 | proc{pond.maximum_size = :blah}, 68 | proc{Pond.new(:maximum_size => :blah) { Object.new }}, 69 | proc{pond.maximum_size = 4.0}, 70 | proc{Pond.new(:maximum_size => 4.0) { Object.new }}, 71 | ] 72 | 73 | procs.each { |p| p.should raise_error RuntimeError, /Bad value for Pond maximum_size:/ } 74 | end 75 | 76 | it "when the maximum_size is decreased should free available objects" do 77 | int = 0 78 | pond = Pond.new(:eager => true) { int += 1 } 79 | 80 | pond.available.should == (1..10).to_a 81 | pond.maximum_size = 8 82 | pond.available.should == (3..10).to_a 83 | pond.maximum_size = 10 84 | pond.available.should == (3..10).to_a 85 | pond.maximum_size = 9 86 | pond.available.should == (3..10).to_a 87 | end 88 | 89 | it "when the maximum_size is decreased should free available objects and checked-out objects upon return" do 90 | int = 0 91 | pond = Pond.new(:eager => true, :maximum_size => 2) { int += 1 } 92 | pond.available.should == [1, 2] 93 | 94 | q1, q2 = Queue.new, Queue.new 95 | t = Thread.new do 96 | pond.checkout do |i| 97 | i.should == 1 98 | q1.push nil 99 | q2.pop 100 | end 101 | end 102 | 103 | q1.pop 104 | 105 | pond.maximum_size = 0 106 | pond.maximum_size.should == 0 107 | 108 | pond.size.should == 1 109 | pond.available.should == [] 110 | pond.allocated.should == {nil => {t => 1}} 111 | 112 | q2.push nil 113 | t.join 114 | 115 | pond.size.should == 0 116 | pond.available.should == [] 117 | pond.allocated.should == {nil => {}} 118 | end 119 | end 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pond 2 | 3 | Pond is a gem that offers thread-safe object pooling. It can wrap anything 4 | that is costly to instantiate, but is usually used for connections. It is 5 | intentionally very similar to the `connection_pool` gem, but is intended to be 6 | more efficient and flexible. It instantiates objects lazily by default, which 7 | is important for things with high overhead like Postgres connections. It can 8 | also be dynamically resized, and does not block on object instantiation. 9 | 10 | Also, it was pretty fun to write. 11 | 12 | ## Installation 13 | 14 | Add this line to your application's Gemfile: 15 | 16 | gem 'pond' 17 | 18 | And then execute: 19 | 20 | $ bundle 21 | 22 | Or install it yourself as: 23 | 24 | $ gem install pond 25 | 26 | ## Usage 27 | 28 | ```ruby 29 | require 'pond' 30 | require 'redis' 31 | 32 | $redis_pond = Pond.new(:maximum_size => 5, :timeout => 0.5) { Redis.new } 33 | 34 | # No connections are established until we need one: 35 | $redis_pond.checkout do |redis| 36 | redis.incr 'my_counter' 37 | redis.lpush 'my_list', 'item' 38 | end 39 | 40 | # Alternatively, wrap it: 41 | $redis = Pond.wrap(:maximum_size => 5, :timeout => 0.5) { Redis.new } 42 | 43 | # You can now use $redis as you normally would. 44 | $redis.incr 'my_counter' 45 | $redis.lpush 'my_list', 'item' 46 | 47 | $redis.pipelined do 48 | # All these commands go to the same Redis connection, and so are pipelined correctly. 49 | $redis.incr 'my_counter' 50 | $redis.lpush 'my_list', 'item' 51 | end 52 | ``` 53 | 54 | Options: 55 | 56 | * :maximum_size - The maximum number of objects you want the pool to contain. 57 | The default is 10. 58 | * :timeout - When attempting to check out an object but none are available, 59 | how many seconds to wait before raising a `Pond::Timeout` error. The 60 | default is 1. Integers or floats are both accepted. 61 | * :collection - How to manage the objects in the pool. The default is :queue, 62 | meaning that pond.checkout will yield the object that hasn't been used in 63 | the longest period of time. This is to prevent connections from becoming 64 | 'stale'. The alternative is :stack, so checkout will yield the object that 65 | has most recently been returned to the pool. This would be preferable if 66 | you're using connections that have their own logic for becoming idle in 67 | periods of low activity. 68 | * :eager - Set this to true to fill the pool with instantiated objects (up to 69 | the maximum size) when it is created, similar to how the `connection_pool` 70 | gem works. 71 | * :detach_if - Set this to a callable object that can determine whether 72 | objects should be returned to the pool or not. See the following example for 73 | more information. 74 | 75 | ### Detaching Objects 76 | 77 | Sometimes objects in the pool outlive their usefulness (connections may fail) 78 | and it becomes necessary to remove them. Pond's detach_if option is useful for 79 | this - you can pass it any callable object, and Pond will pass it objects from 80 | the pool that have been checked out before they are checked back in. For 81 | example, when using Pond with PostgreSQL connections: 82 | 83 | ```ruby 84 | require 'pond' 85 | require 'pg' 86 | 87 | $pg_pond = Pond.new(:detach_if => lambda {|c| c.finished?}) do 88 | PG.connect(:dbname => "pond_test") 89 | end 90 | ``` 91 | 92 | Now, after a PostgreSQL connection has been used, but before it is returned to 93 | the pool, it will be passed to that lambda to see if it should be detached or 94 | not. If the lambda returns truthy, the connection will be detached (and made 95 | available for garbage collection), and a new one will be instantiated to 96 | replace it as necessary (until the pool returns to its maximum size). 97 | 98 | ## Contributing 99 | 100 | I don't plan on adding too many more features to Pond, since I want to keep 101 | its design simple. If there's something you'd like to see it do, open an issue 102 | so we can discuss it before going to the trouble of creating a pull request. 103 | -------------------------------------------------------------------------------- /lib/pond.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'monitor' 4 | 5 | require 'pond/version' 6 | 7 | class Pond 8 | class Timeout < StandardError; end 9 | 10 | attr_reader :allocated, :available, :timeout, :collection, :maximum_size, :detach_if 11 | 12 | DEFAULT_DETACH_IF = lambda { |_| false } 13 | 14 | def initialize( 15 | maximum_size: 10, 16 | eager: false, 17 | timeout: 1, 18 | collection: :queue, 19 | detach_if: DEFAULT_DETACH_IF, 20 | &block 21 | ) 22 | @block = block 23 | @monitor = Monitor.new 24 | @cv = MonitorMixin::ConditionVariable.new(@monitor) 25 | 26 | @allocated = {} 27 | @available = Array.new(eager ? maximum_size : 0, &block) 28 | 29 | self.timeout = timeout 30 | self.collection = collection 31 | self.detach_if = detach_if 32 | self.maximum_size = maximum_size 33 | end 34 | 35 | def checkout(scope: nil, &block) 36 | raise "Can't checkout with a non-frozen scope" unless scope.frozen? 37 | 38 | if object = current_object(scope: scope) 39 | yield object 40 | else 41 | checkout_object(scope: scope, &block) 42 | end 43 | end 44 | 45 | def size 46 | sync { @allocated.inject(@available.size){|sum, (h, k)| sum + k.length} } 47 | end 48 | 49 | def timeout=(timeout) 50 | raise "Bad value for Pond timeout: #{timeout.inspect}" unless Numeric === timeout && timeout >= 0 51 | sync { @timeout = timeout } 52 | end 53 | 54 | def collection=(type) 55 | raise "Bad value for Pond collection: #{type.inspect}" unless [:stack, :queue].include?(type) 56 | sync { @collection = type } 57 | end 58 | 59 | def maximum_size=(max) 60 | raise "Bad value for Pond maximum_size: #{max.inspect}" unless Integer === max && max >= 0 61 | sync do 62 | @maximum_size = max 63 | {} until size <= max || pop_object.nil? 64 | end 65 | end 66 | 67 | def detach_if=(callable) 68 | raise "Object given for Pond detach_if must respond to #call" unless callable.respond_to?(:call) 69 | sync { @detach_if = callable } 70 | end 71 | 72 | private 73 | 74 | def checkout_object(scope:) 75 | lock_object(scope: scope) 76 | yield current_object(scope: scope) 77 | ensure 78 | unlock_object(scope: scope) 79 | end 80 | 81 | def lock_object(scope:) 82 | deadline = Time.now + @timeout 83 | 84 | until current_object(scope: scope) 85 | raise Timeout if (time_left = deadline - Time.now) < 0 86 | 87 | sync do 88 | if object = get_object(time_left) 89 | set_current_object(object, scope: scope) 90 | end 91 | end 92 | end 93 | 94 | # We need to protect changes to @allocated and @available with the monitor 95 | # so that #size always returns the correct value. But, we don't want to 96 | # call the instantiation block while we have the lock, since it may take a 97 | # long time to return. So, we set the checked-out object to the block as a 98 | # signal that it needs to be called. 99 | if current_object(scope: scope) == @block 100 | set_current_object(@block.call, scope: scope) 101 | end 102 | end 103 | 104 | def unlock_object(scope:) 105 | object = nil 106 | detach_if = nil 107 | should_return_object = nil 108 | 109 | sync do 110 | object = current_object(scope: scope) 111 | detach_if = self.detach_if 112 | should_return_object = object && object != @block && size <= maximum_size 113 | end 114 | 115 | begin 116 | should_return_object = !detach_if.call(object) if should_return_object 117 | detach_check_finished = true 118 | ensure 119 | sync do 120 | @available << object if detach_check_finished && should_return_object 121 | @allocated[scope].delete(Thread.current) 122 | @cv.signal 123 | end 124 | end 125 | end 126 | 127 | def get_object(timeout) 128 | pop_object || size < maximum_size && @block || @cv.wait(timeout) && false 129 | end 130 | 131 | def pop_object 132 | case collection 133 | when :queue then @available.shift 134 | when :stack then @available.pop 135 | end 136 | end 137 | 138 | def current_object(scope:) 139 | sync { (@allocated[scope] ||= {})[Thread.current] } 140 | end 141 | 142 | def set_current_object(object, scope:) 143 | sync { (@allocated[scope] ||= {})[Thread.current] = object } 144 | end 145 | 146 | def sync(&block) 147 | @monitor.synchronize(&block) 148 | end 149 | 150 | class << self 151 | def wrap(*args, &block) 152 | Wrapper.new(*args, &block) 153 | end 154 | end 155 | 156 | class Wrapper < BasicObject 157 | attr_reader :pond 158 | 159 | def initialize(*args, **kwargs, &block) 160 | @pond = ::Pond.new(*args, **kwargs, &block) 161 | end 162 | 163 | def method_missing(*args, **kwargs, &block) 164 | @pond.checkout { |object| object.public_send(*args, **kwargs, &block) } 165 | end 166 | end 167 | end 168 | -------------------------------------------------------------------------------- /spec/checkout_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'spec_helper' 4 | 5 | describe Pond, "#checkout" do 6 | it "should yield objects specified in the block" do 7 | pond = Pond.new { 1 } 8 | pond.checkout { |i| i.should == 1 } 9 | end 10 | 11 | it "should return the value returned by the block" do 12 | pond = Pond.new { 1 } 13 | value = pond.checkout { |i| 'value' } 14 | value.should == 'value' 15 | end 16 | 17 | it "should instantiate objects when needed" do 18 | int = 0 19 | pond = Pond.new { int += 1 } 20 | 21 | pond.size.should == 0 22 | 23 | pond.checkout do |i| 24 | pond.available.should == [] 25 | pond.allocated.should == {nil => {Thread.current => 1}} 26 | i.should == 1 27 | end 28 | 29 | pond.available.should == [1] 30 | pond.allocated.should == {nil => {}} 31 | pond.size.should == 1 32 | 33 | pond.checkout do |i| 34 | pond.available.should == [] 35 | pond.allocated.should == {nil => {Thread.current => 1}} 36 | i.should == 1 37 | end 38 | 39 | pond.available.should == [1] 40 | pond.allocated.should == {nil => {}} 41 | pond.size.should == 1 42 | end 43 | 44 | it "should checkout objects to the given scope, if any" do 45 | int = 0 46 | pond = Pond.new(eager: true) { int += 1 } 47 | pond.size.should == 10 48 | pond.available.should == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 49 | 50 | pond.checkout do |a| 51 | pond.checkout do |b| 52 | pond.checkout(scope: :blah) do |c| 53 | pond.checkout(scope: :blah) do |d| 54 | pond.checkout do |e| 55 | [a, b, c, d, e].should == [1, 1, 2, 2, 1] 56 | end 57 | end 58 | end 59 | end 60 | end 61 | 62 | pond.available.should == [3, 4, 5, 6, 7, 8, 9, 10, 2, 1] 63 | end 64 | 65 | it "should throw an error if the passed scope is not frozen" do 66 | int = 0 67 | pond = Pond.new(eager: true) { int += 1 } 68 | 69 | proc { pond.checkout(scope: String.new) }.should raise_error RuntimeError, "Can't checkout with a non-frozen scope" 70 | end 71 | 72 | it "should not instantiate objects in excess of the specified maximum_size" do 73 | object = nil 74 | pond = Pond.new(:maximum_size => 1) { object = Object.new } 75 | object_ids = [] 76 | 77 | threads = 20.times.map do 78 | pond.checkout do |obj| 79 | object_ids << obj.object_id 80 | end 81 | end 82 | 83 | object_ids.uniq.should == [object.object_id] 84 | end 85 | 86 | it "should give different objects to different threads" do 87 | int = 0 88 | pond = Pond.new { int += 1 } 89 | 90 | q1, q2 = Queue.new, Queue.new 91 | 92 | t = Thread.new do 93 | pond.checkout do |i| 94 | i.should == 1 95 | q1.push nil 96 | q2.pop 97 | end 98 | end 99 | 100 | q1.pop 101 | 102 | pond.size.should == 1 103 | pond.allocated.should == {nil => {t => 1}} 104 | pond.available.should == [] 105 | 106 | pond.checkout { |i| i.should == 2 } 107 | 108 | pond.size.should == 2 109 | pond.allocated.should == {nil => {t => 1}} 110 | pond.available.should == [2] 111 | 112 | q2.push nil 113 | t.join 114 | 115 | pond.allocated.should == {nil => {}} 116 | pond.available.should == [2, 1] 117 | end 118 | 119 | it "should be re-entrant" do 120 | pond = Pond.new { Object.new } 121 | pond.checkout do |obj1| 122 | pond.checkout do |obj2| 123 | obj1.should == obj2 124 | end 125 | end 126 | end 127 | 128 | it "should support a thread checking out objects from distinct Pond instances" do 129 | pond1 = Pond.new { [] } 130 | pond2 = Pond.new { {} } 131 | 132 | pond1.checkout do |one| 133 | pond2.checkout do |two| 134 | one.should == [] 135 | two.should == {} 136 | end 137 | end 138 | end 139 | 140 | it "should yield an object to only one thread when many are waiting" do 141 | pond = Pond.new(:maximum_size => 1, timeout: 360000) { 2 } 142 | 143 | q1, q2, q3 = Queue.new, Queue.new, Queue.new 144 | 145 | threads = 4.times.map do 146 | Thread.new do 147 | Thread.current[:value] = 0 148 | 149 | q1.push nil 150 | 151 | pond.checkout do |o| 152 | Thread.current[:value] = o 153 | q2.push nil 154 | q3.pop 155 | end 156 | end 157 | end 158 | 159 | 4.times { q1.pop } 160 | q2.pop 161 | 162 | threads.map{|t| t[:value]}.sort.should == [0, 0, 0, 2] 163 | 164 | 4.times { q3.push nil } 165 | 166 | threads.each &:join 167 | end 168 | 169 | it "should treat the collection of objects as a queue by default" do 170 | int = 0 171 | pond = Pond.new { int += 1 } 172 | results = [] 173 | 174 | q = Queue.new 175 | m = Mutex.new 176 | cv = ConditionVariable.new 177 | 178 | 4.times do 179 | threads = 4.times.map do 180 | Thread.new do 181 | m.synchronize do 182 | pond.checkout do |i| 183 | results << i 184 | q.push nil 185 | cv.wait(m) 186 | end 187 | cv.signal 188 | end 189 | end 190 | end 191 | 192 | 4.times { q.pop } 193 | cv.signal 194 | threads.each(&:join) 195 | end 196 | 197 | pond.size.should == 4 198 | results.should == (1..4).cycle(4).to_a 199 | end 200 | 201 | it "should treat the collection of objects as a stack if configured that way" do 202 | int = 0 203 | pond = Pond.new(:collection => :stack) { int += 1 } 204 | results = [] 205 | 206 | q = Queue.new 207 | m = Mutex.new 208 | cv = ConditionVariable.new 209 | 210 | 4.times do 211 | threads = 4.times.map do 212 | Thread.new do 213 | m.synchronize do 214 | pond.checkout do |i| 215 | results << i 216 | q.push nil 217 | cv.wait(m) 218 | end 219 | cv.signal 220 | end 221 | end 222 | end 223 | 224 | 4.times { q.pop } 225 | cv.signal 226 | threads.each(&:join) 227 | end 228 | 229 | pond.size.should == 4 230 | results.should == [1, 2, 3, 4, 4, 3, 2, 1, 1, 2, 3, 4, 4, 3, 2, 1] 231 | end 232 | 233 | it "should raise a timeout error if it takes too long to return an object" do 234 | pond = Pond.new(:timeout => 0.01, :maximum_size => 1){1} 235 | 236 | q1, q2 = Queue.new, Queue.new 237 | t = Thread.new do 238 | pond.checkout do 239 | q1.push nil 240 | q2.pop 241 | end 242 | end 243 | 244 | q1.pop 245 | 246 | proc{pond.checkout{}}.should raise_error Pond::Timeout 247 | 248 | q2.push nil 249 | t.join 250 | end 251 | 252 | it "with a block that raises an error should check the object back in and propagate the error" do 253 | pond = Pond.new { 1 } 254 | proc do 255 | pond.checkout do 256 | raise "Blah!" 257 | end 258 | end.should raise_error RuntimeError, "Blah!" 259 | 260 | pond.allocated.should == {nil => {}} 261 | pond.available.should == [1] 262 | end 263 | 264 | it "should not block other threads if the object instantiation takes a long time" do 265 | t = nil 266 | q1, q2 = Queue.new, Queue.new 267 | pond = Pond.new do 268 | q1.push nil 269 | q2.pop 270 | end 271 | 272 | q2.push 1 273 | 274 | pond.checkout do |i| 275 | q1.pop 276 | i.should == 1 277 | 278 | t = Thread.new do 279 | pond.checkout do |i| 280 | i.should == 2 281 | end 282 | end 283 | 284 | q1.pop 285 | end 286 | 287 | pond.checkout { |i| i.should == 1 } 288 | 289 | q2.push 2 290 | t.join 291 | end 292 | 293 | it "should not leave the Pond in a bad state if object instantiation fails" do 294 | int = 0 295 | error = false 296 | pond = Pond.new do 297 | raise "Instantiation Error!" if error 298 | int += 1 299 | end 300 | 301 | pond.checkout { |i| i.should == 1 } 302 | 303 | pond.size.should == 1 304 | pond.allocated.should == {nil => {}} 305 | pond.available.should == [1] 306 | 307 | error = true 308 | 309 | pond.checkout do |i| 310 | i.should == 1 311 | 312 | t = Thread.new do 313 | Thread.current.report_on_exception = false 314 | pond.checkout{} 315 | end 316 | 317 | proc { t.join }.should raise_error RuntimeError, "Instantiation Error!" 318 | end 319 | 320 | pond.size.should == 1 321 | pond.allocated.should == {nil => {}} 322 | pond.available.should == [1] 323 | 324 | error = false 325 | 326 | pond.checkout do |i| 327 | i.should == 1 328 | 329 | t = Thread.new do 330 | pond.checkout { |j| j.should == 2 } 331 | end 332 | 333 | t.join 334 | end 335 | 336 | pond.size.should == 2 337 | pond.allocated.should == {nil => {}} 338 | pond.available.should == [2, 1] 339 | end 340 | 341 | it "removes the object from the pool if the detach_if block returns true" do 342 | int = 0 343 | pond = Pond.new(detach_if: lambda { |obj| obj < 2 }) { int += 1 } 344 | pond.available.should == [] 345 | 346 | # allocate 1, should not check back in 347 | pond.checkout {|i| i.should == 1} 348 | pond.available.should == [] 349 | 350 | # allocate 2, should be nothing else in the pond 351 | pond.checkout do |i| 352 | i.should == 2 353 | pond.available.should == [] 354 | end 355 | 356 | # 2 should still be in the pond 357 | pond.available.should == [2] 358 | end 359 | 360 | it "should not block other threads if the detach_if block takes a long time" do 361 | i = 0 362 | q1, q2 = Queue.new, Queue.new 363 | 364 | detach_if = proc do 365 | q1.push nil 366 | q2.pop 367 | end 368 | 369 | pond = Pond.new(:detach_if => detach_if) do 370 | i += 1 371 | end 372 | 373 | t = Thread.new do 374 | pond.checkout do |i| 375 | i.should == 1 376 | end 377 | end 378 | 379 | q1.pop 380 | pond.available.should == [] 381 | pond.allocated.should == {nil => {t => 1}} 382 | 383 | # t is in the middle of invoking detach_if, we should still be able to 384 | # instantiate new objects and check them out. 385 | pond.checkout do |i| 386 | i.should == 2 387 | q2.push nil 388 | q2.push nil 389 | end 390 | 391 | t.join 392 | end 393 | 394 | it "should not leave the Pond in a bad state if the detach_if block fails" do 395 | i = 0 396 | error = false 397 | detach_if = proc do |obj| 398 | raise "Detach Error!" if error 399 | false 400 | end 401 | 402 | pond = Pond.new(:eager => true, :maximum_size => 5, :detach_if => detach_if) do 403 | i += 1 404 | end 405 | 406 | pond.size.should == 5 407 | pond.allocated.should == {} 408 | pond.available.should == [1, 2, 3, 4, 5] 409 | 410 | pond.checkout {} 411 | 412 | pond.available.should == [2, 3, 4, 5, 1] 413 | 414 | error = true 415 | 416 | checked_out = nil 417 | proc { 418 | pond.checkout do |i| 419 | checked_out = i 420 | end 421 | }.should raise_error RuntimeError, "Detach Error!" 422 | 423 | checked_out.should == 2 424 | 425 | pond.available.should == [3, 4, 5, 1] 426 | pond.allocated.should == {nil => {}} 427 | end 428 | end 429 | --------------------------------------------------------------------------------