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 CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- threadz (1.1.0.rc1)
4
+ threadz (1.1.0.rc3)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
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
- # Exception handling: well-supported, see the specs though. Much better examples.
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
- The thread pool is also smart -- depending on load, it can either spawn or cull additional threads (at a rate you can set).
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
- == Examples
55
+ # See the specs for more error handling stuff. Much better examples.
50
56
 
51
- For examples, please see the well-documented specs. They're all fairly simple and straightforward. Please message me if they're not.
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
- == Disclaimer
59
+ == Examples
54
60
 
55
- Consider this product in late alpha. There are still some bugs to be worked out and the API may change.
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 you care about that gets pushed off to
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
- # +:latent+:: If latent, none of the jobs in the batch will actually start
11
- # executing until the +start+ method is called.
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
- @when_done_blocks = []
25
+ @when_done_callbacks = []
17
26
  @sleeper = ::Threadz::Sleeper.new
18
- @error_lock = Mutex.new
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
- @max_retries = opts[:max_retries] || 3
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 can't
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 << j}
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
- # Put the current thread to sleep until the batch is done processing.
62
- # There are options available:
63
- # +:timeout+:: If specified, will only wait for at least this many seconds
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
- arr = nil
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
- arr = nil
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 { # in case another thread tries to push new jobs onto the queue while we're starting
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
- send_to_threadpool(@job_queue.pop)
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
- @job_lock.synchronize { completed? ? block.call : @when_done_blocks << block }
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
- @when_done_blocks.each do |b|
120
- b.call
133
+ callbacks = nil
134
+ @job_lock.synchronize do
135
+ callbacks = @when_done_callbacks.dup
136
+ @when_done_callbacks.clear
121
137
  end
122
- @when_done_blocks = []
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
- # Lock in case we get two threads at the "fork in the road" at the same time
148
- # Note: locking here actually creates undesirable behavior. Still investigating why,
149
- # seems like it should be useful.
150
- #@job_lock.lock
151
- @jobs_count.decrement
152
- # fork in the road
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
@@ -1,6 +1,6 @@
1
1
  module Threadz
2
2
  # Directives: Special instructions for threads that are communicated via the queue
3
- class Directive
3
+ class Directive # :nodoc: all
4
4
  # The thread that consumes this directive immediately dies
5
5
  SUICIDE_PILL = "__THREADZ_SUICIDE_PILL"
6
6
  end
@@ -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
@@ -2,7 +2,7 @@ require 'thread'
2
2
  require 'timeout'
3
3
 
4
4
  module Threadz
5
- class Sleeper
5
+ class Sleeper # :nodoc: all
6
6
  def initialize
7
7
  @waiters = Queue.new
8
8
  end
@@ -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
- # By default, this is 10.
21
- # :maximum_size:: The highest number of threads that can be allocated. By default, this is the minimum size x 5.
22
- # :kill_threshold:: Constant that determines when new threads are needed or when threads can be killed off.
23
- # If the internally tracked kill score falls to positive kill_threshold, then a thread is killed off and the
24
- # kill score is reset. If the kill score rises to negative kill_threshold, then a new thread
25
- # is created and the kill score is reset. Every 0.1 seconds, the state of all threads in the
26
- # pool is checked. If there is more than one idle thread (and we're above minimum size), the
27
- # kill score is incremented by THREADS_IDLE_SCORE for each idle thread. If there are no idle threads
28
- # (and we're below maximum size) the kill score is decremented by THREADS_KILL_SCORE for each queued job.
29
- # If the thread pool is being perfectly utilized (no queued work or idle workers), the kill score will decay
30
- # and lose 10% of its value.
31
- # In the default case of kill_threshold=10, if the thread pool is overworked for 10 consecutive checks (that is,
32
- # 1 second), a new thread will be created and the counter reset. Similarly, if the thread pool is underutilized
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 batches.
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 Threadz::ThreadPool::Batch
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
- return Batch.new(self, opts)
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(@killscore != 0)
118
+ # Decay
119
+ if @killscore != 0 # documented
112
120
  @killscore *= 0.9
113
121
  end
114
- if(@killscore.abs < 1)
122
+ if @killscore.abs < 1
115
123
  @killscore = 0
116
124
  end
117
125
  end
@@ -1,4 +1,4 @@
1
1
  module Threadz
2
- VERSION = "1.1.0.rc2"
2
+ VERSION = "1.1.0.rc3"
3
3
  end
4
4
 
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 up to 3 times by default" do
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 == 3
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 == 3
264
- b.error_handler_errors.length.should == 3
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.rc2
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: