Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Add -j <max_jobs> and -m (all tasks multitasks) -- solution included #113

Closed
wants to merge 10 commits into from

9 participants

@michaeljbishop

PROBLEM SUMMARY (THE CASE FOR -j and -m)

Rake can be unusable for builds invoking large numbers of concurrent external processes.

PROBLEM DESCRIPTION

Rake makes it easy to maximize concurrency in builds with its "multitask" function. When using rake to build non-ruby projects quite often rake needs to execute shell tasks to process files. Unfortunately, when executing multitasks, rake spawns a new thread for each task-prerequisite. This shouldn't cause problems when the build code is pure ruby (for green threads), but when the tasks are executing external processes, the sheer number of spawned processes can cause the machine to thrash. Additionally ruby can reach the maximum number of open files (presumably because it's reading stdout for all those processes).

SOLUTION SUMMARY

This request includes the code to add support for a --jobs NUMBER (-j) command-line option to specify the number of simultaneous tasks to execute.

  • To maintain backward compatibility, not passing -j reverts to the old behavior of unlimited concurrent tasks.

As a nod to drake, a --multitask (-m) flag is also included which when supplied, changes tasks into multitasks.

SOLUTION

Rather than spawning a new thread per prerequisite MultiTask now sends its prerequisites to a WorkerPool object. WorkerPool.new(n).execute_blocks has the same semantics as Thread.new...join but caps the thread count at n.

Core Change

threads = @prerequisites.collect { |p|
  Thread.new(p) { |r| application[r, @scope].invoke_with_call_chain(args, invocation_chain) }
}
threads.each { |t| t.join }

...becomes...

@@wp ||= WorkerPool.new(application.options.thread_pool_size)

blocks = @prerequisites.collect { |r|
  lambda { application[r, @scope].invoke_with_call_chain(args, invocation_chain) }
}
@@wp.execute_blocks blocks

To support -m, the MultiTask implementation has moved to Task#invoke_prerequisites_concurrently and is called from MultiTask#invoke_prerequisites. This enables concurrent behavior for Task when -m is used.

Details

WorkerPool#execute_blocks adds the passed-in blocks to a queue, ensures there are enough threads to execute them (under the maximum), and sleeps the current thread until the blocks are processed.

This creates a few potential problems:

What if all of the blocks then called #execute_blocks? Wouldn't that sleep all the threads?

Yes it would. This is solved as #execute_blocks removes the current thread from the thread pool just before it sleeps and creates a new one in its place. When all the blocks are processed, the current thread is added back to the pool (adjusting for the max-size). There are always enough available threads in the thread pool for processing.

When do the threads shutdown?

WorkerPool#execute_blocks knows how many threads are waiting for their blocks to be processed. If, upon its awakening, it notices there are no threads waiting on blocks, it shuts down the thread pool.

Statistics

 ---LINES--         ----LOC---
  old   new  diff    old   new  diff  File Name
 ----------  ----   ----------  ----  ----------
  598   605    +7    477   484    +7  lib/rake/application.rb
   16    13    -3     11     8    -3  lib/rake/multi_task.rb
  327   341   +14    210   222   +12  lib/rake/task.rb
        111                 80        lib/rake/worker_pool.rb
 4264  4393         2696  2792        TOTAL
 ------------------------------------------------
             +129                +96  SUMMARY

TESTS

Tests are included for all new functionality

REQUIREMENTS

The Ruby version requirements remain the same. lib/rake/worker_pool.rb adds two new requirements: thread and set

michaeljbishop added some commits
@michaeljbishop michaeljbishop Rake now supports a --jobs <n> command-line option.
DESCRIPTION
-----------
The new option: "--jobs number (-j)" specifies the maximum number of
concurrent tasks. The suggested value is equal to the number of CPUs.

Sample values:
  default: unlimited concurrent tasks (standard rake behavior)
  1: one task at a time

The code consists of two major edits, the first is a change to
`application.rb` to support the parsing of the option.

The second is a more substantial change to `multi_task.rb` which
replaces the multi-task scheduling algorithm. Instead of spawning a new
thread for every pre-requisite that needs to be executed, a block is
created which calls the pre-requisite and added to a Queue.

