generic_connection_pool 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Kame Chen
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,47 @@
1
+ = generic_connection_pool
2
+
3
+ == DESCRIPTION
4
+ Always network clients require a connection pool, like database connection, cache connection and others.
5
+ Generic connection pool can be used with anything. It is inspired from ActiveRecord ConnectionPool.
6
+ Sharing a limited number of network connections among many threads.
7
+ Connections are created delayed.
8
+
9
+ == USEAGE
10
+
11
+ Connections can be obtained and used from a connection pool in several
12
+ ways:
13
+
14
+ 1. Use connection_pool.connection to obtain a connection instance.
15
+ and when you're done with the connection(s) and wish it to be returned
16
+ to the pool, you can call connection_pool.release_connection to release
17
+ connection back to the connection pool.
18
+ 2. Manually check out a connection from the pool with connection_pool.checkout.
19
+ You are responsible for returning this connection to the pool when
20
+ finished by calling connection_pool.checkin(connection).
21
+ 3. Use connection_pool.with_connection(&block), which obtains a connection,
22
+ yields it as the sole argument to the block, and returns it to the pool
23
+ after the block completes.
24
+
25
+ Connections in the pool may be Adapter instance that should implete methods like
26
+ verify!, disconnect!, active? etc.
27
+
28
+ == Example
29
+ require 'generic_connection_pool'
30
+ connection_pool = ConnectionPool.new(:size => 5, :timeout => 5) do
31
+ new_connection_adapter
32
+ end
33
+ connection_pool.connection
34
+ connection_pool.release_connection
35
+
36
+ connection = connection_pool.checkout
37
+ connection_pool.checkin(connection)
38
+
39
+ connection.with_connection{|connection| do_somting_with_connection}
40
+
41
+ Do something in the block, that always create an adapter instance or connect to a server.
42
+
43
+ == INSTALL
44
+ gem install generic_connection_pool
45
+
46
+ == AUTHOR
47
+ Kame Chen
data/Rakefile ADDED
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+ require 'rake/gempackagetask'
6
+ require 'spec'
7
+ require 'spec/rake/spectask'
8
+
9
+ desc 'Default: run test.'
10
+ task :default => :spec
11
+
12
+ desc 'Test the ao_locked gem.'
13
+ Spec::Rake::SpecTask.new(:spec) do |t|
14
+ t.spec_files = FileList['spec/**/*_spec.rb']
15
+ t.spec_opts = ['-c','-f','nested']
16
+ end
17
+
18
+ desc 'Generate documentation for the ao_locked gem.'
19
+ Rake::RDocTask.new(:rdoc) do |rdoc|
20
+ rdoc.rdoc_dir = 'rdoc'
21
+ rdoc.title = 'ConnectionPool'
22
+ rdoc.options << '--line-numbers' << '--inline-source'
23
+ rdoc.rdoc_files.include('README')
24
+ rdoc.rdoc_files.include('lib/**/*.rb')
25
+ end
26
+
27
+ spec = eval(File.read(File.expand_path("../generic_connection_pool.gemspec", __FILE__)))
28
+ Rake::GemPackageTask.new(spec) do |pkg|
29
+ pkg.need_zip = false
30
+ pkg.need_tar = false
31
+ end
@@ -0,0 +1,247 @@
1
+ require 'rubygems'
2
+ require 'active_support/core_ext/module/synchronization'
3
+ require 'monitor'
4
+ require 'set'
5
+ require 'timeout'
6
+ # Generic connection pool class inspired from ActiveRecord ConnectionPool.
7
+ # Sharing a limited number of network connections among many threads.
8
+ # Connections are created delayed.
9
+ #
10
+ # == Introduction
11
+ #
12
+ # A connection pool synchronizes thread access to a limited number of
13
+ # network connections. The basic idea is that each thread checks out a
14
+ # database connection from the pool, uses that connection, and checks the
15
+ # connection back in. ConnectionPool is completely thread-safe, and will
16
+ # ensure that a connection cannot be used by two threads at the same time,
17
+ # as long as ConnectionPool's contract is correctly followed. It will also
18
+ # handle cases in which there are more threads than connections: if all
19
+ # connections have been checked out, and a thread tries to checkout a
20
+ # connection anyway, then ConnectionPool will wait until some other thread
21
+ # has checked in a connection.
22
+ #
23
+ # == Obtaining (checking out) a connection
24
+ #
25
+ # Connections can be obtained and used from a connection pool in several
26
+ # ways:
27
+ #
28
+ # 1. Use connection_pool.connection to obtain a connection instance.
29
+ # and when you're done with the connection(s) and wish it to be returned
30
+ # to the pool, you can call connection_pool.release_connection to release
31
+ # connection back to the connection pool.
32
+ # 2. Manually check out a connection from the pool with connection_pool.checkout.
33
+ # You are responsible for returning this connection to the pool when
34
+ # finished by calling connection_pool.checkin(connection).
35
+ # 3. Use connection_pool.with_connection(&block), which obtains a connection,
36
+ # yields it as the sole argument to the block, and returns it to the pool
37
+ # after the block completes.
38
+ #
39
+ # Connections in the pool may be Adapter instance that should implete methods like
40
+ # verify!, disconnect!, active? etc.
41
+ #
42
+ # == Example
43
+ #
44
+ # connection_pool = ConnectionPool.new(:size => 5, :timeout => 5) do
45
+ # new_connection_adapter
46
+ # end
47
+ # connection_pool.connection
48
+ # connection_pool.release_connection
49
+ #
50
+ # connection = connection_pool.checkout
51
+ # connection_pool.checkin(connection)
52
+ #
53
+ # connection.with_connection{|connection| do_somting_with_connection}
54
+ #
55
+ #
56
+ # Do something in the block, that always create an adapter instance or connect to a server.
57
+ #
58
+ class ConnectionPool
59
+ VERSION = '0.1.0'
60
+
61
+ class ConnectionTimeoutError < ::Timeout::Error
62
+ end
63
+
64
+ attr_reader :options
65
+
66
+ DEFAULT_OPTIONS = { :size => 5, :timeout => 5 }
67
+
68
+
69
+
70
+ # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification
71
+ # object which describes database connection information (e.g. adapter,
72
+ # host name, username, password, etc), as well as the maximum size for
73
+ # this ConnectionPool.
74
+ #
75
+ # The default ConnectionPool maximum size is 5.
76
+ def initialize(options = {}, &block)
77
+ @options = DEFAULT_OPTIONS.merge(options)
78
+
79
+ raise ArgumentError, "Connection pool requires a block that create a new connection!" unless block
80
+
81
+ @connection_block = block
82
+
83
+ # The cache of reserved connections mapped to threads
84
+ @reserved_connections = {}
85
+
86
+ # The mutex used to synchronize pool access
87
+ @connection_mutex = Monitor.new
88
+ @queue = @connection_mutex.new_cond
89
+
90
+ # default 5 second timeout
91
+ @timeout = @options[:timeout]
92
+
93
+ # default max pool size to 5
94
+ @size = @options[:size]
95
+
96
+ @connections = []
97
+ @checked_out = []
98
+ end
99
+
100
+ # Retrieve the connection associated with the current thread, or call
101
+ # #checkout to obtain one if necessary.
102
+ #
103
+ # #connection can be called any number of times; the connection is
104
+ # held in a hash keyed by the thread id.
105
+ def connection
106
+ @reserved_connections[current_connection_id] ||= checkout
107
+ end
108
+
109
+ # Signal that the thread is finished with the current connection.
110
+ # #release_connection releases the connection-thread association
111
+ # and returns the connection to the pool.
112
+ def release_connection
113
+ conn = @reserved_connections.delete(current_connection_id)
114
+ checkin conn if conn
115
+ end
116
+
117
+ # If a connection already exists yield it to the block. If no connection
118
+ # exists checkout a connection, yield it to the block, and checkin the
119
+ # connection when finished.
120
+ def with_connection
121
+ fresh_connection = true unless connection_cached?
122
+ yield connection
123
+ ensure
124
+ release_connection if fresh_connection
125
+ end
126
+
127
+ # Returns true if a connection has already been opened.
128
+ def connected?
129
+ !@connections.empty?
130
+ end
131
+
132
+ # Disconnects all connections in the pool, and clears the pool.
133
+ def disconnect!
134
+ @reserved_connections.each do |name,conn|
135
+ checkin conn
136
+ end
137
+ @reserved_connections = {}
138
+ @connections.each do |conn|
139
+ conn.disconnect!
140
+ end
141
+ @connections = []
142
+ end
143
+
144
+ # Verify active connections and remove and disconnect connections
145
+ # associated with stale threads.
146
+ def verify_active_connections! #:nodoc:
147
+ clear_stale_cached_connections!
148
+ @connections.each do |connection|
149
+ connection.verify!
150
+ end
151
+ end
152
+
153
+ # Return any checked-out connections back to the pool by threads that
154
+ # are no longer alive.
155
+ def clear_stale_cached_connections!
156
+ keys = @reserved_connections.keys - Thread.list.find_all { |t|
157
+ t.alive?
158
+ }.map { |thread| thread.object_id }
159
+ keys.each do |key|
160
+ checkin @reserved_connections[key]
161
+ @reserved_connections.delete(key)
162
+ end
163
+ end
164
+
165
+ # Check-out a database connection from the pool, indicating that you want
166
+ # to use it. You should call #checkin when you no longer need this.
167
+ #
168
+ # This is done by either returning an existing connection, or by creating
169
+ # a new connection. If the maximum number of connections for this pool has
170
+ # already been reached, but the pool is empty (i.e. they're all being used),
171
+ # then this method will wait until a thread has checked in a connection.
172
+ # The wait time is bounded however: if no connection can be checked out
173
+ # within the timeout specified for this pool, then a ConnectionTimeoutError
174
+ # exception will be raised.
175
+ #
176
+ # Returns: connection instance return by the connection_block
177
+ #
178
+ def checkout
179
+ # Checkout an available connection
180
+ @connection_mutex.synchronize do
181
+ loop do
182
+ conn = if @checked_out.size < @connections.size
183
+ checkout_existing_connection
184
+ elsif @connections.size < @size
185
+ checkout_new_connection
186
+ end
187
+ return conn if conn
188
+ # No connections available; wait for one
189
+ if @queue.wait(@timeout)
190
+ next
191
+ else
192
+ # try looting dead threads
193
+ clear_stale_cached_connections!
194
+ if @size == @checked_out.size
195
+ raise ConnectionTimeoutError, "Could not obtain a connection within #{@timeout} seconds. The max pool size is currently #{@size}; consider increasing it."
196
+ end
197
+ end
198
+ end
199
+ end
200
+ end
201
+
202
+ # Check-in a connection back into the pool, indicating that you
203
+ # no longer need this connection.
204
+ #
205
+ # +conn+: which was obtained by earlier by calling +checkout+ on this pool.
206
+ def checkin(conn)
207
+ @connection_mutex.synchronize do
208
+ @checked_out.delete conn
209
+ @queue.signal
210
+ end
211
+ end
212
+
213
+ # whether connection cached in the current thread
214
+ def connection_cached?
215
+ !!@reserved_connections[current_connection_id]
216
+ end
217
+
218
+ synchronize :verify_active_connections!, :connected?, :disconnect!, :with => :@connection_mutex
219
+
220
+ private
221
+ def new_connection
222
+ @connection_block.call
223
+ end
224
+
225
+ def current_connection_id #:nodoc:
226
+ Thread.current.object_id
227
+ end
228
+
229
+ def checkout_new_connection
230
+ c = new_connection
231
+ @connections << c
232
+ checkout_and_verify(c)
233
+ end
234
+
235
+ def checkout_existing_connection
236
+ c = (@connections - @checked_out).first
237
+ checkout_and_verify(c)
238
+ end
239
+
240
+ def checkout_and_verify(c)
241
+ # connection must have verify! method that verify the connection active;
242
+ # it should auto retry to reconnect if not active.
243
+ c.verify!
244
+ @checked_out << c
245
+ c
246
+ end
247
+ end
@@ -0,0 +1 @@
1
+ require File.expand_path('../connection_pool', __FILE__)
@@ -0,0 +1,98 @@
1
+ require File.expand_path("../", __FILE__) + '/spec_helper'
2
+
3
+ def mock_connection_adapter
4
+ mock_connection_adapter = mock("connection_adapter")
5
+ mock_connection_adapter.stub!(:verify!)
6
+ mock_connection_adapter.stub!(:active?)
7
+ mock_connection_adapter.stub!(:disconnect!)
8
+ mock_connection_adapter
9
+ end
10
+
11
+ describe ConnectionPool do
12
+ describe "initialize ConnectionPool" do
13
+ it "raise ArgumentError if block not given" do
14
+ lambda{ConnectionPool.new}.should raise_error(ArgumentError)
15
+ end
16
+ it "connection pool size default 5, and can customize" do
17
+ ConnectionPool.new{}.options[:size].should == 5
18
+ ConnectionPool.new(:size => 10){}.options[:size].should == 10
19
+ end
20
+ it "connection timeout default 5, and can customize" do
21
+ ConnectionPool.new{}.options[:timeout].should == 5
22
+ ConnectionPool.new(:timeout => 10){}.options[:timeout].should == 10
23
+ end
24
+ end
25
+
26
+ describe "#connection" do
27
+ before do
28
+ @connection_pool = ConnectionPool.new{mock_connection_adapter}
29
+ end
30
+ it "should return a connection from connection pool" do
31
+ @connection_pool.connection.should_not be_nil
32
+ end
33
+ it "should cache the connection when call the #connection method and repeat call multi times should return the same connection if in the same thread" do
34
+ @connection_pool.connection.should == @connection_pool.connection
35
+ @connection_pool.should be_connection_cached
36
+ end
37
+ it "in different threads should return different connection when call #connection method" do
38
+ main_thread_connection = @connection_pool.connection
39
+ Thread.new{@connection_pool.connection.should_not == main_thread_connection}
40
+ end
41
+ end
42
+
43
+ describe "#release_connection" do
44
+ before do
45
+ @connection_pool = ConnectionPool.new{mock_connection_adapter}
46
+ end
47
+ it "should delete the cached connection" do
48
+ @connection_pool.connection
49
+ @connection_pool.release_connection
50
+ @connection_pool.should_not be_connection_cached
51
+ end
52
+ it "should check in the connection back to the connection pool" do
53
+ @connection_pool.connection
54
+ @connection_pool.release_connection
55
+ @connection_pool.instance_variable_get("@checked_out").should be_empty
56
+ end
57
+ end
58
+
59
+ describe "#checkout " do
60
+ before do
61
+ @connection_pool = ConnectionPool.new{mock_connection_adapter}
62
+ @exist_connections = []
63
+ 5.times do
64
+ Thread.new{con = @connection_pool.checkout; @exist_connections << con}
65
+ end
66
+ @exist_connections.each{|con|@connection_pool.checkin(con)}
67
+ end
68
+ it "should checkout the exist connection if connections not empty" do
69
+ con = @connection_pool.checkout
70
+ @exist_connections.should include(con)
71
+ end
72
+ it "should raise error if can not obtain a connection for timeout" do
73
+ threads = []
74
+ 5.times do
75
+ threads << Thread.new{@connection_pool.checkout; sleep(5)}
76
+ end
77
+ lambda{@connection_pool.checkout}.should raise_error(ConnectionPool::ConnectionTimeoutError)
78
+ threads.each(&:join)
79
+ end
80
+ end
81
+
82
+ describe "#with_connection" do
83
+ before do
84
+ @connection_pool = ConnectionPool.new{mock_connection_adapter}
85
+ end
86
+ it "should check in the connection back to the pool if it is a fresh connection" do
87
+ @connection_pool.with_connection{|connection|}
88
+ @connection_pool.should_not be_connection_cached
89
+ @connection_pool.instance_variable_get("@checked_out").should be_empty
90
+ end
91
+ it "should not check in the connection back to the pool if it is a cached connection" do
92
+ @connection_pool.connection
93
+ @connection_pool.with_connection{|connection|}
94
+ @connection_pool.should be_connection_cached
95
+ @connection_pool.instance_variable_get("@checked_out").should_not be_empty
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,2 @@
1
+ $:.unshift(File.expand_path('../../lib', __FILE__))
2
+ require 'connection_pool'
metadata ADDED
@@ -0,0 +1,105 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: generic_connection_pool
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - kame
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-12-28 00:00:00 +08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: activesupport
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rspec
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :development
48
+ version_requirements: *id002
49
+ description: " Always network clients require a connection pool, like database connection, cache connection and others.\n\
50
+ Generic connection pool can be used with anything. It is inspired from ActiveRecord ConnectionPool.\n\
51
+ Sharing a limited number of network connections among many threads.\n\
52
+ Connections are created delayed.\n"
53
+ email:
54
+ - kamechb@gmail.com
55
+ executables: []
56
+
57
+ extensions: []
58
+
59
+ extra_rdoc_files: []
60
+
61
+ files:
62
+ - lib/connection_pool.rb
63
+ - lib/generic_connection_pool.rb
64
+ - spec/connection_pool_spec.rb
65
+ - spec/spec_helper.rb
66
+ - Rakefile
67
+ - README
68
+ - Gemfile
69
+ - LICENSE
70
+ has_rdoc: true
71
+ homepage:
72
+ licenses: []
73
+
74
+ post_install_message:
75
+ rdoc_options: []
76
+
77
+ require_paths:
78
+ - lib
79
+ required_ruby_version: !ruby/object:Gem::Requirement
80
+ none: false
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ hash: 3
85
+ segments:
86
+ - 0
87
+ version: "0"
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ hash: 3
94
+ segments:
95
+ - 0
96
+ version: "0"
97
+ requirements: []
98
+
99
+ rubyforge_project:
100
+ rubygems_version: 1.6.2
101
+ signing_key:
102
+ specification_version: 3
103
+ summary: Always network clients require a connection pool, like database connection, cache connection and others. Generic connection pool can be used with anything. It is inspired from ActiveRecord ConnectionPool. Sharing a limited number of network connections among many threads. Connections are created delayed.
104
+ test_files: []
105
+