actionpool 0.2.2

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