From 3a1b5bc90eac771d473e25398138b9866100b9fa Mon Sep 17 00:00:00 2001 From: ptarjan Date: Tue, 21 Jun 2016 17:31:43 -0700 Subject: [PATCH] Do not run tasks when their prerequisites fail If tasks A and B both depend on C, they race to grab the invocation lock on C. If A wins, it sets @already_invoked and executes C while holding the lock. When C completes, A releases the lock, B acquires it and immediately returns since C is @already_invoked. Unfortunately, this does not distinguish failed execution of C. Instead, if executing C raises an exception for A, we want to raise the same exception in B so it is aborted. --- lib/rake/multi_task.rb | 36 ++++++++++++++++++++++++++++++++++++ test/test_rake_multi_task.rb | 21 +++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/lib/rake/multi_task.rb b/lib/rake/multi_task.rb index 5418a7a7b..7118dc411 100644 --- a/lib/rake/multi_task.rb +++ b/lib/rake/multi_task.rb @@ -4,6 +4,42 @@ module Rake # parallel using Ruby threads. # class MultiTask < Task + + # Same as invoke, but explicitly pass a call chain to detect + # circular dependencies. This is largely copied from Rake::Task + # but has been updated such that if multiple tasks depend on this + # one in parallel, they will all fail if the first execution of + # this task fails. + def invoke_with_call_chain(task_args, invocation_chain) + new_chain = Rake::InvocationChain.append(self, invocation_chain) + @lock.synchronize do + begin + if @already_invoked + if @invocation_exception + if application.options.trace + application.trace "** Previous invocation of #{name} failed #{format_trace_flags}" + end + raise @invocation_exception + else + return + end + end + + if application.options.trace + application.trace "** Invoke #{name} #{format_trace_flags}" + end + @already_invoked = true + + invoke_prerequisites(task_args, new_chain) + execute(task_args) if needed? + rescue Exception => ex + add_chain_to(ex, new_chain) + @invocation_exception = ex + raise + end + end + end + private def invoke_prerequisites(task_args, invocation_chain) # :nodoc: invoke_prerequisites_concurrently(task_args, invocation_chain) diff --git a/test/test_rake_multi_task.rb b/test/test_rake_multi_task.rb index 9f8fed6d5..f7a004164 100644 --- a/test/test_rake_multi_task.rb +++ b/test/test_rake_multi_task.rb @@ -61,4 +61,25 @@ def test_multitasks_with_parameters assert @runs[0] == "b" assert @runs[1] == "bmt" end + + def test_cross_thread_prerequisite_failures + failed = false + + multitask :fail_once do + fail_now = !failed + failed = true + raise 'failing once' if fail_now + end + + task a: :fail_once + task b: :fail_once + + assert_raises RuntimeError do + Rake::Task[:a].invoke + end + + assert_raises RuntimeError do + Rake::Task[:b].invoke + end + end end