Additionally, a thread-pool is created to pull the blocks off the queue
and execute them. Finally, the MultiTask queueing up its prerequisites
will itself participate in the block-processing while waiting for its
prerequisites to finish processing.

It can tell when its prerequisites are finished by enveloping the
queued blocks in another block that adds a little bookkeeping.

VERSION REQUIREMENTS
--------------------
Rake ruby version requirements remain unchanged.
295c7a4
@michaeljbishop michaeljbishop Fixed a bug where the MultiTask tests were not testing the thread pool
This is because I left in the original code which just passed through
to spawn unlimited threads.

Now the code always uses the thread pool, and sets the initial
limit to be the maximum Fixnum (which means virtually unlimited)

This is nice because it means the rake MultiTask tests are now
stressing the thread pool implementation.

Additionally, the thread pool size can now be changed dynamically
to adjust to load by changing 'application.options.thread_pool_size'
while rake is running.

There is no code that observes load, but it certainly could and
adjust as it saw fit.

Notes:
  While the threads are in the their processing loop, they add
  other threads need to be added to the pool to meet the limit.
  Additionally, if the thread pool size is larger than the
  application preference the thread exits.
d1229f0
@michaeljbishop michaeljbishop Fixed bug where the stack was being blown
The previous "add blocks to queue" method worked, but had the
unnecessary side-effect of blowing the stack for large amounts of
prerequisites.

This is a new thread pool implementation which retains all the
advantages of the original, but keeps the stack size the same as the
pre-thread-pool rake.

It's closer to the pre-thread-pool rake implementation with the
addition of checking the thread pool size before spawning a new thread.
578b637
@michaeljbishop michaeljbishop Added -m option which forces every task to be a multitask
This change pulls the concurrent implementation of
'invoke_prerequisites' out of MultiTask into Task while changing the
method name to 'invoke_prerequisites_concurrently'

Then, if -m is passed, Task calls 'invoke_prerequisites_concurrently'
so everything is multithreaded.

MultiTask always calls 'invoke_prerequisites_concurrently'
0ee43db
@michaeljbishop michaeljbishop Forgot to update the rdoc for the -m option c3f5260
@michaeljbishop michaeljbishop Rake now has a throttle on simultaneous task count
This is implemented by adding Rake::WorkerPool which provides a way for
callers to synchronously execute blocks by a thread pool.

What has not changed (and is still problematic) is that Rake creates a
new thread for each and every MultiTask prerequisite.

NOTES: Passes all rake tests
faa1ff1
@michaeljbishop michaeljbishop MultiTask no longer spawns a thread for each prerequisite
WorkerPool:
- now has the ability to execute an array of blocks and wait for them
all to execute.
- only adds a new thread when there is no thread waiting for action.
This slows the ramp up and threads are better reused
- has a minimum and maximum size. By default, minimum is 1 and maximum
is the maximum fixnum
- removed unused #wait call

MultiTask:
- Now uses WorkerPool#execte_blocks to execute its prerequisites
7fa886d
@michaeljbishop michaeljbishop Fixed WorkerPool so it can synchronously execute a group of blocks am…
…ongst a thread pool.

Since the WorkerPool was only needed in MultiTask, I changed Task back to simply executing it's actions and calling its prerequisites.

Merge branch 'master' into everything_is_a_multitask

Conflicts:
	lib/rake/application.rb
	lib/rake/multi_task.rb
2a2bb2f
@michaeljbishop michaeljbishop Added tests. tidied up command-line ops. WorkerPool default size is F…
…IXNUM_MAX.

* The default WorkerPool maximum size was changed to FIXNUM_MAX. This
  is ok  because only enough blocks are added to the thread pool to
  cover the requested number of blocks (but not past the maximum).
* Added a #join call which clears the thread pool of all threads. This
  is called inside #execute_blocks when there are no more threads
  waiting on the thread pool.
* Suppressed "multithreads" output when specifying -m
* Removed unnecessary exception backtrace concatenating
* -j now is kinder when receiving bad input. If it has no parameter
  or the parameter can't be parsed, it defaults to 2.
