common-pool 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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: