common-pool 0.3.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.
Files changed (5) hide show
  1. data/CHANGELOG +3 -0
  2. data/README +51 -0
  3. data/lib/common_pool.rb +349 -0
  4. data/test/common_pool_test.rb +169 -0
  5. metadata +57 -0
data/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ *0.5.0* (14th May 2007)
2
+
3
+ * First release
data/README ADDED
@@ -0,0 +1,51 @@
1
+ == common-pool
2
+
3
+ Object pooling with idle objects eviction check similar with Apache Common Pool.
4
+
5
+ Links:
6
+ * http://common-pool.rubyforge.org
7
+ * http://www.pluitsolutions.com/common-pool
8
+
9
+ == INSTALLATION
10
+
11
+ $ gem install common-pool
12
+
13
+ == EXAMPLE
14
+
15
+ require 'common_pool'
16
+
17
+ # Extend data source object
18
+ class RandomNumberDataSource < CommonPool::PoolDataSource
19
+ # Overwrite to return object to be stored in the pool.
20
+ def create_object
21
+ rand(1000)
22
+ end
23
+
24
+ # Overwrite to check if idle object in the pool is still valid.
25
+ def valid?(object)
26
+ true
27
+ end
28
+ end
29
+
30
+ # Create a new object pool
31
+ object_pool = ObjectPool.new(RandomNumberDataSource.new)
32
+
33
+ # Borrow and return an object from the pool
34
+ object = object_pool.borrow_object
35
+ object_pool.return_object(object)
36
+
37
+ # Borrow and invalidate object
38
+ object = object_pool.borrow_object
39
+ object_pool.return_object(object)
40
+
41
+ # Create object pool with idle objects eviction thread
42
+ object_pool = ObjectPool.new(RandomNumberDataSource.new) do |config|
43
+ config.min_idle = 5
44
+ config.max_idle = 10
45
+ config.idle_check_no_per_run = 10 # check max 10 idle objects per run
46
+ config.idle_check_interval = 10 * 60 # check every 10 minutes
47
+ end
48
+
49
+ # Return a hash of pool instance status variables, including active and idle objects list and
50
+ # configuration options
51
+ object_pool.status_info
@@ -0,0 +1,349 @@
1
+ #--
2
+ # Copyright (c) 2007 Herryanto Siatono, Pluit Solutions
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require 'timeout'
25
+ require 'thread'
26
+ require 'logger'
27
+
28
+ module CommonPool
29
+ class CommonPoolError < StandardError; end;
30
+
31
+ # Pool configuration.
32
+ class Configuration
33
+ # Minimum number of idle objects in the pool. Default: 0.
34
+ attr_accessor :min_idle
35
+
36
+ # Maximum number of idle objects in the pool. Default: 8.
37
+ attr_accessor :max_idle
38
+
39
+ # Maximum idle time in seconds, before an object can be evicted. Default: 1800.
40
+ attr_accessor :max_idle_time
41
+
42
+ # Maximum number of active objects in the pool. Default: 8.
43
+ attr_accessor :max_active
44
+
45
+ # Request timeout when creating object for the pool, timeout in seconds. Default: 30.
46
+ # If set to 0 or less, there will be no timeout.
47
+ attr_accessor :timeout
48
+
49
+ # Request timeout when validation idle object in the pool, timeout in seconds. Default: 30.
50
+ # If set to 0 or less, there will be no timeout.
51
+ attr_accessor :validation_timeout
52
+
53
+ # Number of objects to check per eviction thread run. Default: 3.
54
+ # If set to 0 or less, all idle objects will be checked.
55
+ attr_accessor :idle_check_no_per_run
56
+
57
+ # Eviction thread interval in seconds. Default: 0.
58
+ # If set to 0 or less, the eviction thread will not run.
59
+ attr_accessor :idle_check_interval
60
+
61
+ # Assign a logger, to log pool debug/info messages, used this for debugging purpose.
62
+ attr_accessor :logger
63
+
64
+ # The data source object to be extended, code>create_object</code> method need to be
65
+ # overridden
66
+ attr_accessor :data_source
67
+
68
+ # Initilize configuration with default settings.
69
+ def initialize
70
+ self.min_idle = 0
71
+ self.max_idle = 8
72
+ self.max_idle_time = 30 * 60
73
+ self.max_active = 8
74
+ self.idle_check_no_per_run = 3
75
+ self.idle_check_interval= 0
76
+ self.timeout = 30
77
+ self.validation_timeout = 30
78
+ end
79
+
80
+ # Convert configuration properties to hash object.
81
+ def to_hash
82
+ { :timeout => self.timeout,
83
+ :min_idle => self.min_idle,
84
+ :max_idle => self.max_idle,
85
+ :max_idle_time => self.max_idle_time,
86
+ :max_active => self.max_active,
87
+ :idle_check_no_per_run => self.idle_check_no_per_run,
88
+ :idle_check_interval => self.idle_check_interval}
89
+ end
90
+ end
91
+
92
+ # Data source for objects to be stored in the pool.
93
+ class PoolDataSource
94
+ # Override to create an object to be stored in the pool.
95
+ def create_object
96
+ raise CommonPoolError, "Overwrite this method to create an object to be stored in the pool."
97
+ end
98
+
99
+ # Override to check the validity of your idle object, called when idle object eviction stread.
100
+ def valid?(object)
101
+ true
102
+ end
103
+ end
104
+
105
+ # First in, first out object pooling implementation.
106
+ class ObjectPool
107
+ attr_reader :active_list, :idle_list, :idle_check_thread, :idle_check_status
108
+ attr_accessor :config, :data_source
109
+
110
+ # Initialize pool instance with default configuration options. Override config as a parameter passed to the block.
111
+ def initialize(data_source = nil, &proc)
112
+ raise ArgumentError, "Data source is required." unless data_source
113
+ self.config = Configuration.new
114
+ self.data_source = data_source
115
+ @active_list = {}
116
+ @idle_list = []
117
+ @idle_check_status = "Not Running"
118
+ @mutex = Mutex.new
119
+ @cond = ConditionVariable.new
120
+
121
+ yield self.config if block_given?
122
+ self.start_check_idle_thread
123
+ end
124
+
125
+ # Configure pool instance after initialization.
126
+ def configure(&proc)
127
+ yield self.config
128
+ self.start_check_idle_thread
129
+ end
130
+
131
+ # Alias for <code>check_idle_thread</code>.
132
+ def start
133
+ start_check_idle_thread
134
+ end
135
+
136
+ # Borrow object from the pool. If max number of active objects reached, <code>CommonPoolErrror</code> will be thrown.
137
+ def borrow_object
138
+ @mutex.synchronize {
139
+ if !@idle_list.empty?
140
+ result = @idle_list.shift
141
+
142
+ elsif @active_list.size < @config.max_active
143
+ result = create_with_timestamp
144
+
145
+ elsif @active_list.size >= @config.max_active
146
+ raise CommonPoolError, "Max number of %d active objects reached." % @config.max_active
147
+
148
+ else
149
+ begin
150
+ timeout(@config.timeout){ @cond.wait(@mutex) }
151
+ rescue TimeoutError
152
+ raise TimeoutError, "#{@config.timeout} seconds request timeout reached."
153
+ end
154
+ result = @idle_list.shift
155
+ end
156
+ @active_list.store(result[1].__id__, result)
157
+
158
+ logger.debug("* Get #{result[1]}") if logger
159
+ result[1]
160
+ }
161
+ end
162
+
163
+ # Return object back to the pool.
164
+ def return_object(object)
165
+ logger.debug("* Return #{object}") if logger
166
+ return if object.nil?
167
+ if @active_list.delete(object.__id__)
168
+ add_to_idle_list(object)
169
+ else
170
+ logger.debug("Session #{object} not returned, coz has been invalidated from used list.") if logger
171
+ end
172
+ end
173
+
174
+ # Invalidate object
175
+ def invalidate_object(object)
176
+ @mutex.synchronize {
177
+ logger.debug("* Invalidate #{object}") if logger
178
+ @active_list.delete(object.__id__)
179
+ nil
180
+ }
181
+ end
182
+
183
+ # Clear object pool, set the idle list and used list to empty.
184
+ def clear()
185
+ @mutex.synchronize {
186
+ @idle_list = []
187
+ @active_list = {}
188
+ }
189
+ end
190
+
191
+ # Reset the max active objects in the pool.
192
+ def max_active=(size)
193
+ @mutex.synchronize {
194
+ if @idle_list.size + @active_list.size > size
195
+ raise ArgumentError, "Cannot change max size to %d. There are objects over the size." % size
196
+ end
197
+ @config.max_active = size
198
+ }
199
+ size
200
+ end
201
+
202
+ # Start check idle objects thread, will only run if <code>idle_check_interval</code> is greater than 0.
203
+ def start_check_idle_thread
204
+ @mutex.synchronize {
205
+ return unless @config.idle_check_interval > 0 and @idle_check_thread.nil?
206
+ @idle_check_status = "Checking..."
207
+
208
+ @idle_check_thread = Thread.new do
209
+ loop do
210
+ thread_id = @idle_check_thread.__id__ if @idle_check_thread
211
+
212
+ begin
213
+ logger.debug(">> Starting idle objects check (Object ID: #{self.__id__}, Thread ID: #{thread_id})") if logger
214
+ logger.debug("Status: " + self.status_info.map {|k,v| "#{k}=#{v}"}.join(', ')) if logger
215
+ @idle_check_status = "Checking..."
216
+
217
+ check_idle
218
+
219
+ @idle_check_status = "Check status: OK. Sleeping now..."
220
+ rescue Exception => e
221
+ @idle_check_status = "Check status: Error - #{e.to_s} \n#{e.backtrace.join("\n")}. Sleeping...\n"
222
+ end
223
+
224
+ logger.debug(@idle_check_status) if logger
225
+ if @config.idle_check_interval > 0
226
+ logger.debug(">> Sleeping (Object ID: #{self.__id__}, Thread ID: #{thread_id}) #{@config.idle_check_interval} seconds ...\n\n") if logger
227
+ sleep @config.idle_check_interval
228
+ else
229
+ logger.debug(">> Idle check interval is set 0 or less. Thus idle check thread will only be executed once.") if logger
230
+ end
231
+ end
232
+ end
233
+ }
234
+ end
235
+
236
+ # Stop idle check thread
237
+ def stop_idle_check_thread
238
+ if @idle_check_thread
239
+ logger.debug(">> Stopping idle objects check thread ...") if logger
240
+ @idle_check_status = "Stopping..."
241
+ @idle_check_thread.kill
242
+ @idle_check_thread = nil
243
+ @idle_check_status = "Not Running"
244
+ end
245
+ end
246
+
247
+ # Restart idle check thread
248
+ def restart_check_idle_thread
249
+ stop_idle_check_thread
250
+ start_idle_check_thread
251
+ end
252
+
253
+ # Return a hash of active and idle objects size with pool instance configuration options.
254
+ def status_info
255
+ {:active_objects => @active_list.size,
256
+ :idle_objects => @idle_list.size}.merge(self.config.to_hash)
257
+ end
258
+
259
+ # Check if an idle object is valid, return false if <code>validation_timeout</code> period reached.
260
+ def valid_idle_object?(object)
261
+ begin
262
+ timeout(self.config.validation_timeout) do
263
+ self.data_source.valid?(object)
264
+ end
265
+ rescue TimeoutError => e
266
+ logger.debug("Timeout #{@config.validation_timeout} seconds validating object: #{object}") if logger
267
+ false
268
+ end
269
+ end
270
+
271
+ protected
272
+
273
+ def logger
274
+ self.config.logger
275
+ end
276
+
277
+ def create_with_timestamp
278
+ [Time.new, self.data_source.create_object]
279
+ end
280
+
281
+ def add_to_idle_list(object)
282
+ @mutex.synchronize {
283
+ if (@config.max_idle > 0) and (@idle_list.size >= @config.max_idle)
284
+ logger.debug("Not returned, max idle #{@config.max_idle} reached") if logger
285
+ return
286
+ end
287
+
288
+ logger.debug("* Add #{object}") if logger
289
+ @idle_list.push([Time.new, object])
290
+ @cond.signal
291
+ }
292
+ end
293
+
294
+ def check_min_idle
295
+ logger.debug("Minimum idle objects: #{@config.min_idle}, current idle size: #{@idle_list.size}") if logger
296
+ return if (@config.min_idle < 1) || (@idle_list.size >= @config.min_idle)
297
+
298
+ objects_to_create = @config.min_idle - @idle_list.size
299
+ logger.debug("Creating additional #{objects_to_create} object(s).") if logger
300
+ objects_to_create.times do
301
+ add_to_idle_list(self.data_source.create_object)
302
+ end
303
+ end
304
+
305
+ def check_idle
306
+ logger.debug("Checking idle objects list size: #{@idle_list.size}, min idle: #{@config.min_idle}") if logger
307
+
308
+ counter = 0
309
+ checked_obj = []
310
+ @idle_list.size.times do
311
+ break if (@config.idle_check_no_per_run > 0) and (counter >= @config.idle_check_no_per_run)
312
+
313
+ counter += 1
314
+ result = @idle_list.shift
315
+ logger.debug("Checking idle object: #{result.join('|')}") if logger
316
+ if result
317
+ if (within_max_idle_time?(result[0]) && valid_idle_object?(result[1]))
318
+ checked_obj.push(result)
319
+ else
320
+ invalidate(result[1])
321
+ end
322
+ else
323
+ logger.debug("No more object available.") if logger
324
+ end
325
+ end
326
+
327
+ checked_obj.each {|result| @idle_list.push(result)}
328
+ check_min_idle
329
+ end
330
+
331
+ def within_max_idle_time?(objtime)
332
+ return true if (@config.max_idle_time < 0)
333
+
334
+ seconds_passed = Time.now - objtime
335
+ exceed_time = seconds_passed > @config.max_idle_time
336
+
337
+ if (@config.max_idle_time == 0) || (exceed_time)
338
+ logger.debug("Idle for #{seconds_passed} seconds, exceeded #{seconds_passed - @config.max_idle_time} seconds") if logger
339
+ return false
340
+ else
341
+ return true
342
+ end
343
+ end
344
+
345
+ def idle_check_status=(s)
346
+ @config.idle_check_status = s
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,169 @@
1
+ #--
2
+ # Copyright (c) 2007 Herryanto Siatono, Pluit Solutions
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
18
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
19
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
20
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
21
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ require File.dirname(__FILE__) + '/../lib/common_pool'
25
+
26
+ class CommonPoolTest < Test::Unit::TestCase
27
+ include CommonPool
28
+
29
+ def test_get_and_release_session_pool
30
+ # test get sessions from pool
31
+ pool = create_pool
32
+
33
+ session_a = pool.borrow_object
34
+ session_b = pool.borrow_object
35
+
36
+ assert_not_equal session_a, session_b
37
+ assert_equal 2, pool.active_list.size
38
+ assert_equal 0, pool.idle_list.size
39
+
40
+ pool.return_object(session_a)
41
+ pool.return_object(session_b)
42
+ assert_equal 0, pool.active_list.size
43
+ assert_equal 2, pool.idle_list.size
44
+ end
45
+
46
+ def test_first_in_first_out
47
+ pool = create_pool
48
+ session_a = pool.borrow_object
49
+ session_b = pool.borrow_object
50
+
51
+ pool.return_object(session_b)
52
+ pool.return_object(session_a)
53
+
54
+ session_new = pool.borrow_object
55
+ assert_equal session_b, session_new
56
+ end
57
+
58
+ def test_max_active_reached
59
+ pool = create_pool
60
+ until pool.active_list.size == pool.config.max_active
61
+ obj = pool.borrow_object
62
+ end
63
+
64
+ begin
65
+ # max active reached, relase object to retrieve a new one
66
+ pool.return_object(obj)
67
+ wait_obj = pool.borrow_object
68
+ assert_equal obj, wait_obj
69
+ end
70
+ end
71
+
72
+ def test_timeout_reached
73
+ pool = create_pool do |config|
74
+ config.timeout = 1
75
+ end
76
+
77
+ until pool.active_list.size == pool.config.max_active
78
+ obj = pool.borrow_object
79
+ end
80
+
81
+ # max active reached wait till timeout
82
+ assert_raise(CommonPoolError) do
83
+ wait_obj = pool.borrow_object
84
+ end
85
+ end
86
+
87
+ def test_invalidate
88
+ pool = create_pool
89
+ session_a = pool.borrow_object
90
+ session_b = pool.borrow_object
91
+
92
+ assert_equal 2, pool.active_list.size
93
+ assert_equal 0, pool.idle_list.size
94
+
95
+ pool.invalidate_object(session_a)
96
+ assert_equal 1, pool.active_list.size
97
+ assert_equal 0, pool.idle_list.size
98
+
99
+ pool.return_object(session_b)
100
+ assert_equal 0, pool.active_list.size
101
+ assert_equal 1, pool.idle_list.size
102
+
103
+ session_new = pool.borrow_object
104
+ assert_equal session_new, session_b
105
+ end
106
+
107
+
108
+ def test_check_min_idle
109
+ pool = create_pool do |config|
110
+ config.min_idle = 2
111
+ config.idle_check_interval = 60
112
+ end
113
+
114
+ # sleep to let the pool populate
115
+ until pool.idle_check_status.match(/Sleeping/)
116
+ sleep 2
117
+ end
118
+
119
+ assert_equal 2, pool.idle_list.size
120
+ end
121
+
122
+ def test_check_max_idle
123
+ pool = create_pool do |config|
124
+ config.min_idle = 0
125
+ config.max_idle = 2
126
+ config.idle_check_interval = 60
127
+ end
128
+
129
+ a = pool.borrow_object
130
+ b = pool.borrow_object
131
+ c = pool.borrow_object
132
+
133
+ # idle will keep up to 2
134
+ pool.return_object(a)
135
+ pool.return_object(b)
136
+ assert_equal 2, pool.idle_list.size
137
+
138
+ # throw away no 3
139
+ pool.return_object(c)
140
+ assert_equal 2, pool.idle_list.size
141
+ end
142
+
143
+ # def test_run
144
+ # pool = create_pool do |config|
145
+ # config.debug = true
146
+ # config.min_idle = 2
147
+ # config.max_active = 3
148
+ # config.max_idle_time = 15
149
+ # config.idle_check = true
150
+ # config.idle_check_interval = 3
151
+ # end
152
+ #
153
+ # pool.idle_check_thread.join
154
+ # end
155
+
156
+ protected
157
+ class TestDataSource < PoolDataSource
158
+ def create_object
159
+ rand(1000)
160
+ end
161
+ end
162
+
163
+ def create_pool(&proc)
164
+ pool = ObjectPool.new(TestDataSource.new) do |config|
165
+ config.logger = Logger.new(STDOUT)
166
+ yield config if block_given?
167
+ end
168
+ end
169
+ end
metadata ADDED
@@ -0,0 +1,57 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: common-pool
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.3.0
7
+ date: 2007-05-14 00:00:00 +08:00
8
+ summary: Common Pool - Object Pooling
9
+ require_paths:
10
+ - lib
11
+ email: herryanto@pluitsolutions.com
12
+ homepage: http://common-pool.rubyforge.net/
13
+ rubyforge_project:
14
+ description:
15
+ autorequire: name
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Herryanto Siatono
31
+ files:
32
+ - lib/common_pool.rb
33
+ - README
34
+ - CHANGELOG
35
+ test_files:
36
+ - test/common_pool_test.rb
37
+ rdoc_options: []
38
+
39
+ extra_rdoc_files:
40
+ - README
41
+ - CHANGELOG
42
+ executables: []
43
+
44
+ extensions: []
45
+
46
+ requirements: []
47
+
48
+ dependencies:
49
+ - !ruby/object:Gem::Dependency
50
+ name: hpricot
51
+ version_requirement:
52
+ version_requirements: !ruby/object:Gem::Version::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0.4"
57
+ version: