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,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
+ }