* Added WorkerPool test
* added tests for -j and -m to the application options test.
f40087d
@michaeljbishop michaeljbishop -m hooked up. test included
Added a test for -m in task.rb. Hooked it up.
Concised code in application.rb
Added documentation for -j
b828747
@pkondzior

+1, is this going to be merged?

@michaeljbishop
@nexussays

Any movement on this? This is incredibly useful, however we depend on the distributed rake gem so we can't directly use @michaeljbishop's changes until they are integrated.

@jaisingh

this would be really helpful and give us a comparable feature to make

@amcbride

Please merge! This would be awesome

@psywombats

This feature would be very useful, hopefully it can be merged in shortly.

@nickma

This will be really helpful for us.

@larsburgess

+1, this would be awesome!

@jimweirich
Owner

First, sorry for the delay in responding to this... I'm going through the backlog of Rake work and its taking some time.

Second, I like the functionality of the patch, but I find the worker pool logic a bit difficult to work through. I'm reluctant to merge this until I have a better handle on what's going on.

@michaeljbishop
@jimweirich
Owner

Excellent. Quick question: If multitask is waiting for the futures I'm assuming it won't be available for job processing. It is possible to get in a state where all the threads in the pool are waiting for futures and nothing is available to actually do the work?

This is the point I reached in my own simple-minded implementation of a thread pool. It seems your initial version side-stepped this problem, but I wasn't clear on how it did it.

@michaeljbishop
@michaeljbishop
@michaeljbishop

Hi Jim. As promised, I have a new implementation that I feel is a much improved version of this pull-request (and a tidier git history to boot), I have removed this repository, created a new one and added a pull request for that implementation here: #131

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Apr 19, 2012
  1. @michaeljbishop

    Rake now supports a --jobs <n> command-line option.

    michaeljbishop authored
    DESCRIPTION
    -----------
    The new option: "--jobs number (-j)" specifies the maximum number of
    concurrent tasks. The suggested value is equal to the number of CPUs.
    
    Sample values:
      default: unlimited concurrent tasks (standard rake behavior)
      1: one task at a time
    
    The code consists of two major edits, the first is a change to
    `application.rb` to support the parsing of the option.
    
    The second is a more substantial change to `multi_task.rb` which
    replaces the multi-task scheduling algorithm. Instead of spawning a new
    thread for every pre-requisite that needs to be executed, a block is
    created which calls the pre-requisite and added to a Queue.
    
    Additionally, a thread-pool is created to pull the blocks off the queue
    and execute them. Finally, the MultiTask queueing up its prerequisites
    will itself participate in the block-processing while waiting for its
    prerequisites to finish processing.
    
    It can tell when its prerequisites are finished by enveloping the
    queued blocks in another block that adds a little bookkeeping.
    
    VERSION REQUIREMENTS
    --------------------
    Rake ruby version requirements remain unchanged.
Commits on Apr 20, 2012
  1. @michaeljbishop

    Fixed a bug where the MultiTask tests were not testing the thread pool

    michaeljbishop authored
    This is because I left in the original code which just passed through
    to spawn unlimited threads.
    
    Now the code always uses the thread pool, and sets the initial
    limit to be the maximum Fixnum (which means virtually unlimited)
    
    This is nice because it means the rake MultiTask tests are now
    stressing the thread pool implementation.
    
    Additionally, the thread pool size can now be changed dynamically
    to adjust to load by changing 'application.options.thread_pool_size'
    while rake is running.
    
    There is no code that observes load, but it certainly could and
    adjust as it saw fit.
    
    Notes:
      While the threads are in the their processing loop, they add
      other threads need to be added to the pool to meet the limit.
      Additionally, if the thread pool size is larger than the
      application preference the thread exits.
Commits on Apr 21, 2012
  1. @michaeljbishop

    Fixed bug where the stack was being blown

    michaeljbishop authored
    The previous "add blocks to queue" method worked, but had the
    unnecessary side-effect of blowing the stack for large amounts of
    prerequisites.
    
    This is a new thread pool implementation which retains all the
    advantages of the original, but keeps the stack size the same as the
    pre-thread-pool rake.
    
    It's closer to the pre-thread-pool rake implementation with the
    addition of checking the thread pool size before spawning a new thread.
  2. @michaeljbishop

    Added -m option which forces every task to be a multitask

    michaeljbishop authored
    This change pulls the concurrent implementation of
    'invoke_prerequisites' out of MultiTask into Task while changing the
    method name to 'invoke_prerequisites_concurrently'
    
    Then, if -m is passed, Task calls 'invoke_prerequisites_concurrently'
    so everything is multithreaded.
    
    MultiTask always calls 'invoke_prerequisites_concurrently'
  3. @michaeljbishop
