actionpool 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,155 @@
1
+ require 'timeout'
2
+
3
+ module ActionPool
4
+ # Exception class used for waking up a thread
5
+ class Wakeup < StandardError
6
+ end
7
+ # Raised within a thread when the timeout is changed
8
+ class Retimeout < StandardError
9
+ end
10
+ class Thread
11
+ # :pool:: pool thread is associated with
12
+ # :t_timeout:: max time a thread is allowed to wait for action
13
+ # :a_timeout:: max time thread is allowed to work
14
+ # :respond_thread:: thread to send execptions to
15
+ # :logger:: LogHelper for logging messages
16
+ # Create a new thread
17
+ def initialize(args)
18
+ raise ArgumentError.new('Hash required for initialization') unless args.is_a?(Hash)
19
+ raise ArgumentError.new('ActionPool::Thread requires a pool') unless args[:pool]
20
+ raise ArgumentError.new('ActionPool::Thread requries thread to respond') unless args[:respond_thread]
21
+ @pool = args[:pool]
22
+ @respond_to = args[:respond_thread]
23
+ @thread_timeout = args[:t_timeout] ? args[:t_timeout].to_f : 0
24
+ @action_timeout = args[:a_timeout] ? args[:a_timeout].to_f : 0
25
+ @kill = false
26
+ @logger = args[:logger].is_a?(LogHelper) ? args[:logger] : LogHelper.new(args[:logger])
27
+ @lock = Mutex.new
28
+ @thread = ::Thread.new{ start_thread }
29
+ end
30
+
31
+ # :force:: force the thread to stop
32
+ # :wait:: wait for the thread to stop
33
+ # Stop the thread
34
+ def stop(*args)
35
+ @kill = true
36
+ @thread.raise Wakeup.new if args.include?(:force) || waiting?
37
+ nil
38
+ end
39
+
40
+ # Currently waiting
41
+ def waiting?
42
+ @lock.synchronize{@status} == :wait
43
+ end
44
+
45
+ # Is the thread still alive
46
+ def alive?
47
+ @thread.alive?
48
+ end
49
+
50
+ # Current thread status
51
+ def status
52
+ @lock.synchronize{ return @status }
53
+ end
54
+
55
+ # arg:: :wait or :run
56
+ # Set current status
57
+ def status(arg)
58
+ raise InvalidType.new('Status can only be set to :wait or :run') unless arg == :wait || arg == :run
59
+ @lock.synchronize{ @status = arg }
60
+ end
61
+
62
+ # Seconds thread will wait for input
63
+ def thread_timeout
64
+ @thread_timeout
65
+ end
66
+
67
+ # Seconds thread will spend working on a given task
68
+ def action_timeout
69
+ @action_timeout
70
+ end
71
+
72
+ # t:: seconds to wait for input (floats allow for values 0 < t < 1)
73
+ # Set the maximum amount of time to wait for a task
74
+ def thread_timeout=(t)
75
+ t = t.to_f
76
+ raise ArgumentError.new('Value must be great than zero or nil') unless t > 0
77
+ @thread_timeout = t
78
+ @thread.raise Retimeout.new if waiting?
79
+ t
80
+ end
81
+
82
+ # t:: seconds to work on a task (floats allow for values 0 < t < 1)
83
+ # Set the maximum amount of time to work on a given task
84
+ # Note: Modification of this will not affect actions already in process
85
+ def action_timeout=(t)
86
+ t = t.to_f
87
+ raise ArgumentError.new('Value must be great than zero or nil') unless t > 0
88
+ @action_timeout = t
89
+ t
90
+ end
91
+
92
+ private
93
+
94
+ # Start our thread
95
+ def start_thread
96
+ begin
97
+ @logger.info("New pool thread is starting (#{self})")
98
+ until(@kill) do
99
+ status(:wait)
100
+ begin
101
+ action = nil
102
+ if(@pool.size > @pool.min && !@thread_timeout.zero?)
103
+ Timeout::timeout(@thread_timeout) do
104
+ action = @pool.action
105
+ end
106
+ else
107
+ action = @pool.action
108
+ end
109
+ status(:run)
110
+ run(action[0], action[1]) unless action.nil?
111
+ status(:wait)
112
+ rescue Timeout::Error => boom
113
+ @kill = true
114
+ rescue Wakeup
115
+ @logger.info("Thread #{::Thread.current} was woken up.")
116
+ rescue Retimeout
117
+ @logger.warn('Thread was woken up to reset thread timeout')
118
+ rescue Exception => boom
119
+ @logger.error("Pool thread caught an exception: #{boom}\n#{boom.backtrace.join("\n")}")
120
+ @respond_to.raise boom
121
+ end
122
+ end
123
+ rescue Retimeout
124
+ @logger.warn('Thread was woken up to reset thread timeout')
125
+ retry
126
+ rescue Wakeup
127
+ @logger.info("Thread #{::Thread.current} was woken up.")
128
+ rescue Exception => boom
129
+ @logger.error("Pool thread caught an exception: #{boom}\n#{boom.backtrace.join("\n")}")
130
+ @respond_to.raise boom
131
+ ensure
132
+ @logger.info("Pool thread is shutting down (#{self})")
133
+ @pool.remove(self)
134
+ @pool.create_thread
135
+ end
136
+ end
137
+
138
+ # action:: task to be run
139
+ # args:: arguments to be passed to task
140
+ # Run the task
141
+ def run(action, args)
142
+ begin
143
+ unless(@action_timeout.zero?)
144
+ Timeout::timeout(@action_timeout) do
145
+ action.call(*args[0])
146
+ end
147
+ else
148
+ action.call(*args[0])
149
+ end
150
+ rescue Timeout::Error => boom
151
+ @logger.warn("Pool thread reached max execution time for action: #{boom}")
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,146 @@
1
+ require 'timeout'
2
+
3
+ module ActionPool
4
+ # Exception class used for waking up a thread
5
+ class Wakeup < StandardError
6
+ end
7
+
8
+ class Thread
9
+ # :pool:: pool thread is associated with
10
+ # :t_timeout:: max time a thread is allowed to wait for action
11
+ # :a_timeout:: max time thread is allowed to work
12
+ # :respond_thread:: thread to send execptions to
13
+ # :logger:: LogHelper for logging messages
14
+ # Create a new thread
15
+ def initialize(args)
16
+ raise ArgumentError.new('Hash required for initialization') unless args.is_a?(Hash)
17
+ raise ArgumentError.new('ActionPool::Thread requires a pool') unless args[:pool]
18
+ raise ArgumentError.new('ActionPool::Thread requries thread to respond') unless args[:respond_thread]
19
+ @pool = args[:pool]
20
+ @respond_to = args[:respond_thread]
21
+ @thread_timeout = args[:t_timeout] ? args[:t_timeout].to_f : 0
22
+ @action_timeout = args[:a_timeout] ? args[:a_timeout].to_f : 0
23
+ @kill = false
24
+ @logger = args[:logger].is_a?(LogHelper) ? args[:logger] : LogHelper.new(args[:logger])
25
+ @lock = Mutex.new
26
+ @thread = ::Thread.new{ start_thread }
27
+ end
28
+
29
+ # :force:: force the thread to stop
30
+ # :wait:: wait for the thread to stop
31
+ # Stop the thread
32
+ def stop(*args)
33
+ @kill = true
34
+ @thread.raise Wakeup.new if args.include?(:force) || waiting?
35
+ nil
36
+ end
37
+
38
+ # Currently waiting
39
+ def waiting?
40
+ @status == :wait
41
+ end
42
+
43
+ # Is the thread still alive
44
+ def alive?
45
+ @thread.alive?
46
+ end
47
+
48
+ # Current thread status
49
+ def status
50
+ @lock.synchronize{ return @status }
51
+ end
52
+
53
+ # arg:: :wait or :run
54
+ # Set current status
55
+ def status(arg)
56
+ raise InvalidType.new('Status can only be set to :wait or :run') unless arg == :wait || arg == :run
57
+ @lock.synchronize{ @status = arg }
58
+ end
59
+
60
+ # Seconds thread will wait for input
61
+ def thread_timeout
62
+ @thread_timeout
63
+ end
64
+
65
+ # Seconds thread will spend working on a given task
66
+ def action_timeout
67
+ @action_timeout
68
+ end
69
+
70
+ # t:: seconds to wait for input (floats allow for values 0 < t < 1)
71
+ # Set the maximum amount of time to wait for a task
72
+ def thread_timeout=(t)
73
+ t = t.to_f
74
+ raise ArgumentError.new('Value must be great than zero or nil') unless t > 0
75
+ @thread_timeout = t
76
+ t
77
+ end
78
+
79
+ # t:: seconds to work on a task (floats allow for values 0 < t < 1)
80
+ # Set the maximum amount of time to work on a given task
81
+ def action_timeout=(t)
82
+ t = t.to_f
83
+ raise ArgumentError.new('Value must be great than zero or nil') unless t > 0
84
+ @action_timeout = t
85
+ t
86
+ end
87
+
88
+ private
89
+
90
+ # Start our thread
91
+ def start_thread
92
+ begin
93
+ @logger.info("New pool thread is starting (#{self})")
94
+ until(@kill) do
95
+ status(:wait)
96
+ begin
97
+ action = nil
98
+ if(@pool.size > @pool.min)
99
+ Timeout::timeout(@thread_timeout) do
100
+ action = @pool.action
101
+ end
102
+ else
103
+ action = @pool.action
104
+ end
105
+ status(:run)
106
+ run(action[0], action[1]) unless action.nil?
107
+ status(:wait)
108
+ rescue Timeout::Error => boom
109
+ @kill = true
110
+ rescue Wakeup
111
+ @logger.info("Thread #{::Thread.current} was woken up.")
112
+ rescue Exception => boom
113
+ @logger.error("Pool thread caught an exception: #{boom}\n#{boom.backtrace.join("\n")}")
114
+ @respond_to.raise boom
115
+ end
116
+ end
117
+ rescue Wakeup
118
+ @logger.info("Thread #{::Thread.current} was woken up.")
119
+ rescue Exception => boom
120
+ @logger.error("Pool thread caught an exception: #{boom}\n#{boom.backtrace.join("\n")}")
121
+ @respond_to.raise boom
122
+ ensure
123
+ @logger.info("Pool thread is shutting down (#{self})")
124
+ @pool.remove(self)
125
+ @pool.create_threads
126
+ end
127
+ end
128
+
129
+ # action:: task to be run
130
+ # args:: arguments to be passed to task
131
+ # Run the task
132
+ def run(action, args)
133
+ begin
134
+ if(@action_timeout > 0)
135
+ Timeout::timeout(@action_timeout) do
136
+ action.call(*args.flatten)
137
+ end
138
+ else
139
+ action.call(*args[0])
140
+ end
141
+ rescue Timeout::Error => boom
142
+ @logger.warn("Pool thread reached max execution time for action: #{boom}")
143
+ end
144
+ end
145
+ end
146
+ end
data/lib/actionpool.rb ADDED
@@ -0,0 +1,7 @@
1
+ begin
2
+ require 'fastthread'
3
+ rescue LoadError
4
+ # we don't care if it's available
5
+ # just load it if it's around
6
+ end
7
+ require 'actionpool/Pool'
data/test.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'actionpool'
2
+
3
+ pool = ActionPool::Pool.new
4
+
5
+ pool << [lambda{|x,y| puts "#{x}, #{y}"}, [1,[:a, :b]]]
6
+
7
+ sleep(1)
@@ -0,0 +1,52 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class GeneralPoolTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new
7
+ end
8
+ def teardown
9
+ @pool.shutdown(true)
10
+ end
11
+ def test_numbers
12
+ assert_equal(10, @pool.size)
13
+ assert_equal(10, @pool.min)
14
+ assert_equal(100, @pool.max)
15
+ assert_equal(0, @pool.action_timeout)
16
+ assert_equal(0, @pool.thread_timeout)
17
+ assert_equal(0, @pool.action_size)
18
+ end
19
+ def test_output
20
+ a = 0
21
+ lock = Mutex.new
22
+ run = lambda{ lock.synchronize{ a += 1 } }
23
+ 100.times{ @pool << run }
24
+ @pool.shutdown
25
+ assert_equal(100, a)
26
+ @pool.status :open
27
+ a = 0
28
+ jobs = [].fill(run,0,100)
29
+ @pool.add_jobs(jobs)
30
+ @pool.shutdown
31
+ assert_equal(100, a)
32
+ @pool.shutdown(true)
33
+ end
34
+ def test_args
35
+ @pool.status :open
36
+ output = nil
37
+ @pool << [lambda{|x| output = x}, [2]]
38
+ assert(2, output)
39
+ @pool.add_jobs([[lambda{|x| output = x}, [3]]])
40
+ assert(3, output)
41
+ @pool << [lambda{|x,y| output = x+y}, [1,2]]
42
+ assert(3, output)
43
+ output = []
44
+ @pool.add_jobs([[lambda{|x,y| output << x + y}, [1,1]], [lambda{|x| output << x}, [3]]])
45
+ assert(output.include?(2))
46
+ assert(output.include?(3))
47
+ @pool << [lambda{|x,y| output = [x,y]}, ['test', [1,2]]]
48
+ assert_equal(output[0], 'test')
49
+ assert(output[1].is_a?(Array))
50
+ @pool.shutdown(true)
51
+ end
52
+ end
@@ -0,0 +1,45 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class GeneralPoolTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new
7
+ end
8
+ def test_numbers
9
+ assert_equal(10, @pool.size)
10
+ assert_equal(10, @pool.min)
11
+ assert_equal(100, @pool.max)
12
+ assert_equal(0, @pool.action_timeout)
13
+ assert_equal(0, @pool.thread_timeout)
14
+ assert_equal(0, @pool.action_size)
15
+ end
16
+ def test_output
17
+ a = 0
18
+ lock = Mutex.new
19
+ run = lambda{ lock.synchronize{ a += 1 } }
20
+ 100.times{ @pool << run }
21
+ @pool.shutdown
22
+ assert_equal(100, a)
23
+ a = 0
24
+ jobs = [].fill(run,0,100)
25
+ @pool.add_jobs(jobs)
26
+ @pool.shutdown
27
+ assert_equal(100, a)
28
+ @pool.shutdown(true)
29
+ end
30
+ def test_args
31
+ output = nil
32
+ @pool << [lambda{|x| output = x}, [2]]
33
+ assert(2, output)
34
+ @pool.add_jobs([[lambda{|x| output = x}, [3]]])
35
+ assert(3, output)
36
+ @pool << [lambda{|x,y| output = x+y}, [1,2]]
37
+ # assert(3, output)
38
+ sleep(0.01)
39
+ output = []
40
+ @pool.add_jobs([[lambda{|x,y| output << x + y}, [1,1]], [lambda{|x| output << x}, [3]]])
41
+ # assert(output.include?(2))
42
+ # assert(output.include?(3))
43
+ @pool.shutdown(true)
44
+ end
45
+ end
@@ -0,0 +1,24 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class GrowPoolTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new
7
+ end
8
+ def teardown
9
+ @pool.shutdown(true)
10
+ end
11
+ def test_grow
12
+ jobs = [].fill(lambda{sleep}, 0..20)
13
+ @pool.add_jobs(jobs)
14
+ Thread.pass
15
+ assert(@pool.size > 10)
16
+ @pool.shutdown(true)
17
+ end
18
+ def test_max
19
+ @pool.create_thread(:force) until @pool.size > @pool.max
20
+ assert(@pool.create_thread.nil?)
21
+ assert(!@pool.create_thread(:force).nil?)
22
+ @pool.shutdown(true)
23
+ end
24
+ end
@@ -0,0 +1,15 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class NoGrowPoolTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new
7
+ end
8
+ def teardown
9
+ @pool.shutdown(true)
10
+ end
11
+ def test_nogrow
12
+ 5.times{ @pool << lambda{} }
13
+ assert_equal(10, @pool.size)
14
+ end
15
+ end
@@ -0,0 +1,34 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class QueueTest < Test::Unit::TestCase
5
+ def setup
6
+ @queue = ActionPool::Queue.new
7
+ end
8
+ def test_pop
9
+ 3.times{|i|@queue << i}
10
+ 3.times{|i|assert(i, @queue.pop)}
11
+ assert(@queue.empty?)
12
+ end
13
+ def test_pause
14
+ 3.times{|i|@queue << i}
15
+ @queue.pause
16
+ output = []
17
+ 3.times{Thread.new{output << @queue.pop}}
18
+ assert(output.empty?)
19
+ assert_equal(3, @queue.size)
20
+ @queue.unpause
21
+ sleep(1)
22
+ assert(@queue.empty?)
23
+ assert_equal(3, output.size)
24
+ 3.times{|i|assert(output.include?(i))}
25
+ @queue << 1
26
+ output = nil
27
+ Thread.new{@queue.wait_empty; output = true}
28
+ assert_nil(output)
29
+ @queue.pop
30
+ Thread.pass
31
+ sleep(0.01)
32
+ assert(output)
33
+ end
34
+ end
@@ -0,0 +1,35 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class ResizePoolTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new
7
+ end
8
+ def teardown
9
+ @pool.shutdown(true)
10
+ end
11
+ def test_resize
12
+ stop = false
13
+ 20.times{ @pool << lambda{ sleep } }
14
+ Thread.pass
15
+ sleep(0.1)
16
+ assert(@pool.size > 10)
17
+ stop = true
18
+ @pool.shutdown(true)
19
+ @pool.status :open
20
+ @pool.max = 10
21
+ assert_equal(10, @pool.max)
22
+ stop = false
23
+ 20.times{ @pool << lambda{ a = 0; a += 1 until stop || a > 9999999999 } }
24
+ assert_equal(10, @pool.size)
25
+ stop = true
26
+ @pool.shutdown(true)
27
+ @pool.status :open
28
+ @pool.max = 20
29
+ stop = false
30
+ 30.times{ @pool << lambda{ a = 0; a += 1 until stop || a > 9999999999 } }
31
+ stop = true
32
+ assert(@pool.size > 10)
33
+ @pool.shutdown(true)
34
+ end
35
+ end
@@ -0,0 +1,36 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class ShutdownPoolTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new
7
+ end
8
+ def teardown
9
+ @pool.shutdown(true)
10
+ end
11
+ def test_close
12
+ result = 0
13
+ @pool << lambda{ result = 5 }
14
+ sleep(0.01)
15
+ assert(5, result)
16
+ @pool.status :closed
17
+ assert_raise(ActionPool::PoolClosed) do
18
+ @pool << lambda{}
19
+ end
20
+ assert_raise(ActionPool::PoolClosed) do
21
+ @pool.add_jobs [lambda{}, lambda{}]
22
+ end
23
+ @pool.shutdown(true)
24
+ end
25
+ def test_shutdown
26
+ assert_equal(10, @pool.size)
27
+ @pool.shutdown
28
+ sleep(0.5)
29
+ assert_equal(0, @pool.size)
30
+ @pool.status :open
31
+ @pool << lambda{ sleep }
32
+ sleep(0.01)
33
+ assert_equal(10, @pool.size)
34
+ @pool.shutdown(true)
35
+ end
36
+ end
@@ -0,0 +1,27 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class ThreadTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new(:min_threads => 1, :max_threads => 1)
7
+ @thread = ActionPool::Thread.new(:pool => @pool, :respond_thread => self, :t_timeout => 60, :a_timeout => 0)
8
+ end
9
+ def teardown
10
+ @pool.shutdown(true)
11
+ end
12
+ def test_thread
13
+ sleep(0.01)
14
+ assert(@thread.waiting?)
15
+ assert_equal(60, @thread.thread_timeout)
16
+ assert_equal(0, @thread.action_timeout)
17
+ assert(@thread.alive?)
18
+ stop = false
19
+ 10.times{ @pool << lambda{ a = 0; a += 1 until stop || a > 9999999999 } }
20
+ assert(!@thread.waiting?)
21
+ @thread.stop(:force)
22
+ sleep(0.01)
23
+ assert(!@thread.alive?)
24
+ stop = true
25
+ @pool.shutdown(true)
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ require 'actionpool'
2
+ require 'test/unit'
3
+
4
+ class TimeoutPoolTest < Test::Unit::TestCase
5
+ def setup
6
+ @pool = ActionPool::Pool.new
7
+ end
8
+ def teardown
9
+ @pool.shutdown(true)
10
+ end
11
+ def test_actiontimeout
12
+ @pool.action_timeout = 0.01
13
+ assert_equal(10, @pool.size)
14
+ stop = false
15
+ @pool.add_jobs [].fill(lambda{loop{ 1+1 }}, 0, 20)
16
+ ::Thread.pass
17
+ assert(@pool.working > 10)
18
+ stop = true
19
+ sleep(0.5)
20
+ assert(@pool.working == 0)
21
+ @pool.shutdown(true)
22
+ end
23
+ def test_threadtimeout
24
+ @pool.thread_timeout = 0.01
25
+ assert_equal(10, @pool.size)
26
+ lock = Mutex.new
27
+ guard = ConditionVariable.new
28
+ @pool.add_jobs [].fill(lambda{ lock.synchronize{ guard.wait(lock) } }, 0, 20)
29
+ ::Thread.pass
30
+ assert_equal(30, @pool.size)
31
+ lock.synchronize{ guard.broadcast }
32
+ ::Thread.pass
33
+ sleep(0.1)
34
+ assert(10, @pool.size)
35
+ @pool.shutdown(true)
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.expand_path("#{__FILE__}/../../lib"))
2
+
3
+ require 'test/unit'
4
+ require 'actionpool'
5
+
6
+ Dir.new("#{File.dirname(__FILE__)}/cases").each{|f|
7
+ require "#{File.dirname(__FILE__)}/cases/#{f}" if f[-2..f.size] == 'rb'
8
+ }