threadz 1.0.0 → 1.1.0.rc2

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/.gitignore CHANGED
File without changes
@@ -0,0 +1,9 @@
1
+ language: ruby
2
+ rvm:
3
+ - "1.9.2"
4
+ - "1.9.3"
5
+ - jruby-19mode # JRuby in 1.9 mode
6
+ - rbx-19mode
7
+ - ruby-head
8
+ # uncomment this line if your project needs to run something other than `rake`:
9
+ # script: bundle exec rspec spec
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ 1.1.0
2
+ =====
3
+ Improved exception handling!
4
+
5
+ 1.0.0
6
+ =====
7
+ No changes from 1.0.0.beta
8
+
1
9
  1.0.0.beta
2
10
  ==========
3
11
  Removing rcov and jeweler dependencies, fixing rspec at 1.x, adding some
data/Gemfile CHANGED
@@ -2,4 +2,5 @@ source "http://rubygems.org"
2
2
 
3
3
  # Specify your gem's dependencies in threadz.gemspec
4
4
  gemspec
5
- gem "rake"
5
+ gem "rake", "~> 10.0"
6
+ gem "rdoc", "~> 3.12"
@@ -1,18 +1,32 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- threadz (1.0.0.beta)
4
+ threadz (1.1.0.rc1)
5
5
 
6
6
  GEM
7
7
  remote: http://rubygems.org/
8
8
  specs:
9
- rake (0.9.2.2)
10
- rspec (1.3.2)
9
+ diff-lcs (1.1.3)
10
+ json (1.7.5)
11
+ json (1.7.5-java)
12
+ rake (10.0.3)
13
+ rdoc (3.12)
14
+ json (~> 1.4)
15
+ rspec (2.12.0)
16
+ rspec-core (~> 2.12.0)
17
+ rspec-expectations (~> 2.12.0)
18
+ rspec-mocks (~> 2.12.0)
19
+ rspec-core (2.12.2)
20
+ rspec-expectations (2.12.1)
21
+ diff-lcs (~> 1.1.3)
22
+ rspec-mocks (2.12.0)
11
23
 
12
24
  PLATFORMS
25
+ java
13
26
  ruby
14
27
 
15
28
  DEPENDENCIES
16
- rake
17
- rspec (~> 1.0)
29
+ rake (~> 10.0)
30
+ rdoc (~> 3.12)
31
+ rspec (~> 2.12)
18
32
  threadz!
File without changes
@@ -1,5 +1,9 @@
1
1
  = Threadz Thread Pool Library
2
2
 