Commits on Apr 24, 2012
  1. @michaeljbishop

    Rake now has a throttle on simultaneous task count

    michaeljbishop authored
    This is implemented by adding Rake::WorkerPool which provides a way for
    callers to synchronously execute blocks by a thread pool.
    
    What has not changed (and is still problematic) is that Rake creates a
    new thread for each and every MultiTask prerequisite.
    
    NOTES: Passes all rake tests
  2. @michaeljbishop

    MultiTask no longer spawns a thread for each prerequisite

    michaeljbishop authored
    WorkerPool:
    - now has the ability to execute an array of blocks and wait for them
    all to execute.
    - only adds a new thread when there is no thread waiting for action.
    This slows the ramp up and threads are better reused
    - has a minimum and maximum size. By default, minimum is 1 and maximum
    is the maximum fixnum
    - removed unused #wait call
    
    MultiTask:
    - Now uses WorkerPool#execte_blocks to execute its prerequisites
Commits on Apr 25, 2012
  1. @michaeljbishop

    Fixed WorkerPool so it can synchronously execute a group of blocks am…

    michaeljbishop authored
    …ongst a thread pool.
    
    Since the WorkerPool was only needed in MultiTask, I changed Task back to simply executing it's actions and calling its prerequisites.
    
    Merge branch 'master' into everything_is_a_multitask
    
    Conflicts:
    	lib/rake/application.rb
    	lib/rake/multi_task.rb
Commits on Apr 27, 2012
  1. @michaeljbishop

    Added tests. tidied up command-line ops. WorkerPool default size is F…

    michaeljbishop authored
    …IXNUM_MAX.
    
    * The default WorkerPool maximum size was changed to FIXNUM_MAX. This
      is ok  because only enough blocks are added to the thread pool to
      cover the requested number of blocks (but not past the maximum).
    * Added a #join call which clears the thread pool of all threads. This
      is called inside #execute_blocks when there are no more threads
      waiting on the thread pool.
    * Suppressed "multithreads" output when specifying -m
    * Removed unnecessary exception backtrace concatenating
    * -j now is kinder when receiving bad input. If it has no parameter
      or the parameter can't be parsed, it defaults to 2.
    * Added WorkerPool test
    * added tests for -j and -m to the application options test.
  2. @michaeljbishop

    -m hooked up. test included

    michaeljbishop authored
    Added a test for -m in task.rb. Hooked it up.
    Concised code in application.rb
    Added documentation for -j
