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 +0 -0
- data/.travis.yml +9 -0
- data/CHANGELOG +8 -0
- data/Gemfile +2 -1
- data/Gemfile.lock +19 -5
- data/MIT-LICENSE +0 -0
- data/README.rdoc +6 -0
- data/Rakefile +25 -31
- data/lib/threadz.rb +1 -1
- data/lib/threadz/atomic_integer.rb +0 -0
- data/lib/threadz/batch.rb +133 -88
- data/lib/threadz/control.rb +14 -0
- data/lib/threadz/directive.rb +0 -0
- data/lib/threadz/errors.rb +18 -0
- data/lib/threadz/sleeper.rb +0 -0
- data/lib/threadz/thread_pool.rb +4 -2
- data/lib/threadz/version.rb +1 -1
- data/spec/atomic_integer_spec.rb +0 -22
- data/spec/basic/thread_pool_spec.rb +1 -1
- data/spec/performance/batch_spec.rb +0 -0
- data/spec/spec_helper.rb +0 -0
- data/spec/threadz_spec.rb +117 -60
- data/threadz.gemspec +1 -1
- metadata +10 -7
data/.gitignore
CHANGED
File without changes
|
data/.travis.yml
ADDED
data/CHANGELOG
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,18 +1,32 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
threadz (1.
|
4
|
+
threadz (1.1.0.rc1)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: http://rubygems.org/
|
8
8
|
specs:
|
9
|
-
|
10
|
-
|
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
|
-
|
29
|
+
rake (~> 10.0)
|
30
|
+
rdoc (~> 3.12)
|
31
|
+
rspec (~> 2.12)
|
18
32
|
threadz!
|
data/MIT-LICENSE
CHANGED
File without changes
|
data/README.rdoc
CHANGED
@@ -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
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
5
|
-
require
|
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__),
|
8
|
+
spec = Gem::Specification.load(File.join(File.dirname(__FILE__), "threadz.gemspec"))
|
10
9
|
|
11
10
|
desc "Default Task"
|
12
|
-
task
|
11
|
+
task "default" => ["spec", "rdoc"]
|
13
12
|
|
14
13
|
|
15
|
-
desc "Run all
|
16
|
-
|
17
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
31
|
-
|
32
|
-
|
33
|
-
|
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 =
|
44
|
-
rdoc.rdoc_files.include(
|
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 =
|
40
|
+
rdoc.rdoc_dir = "doc"
|
47
41
|
end
|
data/lib/threadz.rb
CHANGED
@@ -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
|
data/lib/threadz/batch.rb
CHANGED
@@ -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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
28
|
+
## Options
|
21
29
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
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
|
-
|
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
|
-
|
59
|
+
alias << push
|
52
60
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
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
|
-
|
61
|
-
|
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
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
76
|
+
# Returns true iff there are no jobs outstanding.
|
77
|
+
def completed?
|
78
|
+
return @jobs_count.value == 0
|
79
|
+
end
|
68
80
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
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
|
-
|
91
|
-
|
92
|
-
|
93
|
-
@
|
94
|
-
|
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
|
-
|
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
|
-
|
100
|
-
|
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
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
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
|
data/lib/threadz/directive.rb
CHANGED
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
|
data/lib/threadz/sleeper.rb
CHANGED
File without changes
|
data/lib/threadz/thread_pool.rb
CHANGED
@@ -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
|
data/lib/threadz/version.rb
CHANGED
data/spec/atomic_integer_spec.rb
CHANGED
@@ -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/" <<
|
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
|
data/spec/spec_helper.rb
CHANGED
File without changes
|
data/spec/threadz_spec.rb
CHANGED
@@ -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
|
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
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
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
|
-
|
47
|
-
|
44
|
+
b.start
|
45
|
+
b.wait_until_done
|
48
46
|
|
49
|
-
|
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
|
58
|
-
b << Proc.new { i
|
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
|
69
|
-
b << [lambda { i
|
70
|
-
b << lambda { i
|
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
|
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
|
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
|
94
|
-
b << lambda { @i
|
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
|
104
|
-
b << lambda { i
|
105
|
-
b << [lambda { i
|
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
|
123
|
-
b << lambda { i
|
124
|
-
b << [lambda { i
|
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
|
138
|
-
b << lambda { i
|
139
|
-
b << [lambda { i
|
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
|
148
|
-
|
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
|
157
|
-
b.push([lambda { i
|
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 =
|
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
|
data/threadz.gemspec
CHANGED
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.
|
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-
|
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: '
|
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: '
|
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:
|
77
|
+
version: 1.3.1
|
75
78
|
requirements: []
|
76
79
|
rubyforge_project: threadz
|
77
80
|
rubygems_version: 1.8.24
|