3
+ {<img src="https://travis-ci.org/nanodeath/threadz.png?branch=master" alt="Build Status" />}[https://travis-ci.org/nanodeath/threadz]
4
+ {<img src="https://codeclimate.com/badge.png" />}[https://codeclimate.com/github/nanodeath/threadz]
5
+ {<img src="https://gemnasium.com/nanodeath/threadz.png" alt="Dependency Status" />}[https://gemnasium.com/nanodeath/threadz]
6
+
3
7
  == Description
4
8
 
5
9
  This is a thread pool library that you can do two main things with, which I'll demonstrate in code:
@@ -38,6 +42,8 @@ This is a thread pool library that you can do two main things with, which I'll d
38
42
  b.wait_until_done(:timeout => 0.1)
39
43
  puts b.completed? ? "finished!" : "didn't finish"
40
44
 
45
+ # Exception handling: well-supported, see the specs though. Much better examples.
46
+
41
47
  The thread pool is also smart -- depending on load, it can either spawn or cull additional threads (at a rate you can set).
42
48
 
43
49
  == Examples
data/Rakefile CHANGED
@@ -1,47 +1,41 @@
1
- require 'rubygems'
2
- require 'rake/testtask'
3
- require 'rdoc/task'
4
- require 'rubygems/package_task'
5
- require 'rubygems/source_info_cache'
6
- require 'spec/rake/spectask'
1
+ require "rubygems"
2
+ require "rake/testtask"
3
+ require "rdoc/task"
4
+ require "rubygems/package_task"
5
+ require "rspec/core/rake_task"
7
6
  require "bundler/gem_tasks"
8
7
 
9
- spec = Gem::Specification.load(File.join(File.dirname(__FILE__), 'threadz.gemspec'))
8
+ spec = Gem::Specification.load(File.join(File.dirname(__FILE__), "threadz.gemspec"))
10
9
 
11
10
  desc "Default Task"
12
- task 'default' => ['spec', 'rdoc']
11
+ task "default" => ["spec", "rdoc"]
13
12
 
14
13
 
15
- desc "Run all test cases"
16
- task 'spec' do |task|
17
- exec 'spec -c -f n spec/*.rb spec/basic/*.rb'
14
+ desc "Run all unit tests"
15
+ RSpec::Core::RakeTask.new(:spec) do |t|
16
+ t.rspec_opts = ["-c", "-f n", "-r ./spec/spec_helper.rb"]
17
+ t.pattern = 'spec/*_spec.rb'
18
18
  end
19
19
 
20
- desc "Run all performance-oriented test cases"
21
- task 'spec:performance' do |task|
22
- exec 'spec -c -f n -t 30.0 spec/spec_helper.rb spec/performance/*.rb'
23
- end
24
20
 
25
- desc "Run *all* specs"
26
- task 'spec:all' do |task|
27
- exec 'spec -c -f n -t 30.0 spec/spec_helper.rb spec/**/*.rb'
28
- end
21
+ namespace "spec" do
22
+ desc "Run all performance-oriented test cases"
23
+ RSpec::Core::RakeTask.new(:performance) do |t|
24
+ t.rspec_opts = ["-c", "-f n", "-r ./spec/spec_helper.rb"]
25
+ t.pattern = 'spec/performance/*_spec.rb'
26
+ end
29
27
 
30
- desc "Run all test cases 10 times (or n times)"
31
- task 'spec-stress', [:times] do |task, args|
32
- args.with_defaults :times => 10
33
- puts "Executing spec #{args.times} times"
34
- puts Rake::Task[:spec].methods.sort.inspect
35
- args.times.times do
36
- Rake::Task[:spec].execute
37
- puts "foo"
28
+ desc "Run *all* specs"
29
+ RSpec::Core::RakeTask.new(:all) do |t|
30
+ t.rspec_opts = ["-c", "-f n", "-r ./spec/spec_helper.rb"]
31
+ t.pattern = 'spec/**/*_spec.rb'
38
32
  end
39
- puts "Done!"
40
33
  end
41
34
 
35
+
42
36
  Rake::RDocTask.new do |rdoc|
43
- rdoc.main = 'README.rdoc'
44
- rdoc.rdoc_files.include('README.rdoc', 'lib/**/*.rb')
37
+ rdoc.main = "README.rdoc"
38
+ rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
45
39
  rdoc.title = "Threadz Thread Pool"
46
- rdoc.rdoc_dir = 'doc'
40
+ rdoc.rdoc_dir = "doc"
47
41
  end
@@ -31,5 +31,5 @@ end
31
31
 
32
32
  Threadz::dputs("Loading threadz")
33
33
 
34
- ['atomic_integer', 'sleeper', 'directive', 'batch', 'thread_pool'].each { |lib| require File.join(File.dirname(__FILE__), 'threadz', lib) }
34
+ ['atomic_integer', 'sleeper', 'directive', 'batch', 'thread_pool', 'errors'].each { |lib| require File.join(File.dirname(__FILE__), 'threadz', lib) }
35
35
 
File without changes
@@ -1,113 +1,158 @@
1
- ['atomic_integer', 'sleeper'].each { |lib| require File.join(File.dirname(__FILE__), lib) }
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
5
- # the attached thread pool. The calling thread can be signaled when the
6
- # batch has completed executing, or a block can be executed.
7
- class Batch
8
- # Creates a new batch attached to the given threadpool. A number of options
9
- # are available:
10
- # +:latent+:: If latent, none of the jobs in the batch will actually start
11
- # executing until the +start+ method is called.
12
- def initialize(threadpool, opts={})
13
- @threadpool = threadpool
14
- @waiting_threads = []
15
- @job_lock = Mutex.new
16
- @jobs_count = AtomicInteger.new(0)
17
- @when_done_blocks = []
18
- @sleeper = ::Threadz::Sleeper.new
4
+ # A batch is a collection of jobs you care about that gets pushed off to
5
+ # the attached thread pool. The calling thread can be signaled when the
6
+ # batch has completed executing, or a block can be executed.
7
+ class Batch
8
+ # Creates a new batch attached to the given threadpool. A number of options
9
+ # are available:
10
+ # +:latent+:: If latent, none of the jobs in the batch will actually start
11
+ # executing until the +start+ method is called.
12
+ def initialize(threadpool, opts={})
13
+ @threadpool = threadpool
14
+ @job_lock = Mutex.new
15
+ @jobs_count = AtomicInteger.new(0)
16
+ @when_done_blocks = []
17
+ @sleeper = ::Threadz::Sleeper.new
18
+ @error_lock = Mutex.new
19
+ @job_errors = []
20
+ @error_handler_errors = []
21
+ @error_handler = opts[:error_handler]
22
+ if @error_handler && !@error_handler.respond_to?(:call)
23
+ raise ArgumentError.new("ErrorHandler must respond to #call")
24
+ end
25
+ @max_retries = opts[:max_retries] || 3
26
+ @verbose = opts[:verbose]
19
27
 
20
- ## Options
28
+ ## Options
21
29
 
22
- #latent
23
- @latent = opts.key?(:latent) ? opts[:latent] : false
24
- if(@latent)
25
- @started = false
26
- else
27
- @started = true
28
- end
29
- @job_queue = Queue.new if @latent
30
+ #latent
31
+ @latent = opts.key?(:latent) ? opts[:latent] : false
32
+ if(@latent)
33
+ @started = false
34
+ else
35
+ @started = true
30
36
  end
37
+ @job_queue = Queue.new if @latent
38
+ end
31
39
 
32
- # Add a new job to the batch. If this is a latent batch, the job can't
33
- # be scheduled until the batch is #start'ed; otherwise it may start
34
- # immediately. The job can be anything that responds to +call+ or an
35
- # array of objects that respond to +call+.
36
- def push(job)
37
- if job.is_a? Array
38
- job.each {|j| self << j}
39
- elsif job.respond_to? :call
40
- @jobs_count.increment
41
- if @latent && !@started
42
- @job_queue << job
43
- else
44
- send_to_threadpool job
45
- end
40
+ # Add a new job to the batch. If this is a latent batch, the job can't
41
+ # be scheduled until the batch is #start'ed; otherwise it may start
42
+ # immediately. The job can be anything that responds to +call+ or an
43
+ # array of objects that respond to +call+.
44
+ def push(job)
45
+ if job.is_a? Array
46
+ job.each {|j| self << j}
47
+ elsif job.respond_to? :call
48
+ @jobs_count.increment
49
+ if @latent && !@started
50
+ @job_queue << job
46
51
  else
47
- raise "Not a valid job: needs to support #call"
52
+ send_to_threadpool(job)
48
53
  end
54
+ else
55
+ raise "Not a valid job: needs to support #call"
49
56
  end
57
+ end
50
58
 
51
- alias << push
59
+ alias << push
52
60
 
53
- # Put the current thread to sleep until the batch is done processing.
54
- # There are options available:
55
- # +:timeout+:: If specified, will only wait for at least this many seconds
56
- # for the batch to finish. Typically used with #completed?
57
- def wait_until_done(opts={})
58
- raise "Threadz: thread deadlocked because batch job was never started" if @latent && !@started
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?
65
+ def wait_until_done(opts={})
66
+ raise "Threadz: thread deadlocked because batch job was never started" if @latent && !@started
59
67
 
60
- timeout = opts.key?(:timeout) ? opts[:timeout] : 0
61
- @sleeper.wait(timeout) unless completed?
68
+ timeout = opts.key?(:timeout) ? opts[:timeout] : 0
69
+ @sleeper.wait(timeout) unless completed?
70
+ errors = self.job_errors
71
+ if !errors.empty? && !@error_handler
72
+ raise JobError.new(errors)
62
73
  end
74
+ end
63
75
 
64
- # Returns true iff there are no jobs outstanding.
65
- def completed?
66
- return @jobs_count.value == 0
67
- end
76
+ # Returns true iff there are no jobs outstanding.
77
+ def completed?
78
+ return @jobs_count.value == 0
79
+ end
68
80
 
69
- # If this is a latent batch, start processing all of the jobs in the queue.
70
- def start
71
- @job_lock.synchronize { # in case another thread tries to push new jobs onto the queue while we're starting
72
- if @latent
73
- @started = true
74
- until @job_queue.empty?
75
- send_to_threadpool @job_queue.pop
76
- end
77
- return true
78
- else
79
- return false
80
- end
81
- }
82
- end
81
+ # Returns the list of errors that occurred in the jobs
82
+ def job_errors
83
+ arr = nil
84
+ @error_lock.synchronize { arr = @job_errors.dup }
85
+ arr
86
+ end
83
87
 
84
- # Execute a given block when the batch has finished processing. If the batch
85
- # has already finished executing, execute immediately.
86
- def when_done(&block)
87
- @job_lock.synchronize { completed? ? block.call : @when_done_blocks << block }
88
- end
88
+ # Returns the list of errors that occurred in the error handler
89
+ def error_handler_errors
90
+ arr = nil
91
+ @error_lock.synchronize { arr = @error_handler_errors.dup }
92
+ arr
93
+ end
89
94
 
90
- private
91
- def handle_done
92
- @sleeper.broadcast
93
- @when_done_blocks.each do |b|
94
- b.call
95
+ # If this is a latent batch, start processing all of the jobs in the queue.
96
+ 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
99
+ @started = true
100
+ until @job_queue.empty?
101
+ send_to_threadpool(@job_queue.pop)
102
+ end
103
+ return true
104
+ else
105
+ return false
95
106
  end
96
- @when_done_blocks = []
107
+ }
108
+ end
109
+
110
+ # Execute a given block when the batch has finished processing. If the batch
111
+ # has already finished executing, execute immediately.
112
+ def when_done(&block)
113
+ @job_lock.synchronize { completed? ? block.call : @when_done_blocks << block }
114
+ end
115
+
116
+ private
117
+ def handle_done
118
+ @sleeper.broadcast
119
+ @when_done_blocks.each do |b|
120
+ b.call
97
121
  end
122
+ @when_done_blocks = []
123
+ end
98
124
 
99
- def send_to_threadpool(job)
100
- @threadpool.process do
125
+ def send_to_threadpool(job)
126
+ @threadpool.process do
127
+ control = Control.new(job)
128
+ retries = 0
129
+ begin
101
130
  job.call
102
- # Lock in case we get two threads at the "fork in the road" at the same time
103
- # Note: locking here actually creates undesirable behavior. Still investigating why,
104
- # seems like it should be useful.
105
- #@job_lock.lock
106
- @jobs_count.decrement
107
- # fork in the road
108
- handle_done if completed?
109
- #@job_lock.unlock
131
+ rescue StandardError => e
132
+ @error_lock.synchronize { @job_errors << e }
133
+ control.job_errors << e
134
+ if @error_handler
135
+ begin
136
+ @error_handler.call(e, control)
137
+ rescue StandardError => e2
138
+ # Who handles the error handler?!
139
+ $stderr.puts %{Exception in error handler: #{e}} if @verbose
140
+ @error_lock.synchronize { @error_handler_errors << e2 }
141
+ control.error_handler_errors << e2
142
+ end
143
+ retries += 1
144
+ retry unless retries >= @max_retries
145
+ end
110
146
  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
111
155
  end
112
156
  end
157
+ end
113
158
  end
@@ -0,0 +1,14 @@
1
+ module Threadz
2
+ # A control through which to manipulate an individual job
3
+ class Control
4
+ attr_reader :job
5
+ attr_reader :job_errors
6
+ attr_reader :error_handler_errors
7
+
8
+ def initialize(job)
9
+ @job = job
10
+ @job_errors = []
11
+ @error_handler_errors = []
12
+ end
13
+ end
14
+ end
File without changes
@@ -0,0 +1,18 @@
1
+ module Threadz
2
+ class ThreadzError < StandardError; end
3
+
4
+ class JobError < ThreadzError
5
+ attr_reader :errors
6
+ def initialize(errors)
7
+ super("One or more jobs failed due to errors (see #errors)")
8
+ @errors = errors
9
+ end
10
+ 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
+ end
File without changes
@@ -1,4 +1,6 @@
1
1
  require 'thread'
2
+ ['control'].each { |lib| require File.join(File.dirname(__FILE__), lib) }
3
+
2
4
 
3
5
  module Threadz
4
6
 
@@ -53,7 +55,7 @@ module Threadz
53
55
  # finishes. If you care about when it finishes, use batches.
54
56
  def process(callback = nil, &block)
55
57
  callback ||= block
56
- @queue << callback
58
+ @queue << Control.new(callback)
57
59
  nil
58
60
  end
59
61
 
@@ -76,7 +78,7 @@ module Threadz
76
78
  end
77
79
  Thread.pass
78
80
  begin
79
- x.call
81
+ x.job.call(x)
80
82
  rescue StandardError => e
81
83
  $stderr.puts "Threadz: Error in thread, but restarting with next job: #{e.inspect}\n#{e.backtrace.join("\n")}"
82
84
  end
@@ -1,4 +1,4 @@
1
1
  module Threadz
2
- VERSION = "1.0.0"
2
+ VERSION = "1.1.0.rc2"
3
3
  end
4
4
 
@@ -2,28 +2,6 @@ $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__))
2
2
  require 'spec_helper'
3
3
 
4
4
  describe Threadz do
5
- describe Fixnum do
6
- it "should perform badly when under heavy thread usage" do
7
- # This test should always fail, but there is a small chance it won't...
8
-
9
- i = 0
10
- n = 100_000
11
- threads = 100
12
- t = []
13
- threads.times do
14
- t << Thread.new do
15
- sleep 0.1
16
- n.times { i += 1 }
17
- end
18
- t << Thread.new do
19
- sleep 0.1
20
- n.times { i -= 1 }
21
- end
22
- end
23
- t.each { |thread| thread.join }
24
- i.should_not == 0
25
- end
26
- end
27
5
  describe Threadz::AtomicInteger do
28
6
  it "should perform better than an int for counting" do
29
7
  i = Threadz::AtomicInteger.new(0)
@@ -7,7 +7,7 @@ describe Threadz::ThreadPool do
7
7
 
8
8
  it "should perform well for IO jobs" do
9
9
  urls = []
10
- urls << "http://www.google.com/" << "http://www.yahoo.com/" << 'http://www.microsoft.com/'
10
+ urls << "http://www.google.com/" << "http://www.yahoo.com/" << "http://www.microsoft.com/"
11
11
  urls << "http://www.cnn.com/" << "http://slashdot.org/" << "http://www.mozilla.org/"
12
12
  urls << "http://www.ubuntu.com/" << "http://github.com/"
13
13
  time_single_threaded = Time.now
File without changes
File without changes
@@ -8,135 +8,133 @@ describe Threadz do
8
8
  end
9
9
 
10
10
  it "should support process and accept a block" do
11
- i = 0
11
+ i = Threadz::AtomicInteger.new(0)
12
12
  3.times do
13
- @T.process { i += 1}
13
+ @T.process { i.increment }
14
14
  end
15
15
  sleep 0.1
16
16
 
17
- i.should == 3
17
+ i.value.should == 3
18
18
  end
19
19
 
20
20
  it "should support process and accept an arg that responds to :call" do
21
- i = 0
21
+ i = Threadz::AtomicInteger.new(0)
22
22
  3.times do
23
- @T.process(Proc.new { i += 1} )
23
+ @T.process(Proc.new { i.increment })
24
24
  end
25
25
  sleep 0.1
26
26
 
27
- i.should == 3
27
+ i.value.should == 3
28
28
  end
29
29
 
30
30
  it "should support creating batches" do
31
- i = 0
32
-
33
31
  lambda { @T.new_batch }.should_not raise_error
34
32
  lambda { @T.new_batch(:latent => true) }.should_not raise_error
35
33
  end
36
34
 
37
35
  it "should not crash when killing threads" do
38
- i = 0
39
- b = @T.new_batch(:latent => true)
40
- 5000.times do
41
- b << lambda { i += 1 }
42
- b << lambda { i -= 1 }
43
- b << [lambda { i += 2}, lambda { i -= 1}]
44
- end
36
+ i = Threadz::AtomicInteger.new(0)
37
+ b = @T.new_batch(:latent => true)
38
+ 5000.times do
39
+ b << lambda { i.increment }
40
+ b << lambda { i.decrement }
41
+ b << [lambda { i.increment(2) }, lambda { i.decrement }]
42
+ end
45
43
 
46
- b.start
47
- b.wait_until_done
44
+ b.start
45
+ b.wait_until_done
48
46
 
49
- 50.times { sleep 0.1 }
47
+ 50.times { sleep 0.1 }
50
48
  end
51
49
 
52
50
  describe Threadz::Batch do
53
51
  it "should support jobs" do
54
- i = 0
52
+ i = Threadz::AtomicInteger.new(0)
55
53
  b = @T.new_batch
56
54
  10.times do
57
- b << lambda { i += 1 }
58
- b << Proc.new { i += 1 }
55
+ b << lambda { i.increment }
56
+ b << Proc.new { i.increment }
59
57
  end
60
58
  b.wait_until_done
61
59
 
62
- i.should == 20
60
+ i.value.should == 20
63
61
  end
64
62
 
65
63
  it "should support arrays of jobs" do
66
- i = 0
64
+ i = Threadz::AtomicInteger.new(0)
67
65
  b = @T.new_batch
68
- b << [lambda { i += 2}, lambda { i -= 1}]
69
- b << [lambda { i += 2}]
70
- b << lambda { i += 1 }
66
+ b << [lambda { i.increment(2) }, lambda { i.decrement }]
67
+ b << [lambda { i.increment(2) }]
68
+ b << lambda { i.increment }
71
69
  b.wait_until_done
72
70
 
73
- i.should == 4
71
+ i.value.should == 4
74
72
  end
75
73
 
76
74
  it "should support reuse" do
77
- i = 0
75
+ i = Threadz::AtomicInteger.new(0)
78
76
  b = @T.new_batch
79
- b << [lambda { i += 2}, lambda { i -= 1}, lambda { i -= 2 }]
77
+ b << [lambda { i.increment(2) }, lambda { i.decrement }, lambda { i.decrement(2) }]
80
78
  b.wait_until_done
81
79
 
82
- i.should == -1
80
+ i.value.should == -1
83
81
 
84
- b << [lambda { i += 9}, lambda { i -= 3}, lambda { i -= 4 }]
82
+ b << [lambda { i.increment(9) }, lambda { i.decrement(3) }, lambda { i.decrement(4) }]
85
83
  b.wait_until_done
86
84
 
87
- i.should == 1
85
+ i.value.should == 1
88
86
  end
89
87
 
90
- it "should play nicely with instance variables" do
91
- @i = 0
88
+ it "should play nicely with instance variables (shouldn't steal binding)" do
89
+ @i = Threadz::AtomicInteger.new(0)
92
90
  b = @T.new_batch
93
- b << [lambda { @i += 2}, lambda { @i -= 1}]
94
- b << lambda { @i += 2}
91
+ b << [lambda { @i.increment(2) }, lambda { @i.decrement }]
92
+ b << lambda { @i.increment(2) }
95
93
  b.wait_until_done
96
94
 
97
- @i.should == 3
95
+ @i.value.should == 3
98
96
  end
99
97
 
100
98
  it "should support latent option" do
101
- i = 0
99
+ i = Threadz::AtomicInteger.new(0)
102
100
  b = @T.new_batch(:latent => true)
103
- b << lambda { i += 1 }
104
- b << lambda { i -= 1 }
105
- b << [lambda { i += 2}, lambda { i -= 1}]
101
+ b << lambda { i.increment }
102
+ b << lambda { i.decrement }
103
+ b << [lambda { i.increment(2) }, lambda { i.decrement }]
106
104
 
107
- i.should == 0
105
+ i.value.should == 0
108
106
 
109
107
  sleep 0.1
110
108
 
111
- i.should == 0
109
+ i.value.should == 0
112
110
 
113
111
  b.start
114
112
  b.wait_until_done
115
113
 
116
- i.should == 1
114
+ i.value.should == 1
117
115
  end
118
116
 
119
117
  it "should support waiting with timeouts" do
120
- i = 0
118
+ i = Threadz::AtomicInteger.new(0)
121
119
  b = @T.new_batch
122
- b << lambda { i += 1 }
123
- b << lambda { i -= 1 }
124
- b << [lambda { i += 2}, lambda { 500000000.times { i += 1}}]
120
+ b << lambda { i.increment }
121
+ b << lambda { i.increment }
122
+ b << [lambda { i.increment(2) }, lambda { 500000000.times { i.increment } }]
125
123
  t = Time.now
126
124
  timeout = 0.2
127
125
  b.wait_until_done(:timeout => timeout)
128
126
 
129
127
  b.completed?.should be_false
130
128
  (Time.now - t).should >= timeout
131
- i.should > 2
129
+ i.value.should > 2
132
130
  end
133
131
 
134
132
  it "should support 'completed?' even without timeouts" do
135
- i = 0
133
+ i = Threadz::AtomicInteger.new(0)
136
134
  b = @T.new_batch
137
- b << lambda { i += 1 }
138
- b << lambda { i -= 1 }
139
- b << [lambda { i += 2}, lambda { sleep 0.01 while i < 10 }]
135
+ b << lambda { i.increment }
136
+ b << lambda { i.decrement }
137
+ b << [lambda { i.increment(2)}, lambda { sleep 0.1 while i.value < 10 }]
140
138
 
141
139
  b.completed?.should be_false
142
140
 
@@ -144,24 +142,27 @@ describe Threadz do
144
142
 
145
143
  b.completed?.should be_false
146
144
 
147
- i = 10
148
- sleep 0.1
145
+ i.set(10)
146
+
147
+ 5.times do
148
+ sleep 1 if !b.completed?
149
+ end
149
150
 
150
151
  b.completed?.should be_true
151
152
  end
152
153
 
153
154
  it "should support 'push'" do
154
- i = 0
155
+ i = Threadz::AtomicInteger.new(0)
155
156
  b = @T.new_batch
156
- b.push(lambda { i += 1 })
157
- b.push([lambda { i += 1 }, lambda { i += 1 }])
157
+ b.push(lambda { i.increment })
158
+ b.push([lambda { i.increment }, lambda { i.increment }])
158
159
  b.wait_until_done
159
160
 
160
- i.should == 3
161
+ i.value.should == 3
161
162
  end
162
163
 
163
164
  it "should support 'when_done'" do
164
- i = ::Threadz::AtomicInteger.new(0)
165
+ i = Threadz::AtomicInteger.new(0)
165
166
  when_done_executed = false
166
167
  b = @T.new_batch(:latent => true)
167
168
 
@@ -218,6 +219,62 @@ describe Threadz do
218
219
  b.completed?.should be_true
219
220
  when_done_executed.should == 10
220
221
  end
222
+
223
+ context "when exceptions occur" do
224
+ it "should throw on #wait_until_done if no exception handler" do
225
+ b = @T.new_batch
226
+ b << lambda { raise }
227
+ expect { b.wait_until_done }.to raise_error(Threadz::JobError)
228
+ end
229
+ it "should execute the exception handler when given (and not throw in #wait_until_done)" do
230
+ error = nil
231
+ b = @T.new_batch(:error_handler => lambda { |e, ctrl| error = e })
232
+ b << lambda { raise }
233
+ b.wait_until_done
234
+ error.should_not be_nil
235
+ end
236
+ it "should retry up to 3 times by default" do
237
+ count = 0
238
+ b = @T.new_batch(:error_handler => lambda { |e, ctrl| count += 1 })
239
+ b << lambda { raise }
240
+ b.wait_until_done
241
+ count.should == 3
242
+ end
243
+ it "should retry up to the designated number of times" do
244
+ count = 0
245
+ # Try again up to 4 times (excluding the first one; that wasn't a "retry")
246
+ b = @T.new_batch(:error_handler => lambda { |e, ctrl| count += 1 }, :max_retries => 4)
247
+ b << lambda { raise }
248
+ b.wait_until_done
249
+ count.should == 4
250
+ end
251
+ it "should stash exceptions in the #job_errors field" do
252
+ b = @T.new_batch
253
+ b.job_errors.should be_empty
254
+ b << lambda { raise }
255
+ expect { b.wait_until_done }.to raise_error(Threadz::JobError)
256
+ b.job_errors.should_not be_empty
257
+ end
258
+ it "shouldn't hang if there's an exception in the error handler" do
259
+ error = nil
260
+ b = @T.new_batch(:error_handler => lambda { |e, ctrl| raise })
261
+ b << lambda { raise }
262
+ b.wait_until_done
263
+ b.job_errors.length.should == 3
264
+ b.error_handler_errors.length.should == 3
265
+ end
266
+ it "should allow you to respond to errors on a per-job basis" do
267
+ job1 = lambda { 1 + 2 }
268
+ job2 = lambda { raise "Hi" }
269
+ errors = {job1 => [], job2 => []}
270
+ b = @T.new_batch(:error_handler => lambda { |e, ctrl| errors[ctrl.job] << e }, :max_retries => 1)
271
+ b << job1
272
+ b << job2
273
+ b.wait_until_done
274
+ errors[job1].length.should == 0
275
+ errors[job2].length.should == 1
276
+ end
277
+ end
221
278
  end
222
279
  end
223
280
  end
@@ -18,5 +18,5 @@ Gem::Specification.new do |s|
18
18
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
19
  s.require_paths = ["lib"]
20
20
 
21
- s.add_development_dependency "rspec", "~> 1.0"
21
+ s.add_development_dependency "rspec", "~> 2.12"
22
22
  end
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: threadz
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
5
- prerelease:
4
+ version: 1.1.0.rc2
5
+ prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Max Aller
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2012-07-16 00:00:00.000000000 Z
12
+ date: 2012-12-21 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: rspec
@@ -18,7 +18,7 @@ dependencies:
18
18
  requirements:
19
19
  - - ~>
20
20
  - !ruby/object:Gem::Version
21
- version: '1.0'
21
+ version: '2.12'
22
22
  type: :development
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
@@ -26,7 +26,7 @@ dependencies:
26
26
  requirements:
27
27
  - - ~>
28
28
  - !ruby/object:Gem::Version
29
- version: '1.0'
29
+ version: '2.12'
30
30
  description: A Ruby threadpool library to handle threadpools and make batch jobs easier.
31
31
  email:
32
32
  - nanodeath@gmail.com
@@ -35,6 +35,7 @@ extensions: []
35
35
  extra_rdoc_files: []
36
36
  files:
37
37
  - .gitignore
38
+ - .travis.yml
38
39
  - CHANGELOG
39
40
  - Gemfile
40
41
  - Gemfile.lock
@@ -44,7 +45,9 @@ files:
44
45
  - lib/threadz.rb
45
46
  - lib/threadz/atomic_integer.rb
46
47
  - lib/threadz/batch.rb
48
+ - lib/threadz/control.rb
47
49
  - lib/threadz/directive.rb
50
+ - lib/threadz/errors.rb
48
51
  - lib/threadz/sleeper.rb
49
52
  - lib/threadz/thread_pool.rb
50
53
  - lib/threadz/version.rb
@@ -69,9 +72,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
69
72
  required_rubygems_version: !ruby/object:Gem::Requirement
70
73
  none: false
71
74
  requirements:
72
- - - ! '>='
75
+ - - ! '>'
73
76
  - !ruby/object:Gem::Version
74
- version: '0'
77
+ version: 1.3.1
75
78
  requirements: []
76
79
  rubyforge_project: threadz
77
80
  rubygems_version: 1.8.24