This page is out of date. Refresh to see the latest.
View
14 doc/command_line_usage.rdoc
@@ -31,15 +31,27 @@ Options are:
[<tt>--execute-print</tt> _code_ (-p)]
Execute some Ruby code, print the result, and exit.
-[<tt>--execute-continue</tt> _code_ (-p)]
+[<tt>--execute-continue</tt> _code_ (-E)]
Execute some Ruby code, then continue with normal task processing.
[<tt>--help</tt> (-H)]
Display some help text and exit.
+[<tt>--jobs</tt> _number_ (-j)]
+ Specifies the maximum number of concurrent tasks. The suggested
+ value is equal to the number of CPUs.
+
+ Sample values:
+ no -j : unlimited concurrent tasks (standard rake behavior)
+ only -j : 2 concurrent tasks
+ -j 16 : 16 concurrent tasks
+
[<tt>--libdir</tt> _directory_ (-I)]
Add _directory_ to the list of directories searched for require.
+[<tt>--multitask</tt> (-m)]
+ Treat all tasks as multitasks. ('make/drake' semantics)
+
[<tt>--nosearch</tt> (-N)]
Do not search for a Rakefile in parent directories.
View
7 lib/rake/application.rb
@@ -325,9 +325,16 @@ def standard_rake_options
"Execute some Ruby code, then continue with normal task processing.",
lambda { |value| eval(value) }
],
+ ['--jobs', '-j [NUMBER]',
+ "Specifies the maximum number of tasks to execute in parallel. (default:2)",
+ lambda { |value| options.thread_pool_size = [(value || 2).to_i,2].max }
+ ],
['--libdir', '-I LIBDIR', "Include LIBDIR in the search path for required modules.",
lambda { |value| $:.push(value) }
],
+ ['--multitask', '-m', "Treat all tasks as multitasks.",
+ lambda { |value| options.always_multitask = true }
+ ],
['--no-search', '--nosearch', '-N', "Do not search parent directories for the Rakefile.",
lambda { |value| options.nosearch = true }
],
View
9 lib/rake/multi_task.rb
@@ -5,12 +5,9 @@ module Rake
#
class MultiTask < Task
private
- def invoke_prerequisites(args, invocation_chain)
- threads = @prerequisites.collect { |p|
- Thread.new(p) { |r| application[r, @scope].invoke_with_call_chain(args, invocation_chain) }
- }
- threads.each { |t| t.join }
+ def invoke_prerequisites(task_args, invocation_chain) # :nodoc:
+ invoke_prerequisites_concurrently(task_args, invocation_chain)
end
- end
+ end
end
View
24 lib/rake/task.rb
@@ -1,4 +1,5 @@
require 'rake/invocation_exception_mixin'
+require 'rake/worker_pool'
module Rake
@@ -171,10 +172,23 @@ def add_chain_to(exception, new_chain)
# Invoke all the prerequisites of a task.
def invoke_prerequisites(task_args, invocation_chain) # :nodoc:
- prerequisite_tasks.each { |prereq|
- prereq_args = task_args.new_scope(prereq.arg_names)
- prereq.invoke_with_call_chain(prereq_args, invocation_chain)
+ if application.options.always_multitask
+ invoke_prerequisites_concurrently(task_args, invocation_chain)
+ else
+ prerequisite_tasks.each { |prereq|
+ prereq_args = task_args.new_scope(prereq.arg_names)
+ prereq.invoke_with_call_chain(prereq_args, invocation_chain)
+ }
+ end
+ end
+
+ def invoke_prerequisites_concurrently(args, invocation_chain)
+ @@wp ||= WorkerPool.new(application.options.thread_pool_size)
+
+ blocks = @prerequisites.collect { |r|
+ lambda { application[r, @scope].invoke_with_call_chain(args, invocation_chain) }
}
+ @@wp.execute_blocks blocks
end
# Format the trace flags for display.
@@ -200,9 +214,9 @@ def execute(args=nil)
@actions.each do |act|
case act.arity
when 1
- act.call(self)
+ act.call(self)
else
- act.call(self, args)
+ act.call(self, args)
end
end
end
View
111 lib/rake/worker_pool.rb
@@ -0,0 +1,111 @@
+require 'thread'
+require 'set'
+
+module Rake
+ class WorkerPool
+ attr_accessor :maximum_size # this is the maximum size of the pool
+
+ def initialize(max = nil)
+ @threads = Set.new # this holds the set of threads in the pool
+ @waiting_threads = Set.new # set of threads waiting in #execute_blocks
+ @threads_mutex = Mutex.new # use this whenever r/w @threads
+ @queue = Queue.new # this holds blocks to be executed
+ @join_cv = ConditionVariable.new # alerts threads sleeping from calling #join
+ if (max && max > 0)
+ @maximum_size = max
+ else
+ @maximum_size = (2**(0.size * 8 - 2) - 1) # FIXNUM_MAX
+ end
+ end
+
+ def execute_blocks(blocks)
+ mutex = Mutex.new
+ cv = ConditionVariable.new
+ exception = nil
+ unprocessed_block_count = blocks.count
+ mutex.synchronize {
+ blocks.each { |block|
+ @queue.enq lambda {
+ begin
+ block.call
+ rescue Exception => e
+ exception = e
+ ensure
+ # we *have* to have this 'ensure' because we *have* to
+ # call cv.signal to wake up WorkerPool#execute_blocks
+ # which is asleep because it called cv.wait(mutex)
+ mutex.synchronize { unprocessed_block_count -= 1; cv.signal }
+ end
+ }
+ }
+ was_in_set = @threads_mutex.synchronize {
+ @waiting_threads.add(Thread.current)
+ @threads.delete? Thread.current
+ }
+ ensure_thread_count(blocks.count)
+ cv.wait(mutex) until unprocessed_block_count == 0
+ @threads_mutex.synchronize {
+ @waiting_threads.delete(Thread.current)
+ @threads.add(Thread.current) if was_in_set
+ # shutdown the thread pool if we were the last thread
+ # waiting on the thread pool to process blocks
+ join if @waiting_threads.count == 0
+ }
+ }
+ # raise any exceptions that arose in the block (the last
+ # exception won)
+ raise exception if exception
+ end
+
+ def ensure_thread_count(count)
+ # here, we need to somehow make sure to add as many threads as
+ # are needed and no more. So (blocks.size - ready threads)
+ @threads_mutex.synchronize {
+ threads_needed = [[@maximum_size,count].min - @threads.size, 0].max
+ threads_needed.times do
+ t = Thread.new do
+ begin
+ while @threads.size <= @maximum_size
+ @queue.deq.call
+ end
+ ensure
+ @threads_mutex.synchronize {
+ @threads.delete(Thread.current)
+ @join_cv.signal
+ }
+ end
+ end
+ @threads.add t
+ end
+ }
+ end
+ private :ensure_thread_count
+
+ def join
+ # *** MUST BE CALLED inside @threads_mutex.synchronize{}
+ # because we don't want any threads added while we wait, only
+ # removed we set the maximum size to 0 and then add enough blocks
+ # to the queue so any sleeping threads will wake up and notice
+ # there are more threads than the limit and exit
+ saved_maximum_size, @maximum_size = @maximum_size, 0
+ @threads.each { @queue.enq lambda { ; } } # wake them all up
+
+ # here, we sleep and wait for a signal off @join_cv
+ # we will get it once for each sleeping thread so we watch the
+ # thread count
+ #
+ # avoid the temptation to change this to
+ # "<code> until <condition>". The condition needs to checked
+ # first or you will deadlock.
+ while (@threads.size > 0)
+ @join_cv.wait(@threads_mutex)
+ end
+
+ # now everything has been executed and we are ready to
+ # start accepting more work so we raise the limit back
+ @maximum_size = saved_maximum_size
+ end
+ private :join
+
+ end
+end
View
20 test/test_rake_application_options.rb
@@ -33,6 +33,7 @@ def test_default_options
assert_nil opts.dryrun
assert_nil opts.ignore_system
assert_nil opts.load_system
+ assert_nil opts.always_multitask
assert_nil opts.nosearch
assert_equal ['rakelib'], opts.rakelib
assert_nil opts.show_prereqs
@@ -40,6 +41,7 @@ def test_default_options
assert_nil opts.show_tasks
assert_nil opts.silent
assert_nil opts.trace
+ assert_nil opts.thread_pool_size
assert_equal ['rakelib'], opts.rakelib
assert ! Rake::FileUtilsExt.verbose_flag
assert ! Rake::FileUtilsExt.nowrite_flag
@@ -110,6 +112,18 @@ def test_help
assert_equal :exit, @exit
end
+ def test_jobs
+ flags(['--jobs', '4'], ['-j', '4']) do |opts|
+ assert_equal 4, opts.thread_pool_size
+ end
+ flags(['--jobs', 'asdas'], ['-j', 'asdas']) do |opts|
+ assert_equal 2, opts.thread_pool_size
+ end
+ flags('--jobs', '-j') do |opts|
+ assert_equal 2, opts.thread_pool_size
+ end
+ end
+
def test_libdir
flags(['--libdir', 'xx'], ['-I', 'xx'], ['-Ixx']) do |opts|
$:.include?('xx')
@@ -118,6 +132,12 @@ def test_libdir
$:.delete('xx')
end
+ def test_multitask
+ flags('--multitask', '-m') do |opts|
+ assert_equal opts.always_multitask, true
+ end
+ end
+
def test_rakefile
flags(['--rakefile', 'RF'], ['--rakefile=RF'], ['-f', 'RF'], ['-fRF']) do |opts|
assert_equal ['RF'], @app.instance_eval { @rakefiles }
View
19 test/test_rake_task.rb
@@ -223,6 +223,25 @@ def c.timestamp() Time.now + 5 end
assert_in_delta now + 10, a.timestamp, 0.1, 'computer too slow?'
end
+ def test_all_multitask
+ mx = Mutex.new
+ result = ""
+ root = task :root
+ ('aa'..'zz').each do |c|
+ task(c.to_sym) { mx.synchronize{ result << c } }
+ task(:root => c.to_sym)
+ end
+ root.invoke
+
+ root.prerequisite_tasks.each { |p| p.reenable };
+ root.reenable
+ task_result = result.dup; result.clear
+
+ Rake.application.options.always_multitask = true
+ root.invoke
+ refute_equal task_result, result
+ end
+
def test_investigation_output
t1 = task(:t1 => [:t2, :t3]) { |t| runlist << t.name; 3321 }
task(:t2)
View
90 test/test_rake_test_worker_pool.rb
@@ -0,0 +1,90 @@
+require File.expand_path('../helper', __FILE__)
+require 'rake/worker_pool'
+require 'test/unit/assertions'
+
+class TestRakeTestWorkerPool < Rake::TestCase
+ include Rake
+
+ def test_block_order
+ mx = Mutex.new
+ block = lambda {|executor,count=20,result=""|
+ return if (count < 1)
+ mx.synchronize{ result << count.to_s }
+ sleep(rand * 0.01)
+ executor.call( [lambda {block.call(executor,count-1,result)}] )
+ result
+ }
+
+ old = lambda {|b|
+ threads = b.collect {|c| Thread.new(c) {|d| d.call } }
+ threads.each {|t| t.join }
+ }
+
+ wp = WorkerPool.new(4)
+ new = lambda {|b| wp.execute_blocks b }
+
+ assert_equal(block.call(old), block.call(new))
+ end
+
+ # test that there are no deadlocks within the worker pool itself
+ def test_deadlocks
+ wp = WorkerPool.new(10)
+ blocks = []
+ 10.times {
+ inner_block = lambda {|count=5|
+ return if (count < 1)
+ sleep(rand * 0.000001)
+ inner_blocks = []
+ 3.times { inner_blocks << lambda {inner_block.call(count-1)} }
+ wp.execute_blocks inner_blocks
+ }
+ blocks << inner_block
+ }
+ wp.execute_blocks blocks
+ end
+
+ # test that throwing an exception way down in the blocks propagates
+ # to the top
+ def test_exceptions
+ wp = WorkerPool.new(4)
+ deep_exception_block = lambda {|count=3|
+ raise Exception.new if ( count < 1 )
+ deep_exception_block.call(count-1)
+ }
+ assert_raises(Exception) do
+ wp.execute_blocks [deep_exception_block]
+ end
+ end
+
+ def test_thread_count
+ mutex = Mutex.new
+ expected_thread_count = 2
+ wp = WorkerPool.new(expected_thread_count)
+
+ # the lambda code will determine how many threads are running in
+ # the pool
+ blocks = []
+ thread_count = 0
+ should_sleep = true
+ (expected_thread_count*2).times do
+ blocks << lambda {
+ mutex.synchronize do; stack_prefix = "#{__FILE__}:#{__LINE__}" # this synchronize will be on the stack
+ sleep 1 if should_sleep # this lets all the threads wait on the mutex
+ threads = Thread.list
+ backtraces = threads.collect {|t| t.backtrace}
+ # sometimes a thread doesn't return a thread count
+ if ( threads.count == backtraces.count )
+ should_sleep = false
+ # finds all the backtraces that contain our mutex.synchronize call
+ our_bt = backtraces.find_all{|bt| bt && bt.index{|tr| tr.start_with? stack_prefix}!=nil }
+ thread_count = [thread_count, our_bt.count].max
+ end
+ end
+ }
+ end
+
+ wp.execute_blocks blocks
+ assert_equal(expected_thread_count, thread_count)
+ end
+end
+
Something went wrong with that request. Please try again.