threadz 1.0.0 → 1.1.0.rc2

Sign up to get free protection for your applications and to get access to all the features.
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