actionpool 0.2.2

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.
@@ -0,0 +1,282 @@
1
+ require 'actionpool/Thread'
2
+ require 'actionpool/Queue'
3
+ require 'actionpool/LogHelper'
4
+ require 'thread'
5
+
6
+ module ActionPool
7
+ # Raised when pool is closed
8
+ class PoolClosed < StandardError
9
+ end
10
+ class Pool
11
+
12
+ # :min_threads:: minimum number of threads in pool
13
+ # :max_threads:: maximum number of threads in pool
14
+ # :t_to:: thread timeout waiting for action to process
15
+ # :a_to:: maximum time action may be worked on before aborting
16
+ # :logger:: logger to print logging messages to
17
+ # Creates a new pool
18
+ def initialize(args={})
19
+ raise ArgumentError.new('Hash required for initialization') unless args.is_a?(Hash)
20
+ @logger = LogHelper.new(args[:logger])
21
+ @queue = ActionPool::Queue.new
22
+ @threads = []
23
+ @lock = Mutex.new
24
+ @thread_timeout = args[:t_to] ? args[:t_to] : 0
25
+ @action_timeout = args[:a_to] ? args[:a_to] : 0
26
+ @max_threads = args[:max_threads] ? args[:max_threads] : 100
27
+ @min_threads = args[:min_threads] ? args[:min_threads] : 10
28
+ @min_threads = @max_threads if @max_threads < @min_threads
29
+ @respond_to = args[:respond_thread] || ::Thread.current
30
+ @open = true
31
+ fill_pool
32
+ end
33
+
34
+ # Pool is closed
35
+ def pool_closed?
36
+ !@open
37
+ end
38
+
39
+ # Pool is open
40
+ def pool_open?
41
+ @open
42
+ end
43
+
44
+ # arg:: :open or :closed
45
+ # Set pool status
46
+ def status(arg)
47
+ @open = arg == :open
48
+ fill_pool if @open
49
+ end
50
+
51
+ # args:: :force forces a new thread. :nowait will create a thread if threads are waiting
52
+ # Create a new thread for pool.
53
+ # Returns newly created thread of nil if pool is at maximum size
54
+ def create_thread(*args)
55
+ return if pool_closed?
56
+ thread = nil
57
+ @lock.synchronize do
58
+ if(((size == working || args.include?(:nowait)) && @threads.size < @max_threads) || args.include?(:force))
59
+ thread = ActionPool::Thread.new(:pool => self, :respond_thread => @respond_to, :a_timeout => @action_timeout, :t_timeout => @thread_timeout, :logger => @logger)
60
+ @threads << thread
61
+ end
62
+ end
63
+ return thread
64
+ end
65
+
66
+ # Fills the pool with the minimum number of threads
67
+ # Returns array of created threads
68
+ def fill_pool
69
+ threads = []
70
+ @lock.synchronize do
71
+ required = min - size
72
+ if(required > 0)
73
+ required.times do
74
+ thread = ActionPool::Thread.new(:pool => self, :respond_thread => @respond_to, :a_timeout => @action_timeout, :t_timeout => @thread_timeout, :logger => @logger)
75
+ @threads << thread
76
+ threads << thread
77
+ end
78
+ end
79
+ end
80
+ return threads
81
+ end
82
+
83
+ # force:: force immediate stop
84
+ # Stop the pool
85
+ def shutdown(force=false)
86
+ status(:closed)
87
+ args = []
88
+ args.push(:force) if force
89
+ @logger.info("Pool is now shutting down #{force ? 'using force' : ''}")
90
+ @queue.clear if force
91
+ @queue.wait_empty
92
+ while(t = @threads.pop) do
93
+ t.stop(*args)
94
+ end
95
+ nil
96
+ end
97
+
98
+ # action:: proc to be executed or array of [proc, [*args]]
99
+ # Add a new proc/lambda to be executed (alias for queue)
100
+ def <<(action)
101
+ case action
102
+ when Proc
103
+ queue(action)
104
+ when Array
105
+ raise ArgumentError.new('Actions to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]') unless action.size == 2 and action[0].is_a?(Proc) and action[1].is_a?(Array)
106
+ queue(action[0], action[1])
107
+ else
108
+ raise ArgumentError.new('Actions to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]')
109
+ end
110
+ nil
111
+ end
112
+
113
+ # action:: proc to be executed
114
+ # Add a new proc/lambda to be executed
115
+ def queue(action, *args)
116
+ raise PoolClosed.new("Pool #{self} is currently closed") if pool_closed?
117
+ raise ArgumentError.new('Expecting block') unless action.is_a?(Proc)
118
+ @queue << [action, args]
119
+ ::Thread.pass
120
+ create_thread
121
+ end
122
+
123
+ # jobs:: Array of proc/lambdas
124
+ # Will queue a list of jobs into the pool
125
+ def add_jobs(jobs)
126
+ raise PoolClosed.new("Pool #{self} is currently closed") if pool_closed?
127
+ raise ArgumentError.new("Expecting an array but received: #{jobs.class}") unless jobs.is_a?(Array)
128
+ @queue.pause
129
+ begin
130
+ jobs.each do |job|
131
+ case job
132
+ when Proc
133
+ @queue << [job, []]
134
+ when Array
135
+ raise ArgumentError.new('Jobs to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]') unless job.size == 2 and job[0].is_a?(Proc) and job[1].is_a?(Array)
136
+ @queue << [job.shift, job]
137
+ else
138
+ raise ArgumentError.new('Jobs to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]')
139
+ end
140
+ end
141
+ ensure
142
+ num = jobs.size - @threads.select{|t|t.waiting?}.size
143
+ num.times{ create_thread(:nowait) } if num > 0
144
+ @queue.unpause
145
+ end
146
+ true
147
+ end
148
+
149
+ # block:: block to process
150
+ # Adds a block to be processed
151
+ def process(*args, &block)
152
+ queue(block, *args)
153
+ nil
154
+ end
155
+
156
+ # Current size of pool
157
+ def size
158
+ @threads.size
159
+ end
160
+
161
+ # Maximum allowed number of threads
162
+ def max
163
+ @max_threads
164
+ end
165
+
166
+ # Minimum allowed number of threads
167
+ def min
168
+ @min_threads
169
+ end
170
+
171
+ # m:: new max
172
+ # Set maximum number of threads
173
+ def max=(m)
174
+ m = m.to_i
175
+ raise ArgumentError.new('Maximum value must be greater than 0') unless m > 0
176
+ @max_threads = m
177
+ @min_threads = m if m < @min_threads
178
+ resize if m < size
179
+ m
180
+ end
181
+
182
+ # m:: new min
183
+ # Set minimum number of threads
184
+ def min=(m)
185
+ m = m.to_i
186
+ raise ArgumentError.new("Minimum value must be greater than 0 and less than or equal to maximum (#{max})") unless m > 0 && m <= max
187
+ @min_threads = m
188
+ m
189
+ end
190
+
191
+ # t:: ActionPool::Thread to remove
192
+ # Removes a thread from the pool
193
+ def remove(t)
194
+ raise ArgumentError.new('Expecting an ActionPool::Thread object') unless t.is_a?(ActionPool::Thread)
195
+ t.stop
196
+ if(@threads.include?(t))
197
+ @threads.delete(t)
198
+ return true
199
+ else
200
+ return false
201
+ end
202
+ end
203
+
204
+ # Maximum number of seconds a thread
205
+ # is allowed to idle in the pool.
206
+ # (nil means thread life is infinite)
207
+ def thread_timeout
208
+ @thread_timeout
209
+ end
210
+
211
+ # Maximum number of seconds a thread
212
+ # is allowed to work on a given action
213
+ # (nil means thread is given unlimited
214
+ # time to work on action)
215
+ def action_timeout
216
+ @action_timeout
217
+ end
218
+
219
+ # t:: timeout in seconds (nil for infinite)
220
+ # Set maximum allowed time thead may idle in pool
221
+ def thread_timeout=(t)
222
+ t = t.to_f
223
+ raise ArgumentError.new('Value must be greater than zero or nil') unless t >= 0
224
+ @thread_timeout = t
225
+ @threads.each{|thread|thread.thread_timeout = t}
226
+ t
227
+ end
228
+
229
+ # t:: timeout in seconds (nil for infinte)
230
+ # Set maximum allowed time thread may work
231
+ # on a given action
232
+ def action_timeout=(t)
233
+ t = t.to_f
234
+ raise ArgumentError.new('Value must be greater than zero or nil') unless t >= 0
235
+ @action_timeout = t
236
+ @threads.each{|thread|thread.action_timeout = t}
237
+ t
238
+ end
239
+
240
+ # Returns the next action to be processed
241
+ def action
242
+ @queue.pop
243
+ end
244
+
245
+ # Number of actions in the queue
246
+ def action_size
247
+ @queue.size
248
+ end
249
+
250
+ # Flush the thread pool. Mainly used for forcibly resizing
251
+ # the pool if existing threads have a long thread life waiting
252
+ # for input.
253
+ def flush
254
+ lock = Mutex.new
255
+ guard = ConditionVariable.new
256
+ @threads.size.times{ queue{ lock.synchronize{ guard.wait(lock) } } }
257
+ Thread.pass
258
+ sleep(0.01)
259
+ lock.synchronize{ guard.broadcast }
260
+ end
261
+
262
+ # Returns current number of threads in the pool working
263
+ def working
264
+ @threads.find_all{|t|!t.waiting?}.size
265
+ end
266
+
267
+ private
268
+
269
+ # Resize the pool
270
+ def resize
271
+ @logger.info("Pool is being resized to stated maximum: #{max}")
272
+ until(size <= max) do
273
+ t = nil
274
+ t = @threads.find{|t|t.waiting?}
275
+ t = @threads.shift unless t
276
+ t.stop
277
+ end
278
+ flush
279
+ nil
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,260 @@
1
+ require 'actionpool/Thread'
2
+ require 'actionpool/Queue'
3
+ require 'actionpool/LogHelper'
4
+ require 'thread'
5
+
6
+ module ActionPool
7
+ # Raised when pool is closed
8
+ class PoolClosed < StandardError
9
+ end
10
+ class Pool
11
+
12
+ # :min_threads:: minimum number of threads in pool
13
+ # :max_threads:: maximum number of threads in pool
14
+ # :t_to:: thread timeout waiting for action to process
15
+ # :a_to:: maximum time action may be worked on before aborting
16
+ # :logger:: logger to print logging messages to
17
+ # Creates a new pool
18
+ def initialize(args={})
19
+ raise ArgumentError.new('Hash required for initialization') unless args.is_a?(Hash)
20
+ @logger = LogHelper.new(args[:logger])
21
+ @queue = ActionPool::Queue.new
22
+ @threads = []
23
+ @lock = Mutex.new
24
+ @thread_timeout = args[:t_to] ? args[:t_to] : 0
25
+ @action_timeout = args[:a_to] ? args[:a_to] : 0
26
+ @max_threads = args[:max_threads] ? args[:max_threads] : 100
27
+ @min_threads = args[:min_threads] ? args[:min_threads] : 10
28
+ @min_threads = @max_threads if @max_threads < @min_threads
29
+ @respond_to = args[:respond_thread] || ::Thread.current
30
+ @open = true
31
+ create_thread
32
+ end
33
+
34
+ # Pool is closed
35
+ def pool_closed?
36
+ !@open
37
+ end
38
+
39
+ # Pool is open
40
+ def pool_open?
41
+ @open
42
+ end
43
+
44
+ # arg:: :open or :closed
45
+ # Set pool status
46
+ def status(arg)
47
+ @open = arg == :open
48
+ end
49
+
50
+ # force:: force creation of a new thread
51
+ # Create a new thread for pool. Returns newly created ActionPool::Thread or
52
+ # nil if pool has reached maximum threads
53
+ def create_thread(force=false)
54
+ return if pool_closed?
55
+ pt = nil
56
+ @lock.synchronize do
57
+ if(@threads.size < @max_threads || force)
58
+ @logger.info('Pool is creating a new thread')
59
+ (min - size > 0 ? min - size : 1).times do |i|
60
+ pt = ActionPool::Thread.new(:pool => self, :respond_thread => @respond_to, :a_timeout => @action_timeout, :t_timeout => @thread_timeout, :logger => @logger)
61
+ @threads << pt
62
+ end
63
+ else
64
+ @logger.info('Pool is at maximum size. Not creating new thread')
65
+ end
66
+ end
67
+ return pt
68
+ end
69
+
70
+ # force:: force immediate stop
71
+ # Stop the pool
72
+ def shutdown(force=false)
73
+ args = [:wait]
74
+ args += [:force] if force
75
+ @logger.info("Pool is now shutting down #{force ? 'using force' : ''}")
76
+ @queue.wait_empty
77
+ while(t = @threads.pop) do
78
+ t.stop(*args)
79
+ end
80
+ nil
81
+ end
82
+
83
+ # action:: proc to be executed or array of [proc, [*args]]
84
+ # Add a new proc/lambda to be executed (alias for queue)
85
+ def <<(action)
86
+ raise PoolClosed.new("Pool #{self} is currently closed") if pool_closed?
87
+ case action
88
+ when Proc
89
+ queue(action)
90
+ when Array
91
+ raise ArgumentError.new('Actions to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]') unless action.size == 2 and action[0].is_a?(Proc) and action[1].is_a?(Array)
92
+ queue(action[0], action[1])
93
+ else
94
+ raise ArgumentError.new('Actions to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]')
95
+ end
96
+ nil
97
+ end
98
+
99
+ # action:: proc to be executed
100
+ # Add a new proc/lambda to be executed
101
+ def queue(action, *args)
102
+ raise ArgumentError.new('Expecting block') unless action.is_a?(Proc)
103
+ @queue << [action, args]
104
+ ::Thread.pass
105
+ create_thread if @queue.num_waiting < 1 # only start a new thread if we need it
106
+ end
107
+
108
+ # jobs:: Array of proc/lambdas
109
+ # Will queue a list of jobs into the pool
110
+ def add_jobs(jobs)
111
+ raise ArgumentError.new("Expecting an array but received: #{jobs.class}") unless jobs.is_a?(Array)
112
+ @queue.pause
113
+ begin
114
+ jobs.each do |job|
115
+ case job
116
+ when Proc
117
+ @queue << [job, []]
118
+ when Array
119
+ raise ArgumentError.new('Jobs to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]') unless job.size == 2 and job[0].is_a?(Proc) and job[1].is_a?(Array)
120
+ @queue << [job.unshift, job]
121
+ else
122
+ raise ArgumentError.new('Jobs to be processed by the pool must be a proc/lambda or [proc/lambda, [*args]]')
123
+ end
124
+ end
125
+ ensure
126
+ create_thread
127
+ @queue.unpause
128
+ end
129
+ true
130
+ end
131
+
132
+ # block:: block to process
133
+ # Adds a block to be processed
134
+ def process(*args, &block)
135
+ queue(block, *args)
136
+ nil
137
+ end
138
+
139
+ # Current size of pool
140
+ def size
141
+ @threads.size
142
+ end
143
+
144
+ # Maximum allowed number of threads
145
+ def max
146
+ @max_threads
147
+ end
148
+
149
+ # Minimum allowed number of threads
150
+ def min
151
+ @min_threads
152
+ end
153
+
154
+ # m:: new max
155
+ # Set maximum number of threads
156
+ def max=(m)
157
+ m = m.to_i
158
+ raise ArgumentError.new('Maximum value must be greater than 0') unless m > 0
159
+ @max_threads = m
160
+ @min_threads = m if m < @min_threads
161
+ resize if m < size
162
+ m
163
+ end
164
+
165
+ # m:: new min
166
+ # Set minimum number of threads
167
+ def min=(m)
168
+ m = m.to_i
169
+ raise ArgumentError.new("Minimum value must be greater than 0 and less than or equal to maximum (#{max})") unless m > 0 && m <= max
170
+ @min_threads = m
171
+ m
172
+ end
173
+
174
+ # t:: ActionPool::Thread to remove
175
+ # Removes a thread from the pool
176
+ def remove(t)
177
+ raise ArgumentError.new('Expecting an ActionPool::Thread object') unless t.is_a?(ActionPool::Thread)
178
+ t.stop
179
+ if(@threads.include?(t))
180
+ @threads.delete(t)
181
+ return true
182
+ else
183
+ return false
184
+ end
185
+ end
186
+
187
+ # Maximum number of seconds a thread
188
+ # is allowed to idle in the pool.
189
+ # (nil means thread life is infinite)
190
+ def thread_timeout
191
+ @thread_timeout
192
+ end
193
+
194
+ # Maximum number of seconds a thread
195
+ # is allowed to work on a given action
196
+ # (nil means thread is given unlimited
197
+ # time to work on action)
198
+ def action_timeout
199
+ @action_timeout
200
+ end
201
+
202
+ # t:: timeout in seconds (nil for infinite)
203
+ # Set maximum allowed time thead may idle in pool
204
+ def thread_timeout=(t)
205
+ t = t.to_f
206
+ raise ArgumentError.new('Value must be great than zero or nil') unless t > 0
207
+ @thread_timeout = t
208
+ @threads.each{|thread|thread.thread_timeout = t}
209
+ t
210
+ end
211
+
212
+ # t:: timeout in seconds (nil for infinte)
213
+ # Set maximum allowed time thread may work
214
+ # on a given action
215
+ def action_timeout=(t)
216
+ t = t.to_f
217
+ raise ArgumentError.new('Value must be great than zero or nil') unless t > 0
218
+ @action_timeout = t
219
+ @threads.each{|thread|thread.action_timeout = t}
220
+ t
221
+ end
222
+
223
+ # Returns the next action to be processed
224
+ def action
225
+ @queue.pop
226
+ end
227
+
228
+ # Number of actions in the queue
229
+ def action_size
230
+ @queue.size
231
+ end
232
+
233
+ # Flush the thread pool. Mainly used for forcibly resizing
234
+ # the pool if existing threads have a long thread life waiting
235
+ # for input.
236
+ def flush
237
+ lock = Mutex.new
238
+ guard = ConditionVariable.new
239
+ @threads.size.times{ queue{ lock.synchronize{ guard.wait(lock) } } }
240
+ Thread.pass
241
+ sleep(0.01)
242
+ lock.synchronize{ guard.broadcast }
243
+ end
244
+
245
+ private
246
+
247
+ # Resize the pool
248
+ def resize
249
+ @logger.info("Pool is being resized to stated maximum: #{max}")
250
+ until(size <= max) do
251
+ t = nil
252
+ t = @threads.find{|t|t.waiting?}
253
+ t = @threads.shift unless t
254
+ t.stop
255
+ end
256
+ flush
257
+ nil
258
+ end
259
+ end
260
+ end
@@ -0,0 +1,53 @@
1
+ require 'thread'
2
+
3
+ module ActionPool
4
+ # Adds a little bit extra functionality to the Queue class
5
+ class Queue < ::Queue
6
+ # Create a new Queue for the ActionPool::Pool
7
+ def initialize
8
+ super
9
+ @wait = false
10
+ @pause_lock = Mutex.new
11
+ @empty_lock = Mutex.new
12
+ @pause_guard = ConditionVariable.new
13
+ @empty_guard = ConditionVariable.new
14
+ end
15
+ # Stop the queue from returning results to requesting
16
+ # threads. Threads will wait for results until signalled
17
+ def pause
18
+ @pause_lock.synchronize{@wait = true}
19
+ end
20
+ # Allow the queue to return results. Any threads waiting
21
+ # will have results given to them.
22
+ def unpause
23
+ @pause_lock.synchronize do
24
+ @wait = false
25
+ @pause_guard.broadcast
26
+ end
27
+ end
28
+ # Check if queue needs to wait before returning
29
+ def pop
30
+ @pause_lock.synchronize do
31
+ @pause_guard.wait(@pause_lock) if @wait
32
+ end
33
+ o = super
34
+ @empty_lock.synchronize do
35
+ @empty_guard.broadcast if empty?
36
+ end
37
+ return o
38
+ end
39
+ # Clear queue
40
+ def clear
41
+ super
42
+ @empty_lock.synchronize do
43
+ @empty_guard.broadcast
44
+ end
45
+ end
46
+ # Park a thread here until queue is empty
47
+ def wait_empty
48
+ @empty_lock.synchronize do
49
+ @empty_guard.wait(@empty_lock) if size > 0
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,46 @@
1
+ require 'thread'
2
+
3
+ module ActionPool
4
+ # Adds a little bit extra functionality to the Queue class
5
+ class Queue < ::Queue
6
+ # Create a new Queue for the ActionPool::Pool
7
+ def initialize
8
+ super
9
+ @wait = false
10
+ @pause_lock = Mutex.new
11
+ @emtpy_lock = Mutex.new
12
+ @pause_guard = ConditionVariable.new
13
+ @empty_guard = ConditionVariable.new
14
+ end
15
+ # Stop the queue from returning results to requesting
16
+ # threads. Threads will wait for results until signalled
17
+ def pause
18
+ @pause_lock.synchronize{@wait = true}
19
+ end
20
+ # Allow the queue to return results. Any threads waiting
21
+ # will have results given to them.
22
+ def unpause
23
+ @pause_lock.synchronize do
24
+ @wait = false
25
+ @pause_guard.broadcast
26
+ end
27
+ end
28
+ # Check if queue needs to wait before returning
29
+ def pop
30
+ @pause_lock.synchronize do
31
+ @pause_guard.wait(@pause_lock) if @wait
32
+ end
33
+ o = super
34
+ @empty_lock.synchronize do
35
+ @empty_guard.broadcast if empty?
36
+ end
37
+ return o
38
+ end
39
+ # Park a thread here until queue is empty
40
+ def wait_empty
41
+ @empty_lock.synchronize do
42
+ @empty_guard.wait(@empty_lock) if size > 0
43
+ end
44
+ end
45
+ end
46
+ end