threadz 1.1.0.rc2 → 1.1.0.rc3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile.lock +1 -1
- data/README.rdoc +12 -6
- data/lib/threadz/atomic_integer.rb +1 -1
- data/lib/threadz/batch.rb +53 -38
- data/lib/threadz/directive.rb +1 -1
- data/lib/threadz/errors.rb +2 -7
- data/lib/threadz/sleeper.rb +1 -1
- data/lib/threadz/thread_pool.rb +29 -21
- data/lib/threadz/version.rb +1 -1
- data/spec/threadz_spec.rb +4 -4
- metadata +4 -1
data/Gemfile.lock
CHANGED
data/README.rdoc
CHANGED
@@ -42,14 +42,20 @@ This is a thread pool library that you can do two main things with, which I'll d
|
|
42
42
|
b.wait_until_done(:timeout => 0.1)
|
43
43
|
puts b.completed? ? "finished!" : "didn't finish"
|
44
44
|
|
45
|
-
#
|
45
|
+
# Error handling
|
46
|
+
b = T3.new_batch(:max_retries => 3)
|
47
|
+
b << lambda { raise }
|
48
|
+
b.wait_until_done
|
49
|
+
puts b.errors
|
46
50
|
|
47
|
-
|
51
|
+
b = T3.new_batch(:max_retries => 3, :error_handler => lambda { |error, control| puts "Error! #{error}" })
|
52
|
+
b << lambda { raise }
|
53
|
+
b.wait_until_done
|
48
54
|
|
49
|
-
|
55
|
+
# See the specs for more error handling stuff. Much better examples.
|
50
56
|
|
51
|
-
|
57
|
+
The thread pool is also smart -- depending on load, it can either spawn or cull additional threads (at a rate you can set).
|
52
58
|
|
53
|
-
==
|
59
|
+
== Examples
|
54
60
|
|
55
|
-
|
61
|
+
For examples, please see the well-documented specs. They're all fairly simple and straightforward. Please message me if you have issues that aren't answered by reading the spec.
|
@@ -5,7 +5,7 @@ module Threadz
|
|
5
5
|
# Provides a thread-safe integer counter thing.
|
6
6
|
# The code used in this file, while slightly verbose, is to optimize
|
7
7
|
# performance. Avoiding additional method calls and blocks is preferred.
|
8
|
-
class AtomicInteger
|
8
|
+
class AtomicInteger # :nodoc:
|
9
9
|
def initialize(value)
|
10
10
|
@value = value
|
11
11
|
@mutex = Mutex.new
|
data/lib/threadz/batch.rb
CHANGED
@@ -1,28 +1,41 @@
|
|
1
1
|
['atomic_integer', 'sleeper', 'errors'].each { |lib| require File.join(File.dirname(__FILE__), lib) }
|
2
2
|
|
3
3
|
module Threadz
|
4
|
-
# A batch is a collection of jobs
|
4
|
+
# A batch is a (typically related) collection of jobs that execute together on
|
5
5
|
# the attached thread pool. The calling thread can be signaled when the
|
6
6
|
# batch has completed executing, or a block can be executed.
|
7
|
+
# The easiest way to create a batch is with the ThreadPool method ThreadPool#new_batch:
|
8
|
+
# tp = Threadz::ThreadPool.new
|
9
|
+
# tp.new_batch(args)
|
10
|
+
# The options to new_batch get passed to Batch#initialize.
|
7
11
|
class Batch
|
8
12
|
# Creates a new batch attached to the given threadpool. A number of options
|
9
13
|
# are available:
|
10
|
-
#
|
11
|
-
#
|
14
|
+
# :latent [false]:: If latent, none of the jobs in the batch will actually start
|
15
|
+
# executing until the #start method is called.
|
16
|
+
# :max_retries [0]:: Specifies the maximum number of times to automatically retry a failed
|
17
|
+
# job. Defaults to 0.
|
18
|
+
# :error_handler [nil]:: Specifies the error handler to be invoked in the case of an error.
|
19
|
+
# It will be called like so: handler.call(error, control) where +error+ is the underlying error and
|
20
|
+
# +control+ is a Control for the job that had the error.
|
12
21
|
def initialize(threadpool, opts={})
|
13
22
|
@threadpool = threadpool
|
14
23
|
@job_lock = Mutex.new
|
15
24
|
@jobs_count = AtomicInteger.new(0)
|
16
|
-
@
|
25
|
+
@when_done_callbacks = []
|
17
26
|
@sleeper = ::Threadz::Sleeper.new
|
18
|
-
|
27
|
+
|
28
|
+
@error_lock = Mutex.new # Locked whenever the list of errors is read or modified
|
19
29
|
@job_errors = []
|
20
30
|
@error_handler_errors = []
|
31
|
+
|
21
32
|
@error_handler = opts[:error_handler]
|
22
33
|
if @error_handler && !@error_handler.respond_to?(:call)
|
23
34
|
raise ArgumentError.new("ErrorHandler must respond to #call")
|
24
35
|
end
|
25
|
-
|
36
|
+
|
37
|
+
@max_retries = opts[:max_retries] || 0
|
38
|
+
|
26
39
|
@verbose = opts[:verbose]
|
27
40
|
|
28
41
|
## Options
|
@@ -37,13 +50,13 @@ module Threadz
|
|
37
50
|
@job_queue = Queue.new if @latent
|
38
51
|
end
|
39
52
|
|
40
|
-
# Add a new job to the batch. If this is a latent batch, the job
|
53
|
+
# Add a new job to the batch. If this is a latent batch, the job won't
|
41
54
|
# be scheduled until the batch is #start'ed; otherwise it may start
|
42
55
|
# immediately. The job can be anything that responds to +call+ or an
|
43
56
|
# array of objects that respond to +call+.
|
44
57
|
def push(job)
|
45
58
|
if job.is_a? Array
|
46
|
-
job.each {|j| self
|
59
|
+
job.each { |j| self.push(j) }
|
47
60
|
elsif job.respond_to? :call
|
48
61
|
@jobs_count.increment
|
49
62
|
if @latent && !@started
|
@@ -58,10 +71,9 @@ module Threadz
|
|
58
71
|
|
59
72
|
alias << push
|
60
73
|
|
61
|
-
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
# for the batch to finish. Typically used with #completed?
|
74
|
+
# Blocks until the batch is done processing.
|
75
|
+
# +:timeout+ [nil]:: If specified, will only wait for this many seconds
|
76
|
+
# for the batch to finish. Typically used with #completed?
|
65
77
|
def wait_until_done(opts={})
|
66
78
|
raise "Threadz: thread deadlocked because batch job was never started" if @latent && !@started
|
67
79
|
|
@@ -80,46 +92,51 @@ module Threadz
|
|
80
92
|
|
81
93
|
# Returns the list of errors that occurred in the jobs
|
82
94
|
def job_errors
|
83
|
-
|
84
|
-
@error_lock.synchronize { arr = @job_errors.dup }
|
85
|
-
arr
|
95
|
+
@error_lock.synchronize { @job_errors.dup }
|
86
96
|
end
|
87
97
|
|
88
98
|
# Returns the list of errors that occurred in the error handler
|
89
99
|
def error_handler_errors
|
90
|
-
|
91
|
-
@error_lock.synchronize { arr = @error_handler_errors.dup }
|
92
|
-
arr
|
100
|
+
@error_lock.synchronize { @error_handler_errors.dup }
|
93
101
|
end
|
94
102
|
|
95
|
-
# If this is a latent batch, start processing all of the jobs in the queue.
|
103
|
+
# If this is a +latent+ batch, start processing all of the jobs in the queue.
|
96
104
|
def start
|
97
|
-
@job_lock.synchronize
|
98
|
-
if @latent
|
105
|
+
@job_lock.synchronize do # in case another thread tries to push new jobs onto the queue while we're starting
|
106
|
+
if @latent && !@started
|
99
107
|
@started = true
|
100
108
|
until @job_queue.empty?
|
101
|
-
|
109
|
+
job = @job_queue.pop
|
110
|
+
send_to_threadpool(job)
|
102
111
|
end
|
103
|
-
return true
|
104
|
-
else
|
105
|
-
return false
|
106
112
|
end
|
107
|
-
|
113
|
+
end
|
108
114
|
end
|
109
115
|
|
110
116
|
# Execute a given block when the batch has finished processing. If the batch
|
111
117
|
# has already finished executing, execute immediately.
|
112
118
|
def when_done(&block)
|
113
|
-
|
119
|
+
call_block = false
|
120
|
+
@job_lock.synchronize do
|
121
|
+
if completed?
|
122
|
+
call_block = true
|
123
|
+
else
|
124
|
+
@when_done_callbacks << block
|
125
|
+
end
|
126
|
+
end
|
127
|
+
yield if call_block
|
114
128
|
end
|
115
129
|
|
116
130
|
private
|
117
131
|
def handle_done
|
118
132
|
@sleeper.broadcast
|
119
|
-
|
120
|
-
|
133
|
+
callbacks = nil
|
134
|
+
@job_lock.synchronize do
|
135
|
+
callbacks = @when_done_callbacks.dup
|
136
|
+
@when_done_callbacks.clear
|
121
137
|
end
|
122
|
-
|
138
|
+
|
139
|
+
callbacks.each { |b| b.call }
|
123
140
|
end
|
124
141
|
|
125
142
|
def send_to_threadpool(job)
|
@@ -144,14 +161,12 @@ module Threadz
|
|
144
161
|
retry unless retries >= @max_retries
|
145
162
|
end
|
146
163
|
end
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
handle_done if completed?
|
154
|
-
#@job_lock.unlock
|
164
|
+
should_handle_done = false
|
165
|
+
@job_lock.synchronize do
|
166
|
+
@jobs_count.decrement
|
167
|
+
should_handle_done = completed?
|
168
|
+
end
|
169
|
+
handle_done if should_handle_done
|
155
170
|
end
|
156
171
|
end
|
157
172
|
end
|
data/lib/threadz/directive.rb
CHANGED
data/lib/threadz/errors.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module Threadz
|
2
|
+
# Generic class that all Threadz errors are a subclass of.
|
2
3
|
class ThreadzError < StandardError; end
|
3
4
|
|
5
|
+
# Thrown when a Job is the origin of an error. The original set of errors are available in the #errors field.
|
4
6
|
class JobError < ThreadzError
|
5
7
|
attr_reader :errors
|
6
8
|
def initialize(errors)
|
@@ -8,11 +10,4 @@ module Threadz
|
|
8
10
|
@errors = errors
|
9
11
|
end
|
10
12
|
end
|
11
|
-
class ErrorHandlerError < ThreadzError
|
12
|
-
attr_reader :error
|
13
|
-
def initialize(error)
|
14
|
-
super("An error occurred in the error handler itself (see #error)")
|
15
|
-
@error = error
|
16
|
-
end
|
17
|
-
end
|
18
13
|
end
|
data/lib/threadz/sleeper.rb
CHANGED
data/lib/threadz/thread_pool.rb
CHANGED
@@ -16,20 +16,25 @@ module Threadz
|
|
16
16
|
|
17
17
|
# Creates a new thread pool into which you can queue jobs.
|
18
18
|
# There are a number of options:
|
19
|
-
# :initial_size:: The number of threads you start out with initially. Also, the minimum number of threads.
|
20
|
-
#
|
21
|
-
# :
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
#
|
26
|
-
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
19
|
+
# :initial_size [10]:: The number of threads you start out with initially. Also, the minimum number of threads.
|
20
|
+
# :maximum_size [+initial_size+ * 5]:: The highest number of threads that can be allocated.
|
21
|
+
# :kill_threshold [10]::
|
22
|
+
# Constant that determines when new threads are needed or when threads can be killed off.
|
23
|
+
# To understand what this means, I'll briefly (ha) explain what's called the +killscore+, which is used to gauge
|
24
|
+
# utilization over time of the threadpool. It's just a number, and it starts at 0. It has a special relationship
|
25
|
+
# to the +kill_threshold+, which will now be explained.
|
26
|
+
# If the +killscore+ rises to positive +kill_threshold+, this indicates that the threadpool is *underutilized*,
|
27
|
+
# a thread is killed off (if we're over the minimum number of threads), and the +killscore+ is reset to 0.
|
28
|
+
# If the +killscore+ falls to negative kill_threshold, this indicates that the threadpool is *overutilized*,
|
29
|
+
# a new thread is created (if we're under the maximum number of threads), and the +killscore+ is reset to 0.
|
30
|
+
#
|
31
|
+
# Every 0.1 seconds, the state of all threads in the pool is checked.
|
32
|
+
# * If there is at least one idle thread (and we're above minimum size), the +killscore+ is incremented by THREADS_IDLE_SCORE for each idle thread.
|
33
|
+
# * If there are no idle threads (and we're below maximum size) the +killscore+ is decremented by THREADS_KILL_SCORE for each queued job.
|
34
|
+
# * If the thread pool is being perfectly utilized (no queued work or idle workers), the +killscore+ will decay by 10%.
|
35
|
+
#
|
36
|
+
# In the default case of kill_threshold=10, if the thread pool is overworked by one job for 10 consecutive checks (that is,
|
37
|
+
# 1 second), a new thread will be created and the counter reset. Similarly, if the thread pool is underutilized by one thread
|
33
38
|
# for 10 consecutive checks, an idle thread will be culled. If you want the thread pool to scale more quickly with
|
34
39
|
# demand, try lowering the kill_threshold value.
|
35
40
|
def initialize(opts={})
|
@@ -46,23 +51,24 @@ module Threadz
|
|
46
51
|
spawn_watch_thread
|
47
52
|
end
|
48
53
|
|
54
|
+
# Returns the number of worker threads this pool is currently managing.
|
49
55
|
def thread_count
|
50
56
|
@worker_threads_count.value
|
51
57
|
end
|
52
58
|
|
53
59
|
# Push a process onto the job queue for the thread pool to pick up.
|
54
60
|
# Note that using this method, you can't keep track of when the job
|
55
|
-
# finishes. If you care about when it finishes, use
|
61
|
+
# finishes. If you care about when it finishes, use a Batch (using #new_batch).
|
56
62
|
def process(callback = nil, &block)
|
57
63
|
callback ||= block
|
58
64
|
@queue << Control.new(callback)
|
59
65
|
nil
|
60
66
|
end
|
61
67
|
|
62
|
-
# Return a new batch that's attached into this thread pool. See
|
63
|
-
# for documention on opts
|
68
|
+
# Return a new batch that's attached into this thread pool. See Batch#new
|
69
|
+
# for documention on +opts+.
|
64
70
|
def new_batch(opts={})
|
65
|
-
|
71
|
+
Batch.new(self, opts)
|
66
72
|
end
|
67
73
|
|
68
74
|
private
|
@@ -88,6 +94,8 @@ module Threadz
|
|
88
94
|
end
|
89
95
|
|
90
96
|
# Kill a thread after it completes its current job
|
97
|
+
# NOTE: Currently this doesn't really work because it pushes a "suicide pill" on the END of the list of jobs,
|
98
|
+
# due to a technical limitation with Ruby's standard Queue.
|
91
99
|
def kill_thread
|
92
100
|
# TODO: ideally this would be unshift, but Queues don't have that. Come up with an alternative.
|
93
101
|
@queue << Directive::SUICIDE_PILL
|
@@ -107,11 +115,11 @@ module Threadz
|
|
107
115
|
@killscore -= THREADS_BUSY_SCORE * @queue.length
|
108
116
|
|
109
117
|
else
|
110
|
-
# Decay
|
111
|
-
if
|
118
|
+
# Decay
|
119
|
+
if @killscore != 0 # documented
|
112
120
|
@killscore *= 0.9
|
113
121
|
end
|
114
|
-
if
|
122
|
+
if @killscore.abs < 1
|
115
123
|
@killscore = 0
|
116
124
|
end
|
117
125
|
end
|
data/lib/threadz/version.rb
CHANGED
data/spec/threadz_spec.rb
CHANGED
@@ -233,12 +233,12 @@ describe Threadz do
|
|
233
233
|
b.wait_until_done
|
234
234
|
error.should_not be_nil
|
235
235
|
end
|
236
|
-
it "should retry
|
236
|
+
it "should not retry by default" do
|
237
237
|
count = 0
|
238
238
|
b = @T.new_batch(:error_handler => lambda { |e, ctrl| count += 1 })
|
239
239
|
b << lambda { raise }
|
240
240
|
b.wait_until_done
|
241
|
-
count.should ==
|
241
|
+
count.should == 1
|
242
242
|
end
|
243
243
|
it "should retry up to the designated number of times" do
|
244
244
|
count = 0
|
@@ -260,8 +260,8 @@ describe Threadz do
|
|
260
260
|
b = @T.new_batch(:error_handler => lambda { |e, ctrl| raise })
|
261
261
|
b << lambda { raise }
|
262
262
|
b.wait_until_done
|
263
|
-
b.job_errors.length.should ==
|
264
|
-
b.error_handler_errors.length.should ==
|
263
|
+
b.job_errors.length.should == 1
|
264
|
+
b.error_handler_errors.length.should == 1
|
265
265
|
end
|
266
266
|
it "should allow you to respond to errors on a per-job basis" do
|
267
267
|
job1 = lambda { 1 + 2 }
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: threadz
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.1.0.
|
4
|
+
version: 1.1.0.rc3
|
5
5
|
prerelease: 6
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -69,6 +69,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
69
69
|
- - ! '>='
|
70
70
|
- !ruby/object:Gem::Version
|
71
71
|
version: '0'
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
hash: 2507925423504722291
|
72
75
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
73
76
|
none: false
|
74
77
|
requirements:
|