threadz 1.1.0.rc2 → 1.1.0.rc3
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.
- 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:
|