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 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: