Skip to content

Commit 81a06eb

Browse files
authored
Keep going (#3)
* Added tests for proc exception re-raising * Implemented keep_going for serial task running * Implemented keep_going for concurrent task running
1 parent dffc5f6 commit 81a06eb

File tree

4 files changed

+97
-18
lines changed

4 files changed

+97
-18
lines changed

CHANGES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Version 0.3.3
4+
5+
* Implemented keep-going flag
6+
37
## Version 0.3.2
48

59
* Add support for running only some tasks in auto_run

shard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: croupier
2-
version: 0.3.2
2+
version: 0.3.3
33
description: A smart task definition and execution library
44
authors:
55
- Roberto Alsina <roberto.alsina@gmail.com>

spec/croupier_spec.cr

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,16 @@ describe "Task" do
284284
end
285285
end
286286

287+
it "should fail if the proc raises an exception" do
288+
with_scenario("empty") do
289+
b = TaskProc.new { raise "foo" }
290+
t = Task.new("output2", proc: b)
291+
expect_raises(Exception, "Task 052cd9c::output2 failed: foo") do
292+
t.run
293+
end
294+
end
295+
end
296+
287297
it "should record hash for outputs in the TaskManager" do
288298
with_scenario("empty") do
289299
t = Task.new(
@@ -461,7 +471,7 @@ describe "TaskManager" do
461471

462472
# Run the same tests for parallel and serial execution of tasks
463473
[false, true].each do |parallel|
464-
describe "run_task, parallel = #{parallel}" do
474+
describe "run_tasks, parallel = #{parallel}" do
465475
it "should run all stale tasks when run_all is false" do
466476
with_scenario("basic", to_create: {"input" => "foo", "input2" => "bar"}) do
467477
TaskManager.tasks["output5"].stale = false
@@ -538,6 +548,43 @@ describe "TaskManager" do
538548
end
539549
end
540550

551+
it "should fail if a proc raises an exception" do
552+
with_scenario("empty") do
553+
b = TaskProc.new { raise "foo" }
554+
Task.new(["output2"], proc: b)
555+
expect_raises(Exception, "Task 052cd9c::output2 failed: foo") do
556+
TaskManager.run_tasks(parallel: parallel)
557+
end
558+
end
559+
end
560+
561+
# This is very hard to assert on parallel execution
562+
unless parallel
563+
it "should abort when a proc raises an exception" do
564+
with_scenario("empty") do
565+
b = TaskProc.new { raise "foo" }
566+
Task.new(["output2"], proc: b)
567+
Task.new(["output1"])
568+
expect_raises(Exception, "Task 052cd9c::output2 failed: foo") do
569+
TaskManager.run_tasks
570+
end
571+
# It should never have executed the second task
572+
File.exists?("output1").should be_false
573+
end
574+
end
575+
end
576+
577+
it "should not abort when a proc raises an exception with keep_going flag" do
578+
with_scenario("empty") do
579+
Task.new(["output2"], proc: TaskProc.new { raise "foo" })
580+
Task.new(["output1"], proc: TaskProc.new { "foo" })
581+
# Even though a proc raises an exception, it's caught
582+
TaskManager.run_tasks(parallel: parallel, keep_going: true)
583+
# It should never have executed the second task
584+
File.exists?("output1").should be_true
585+
end
586+
end
587+
541588
it "should handle a task that generates multiple outputs" do
542589
with_scenario("empty") do
543590
p = TaskProc.new { ["foo", "bar"] }

src/croupier.cr

Lines changed: 44 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ require "log"
77
require "yaml"
88

99
module Croupier
10-
VERSION = "0.3.2"
10+
VERSION = "0.3.3"
1111

1212
# A Task is an object that may generate output
1313
#
@@ -107,7 +107,11 @@ module Croupier
107107
call_results = Array(String | Nil).new
108108
@procs.each do |proc|
109109
Fiber.yield
110-
result = proc.call
110+
begin
111+
result = proc.call
112+
rescue ex
113+
raise "Task #{self} failed: #{ex}"
114+
end
111115
if result.nil?
112116
call_results << nil
113117
elsif result.is_a?(String)
@@ -380,33 +384,49 @@ module Croupier
380384
#
381385
# If `run_all` is true, run non-stale tasks too
382386
# If `dry_run` is true, only log what would be done, but don't do it
383-
def run_tasks(run_all : Bool = false, dry_run : Bool = false, parallel : Bool = false)
387+
# If `parallel` is true, run tasks in parallel
388+
# If `keep_going` is true, keep going even if a task fails
389+
def run_tasks(
390+
run_all : Bool = false,
391+
dry_run : Bool = false,
392+
parallel : Bool = false,
393+
keep_going : Bool = false
394+
)
384395
mark_stale_inputs
385396
_, tasks = sorted_task_graph
386397
check_dependencies
387-
run_tasks(tasks, run_all, dry_run, parallel)
398+
run_tasks(tasks, run_all, dry_run, parallel, keep_going)
388399
end
389400

390401
# Run the tasks needed to create or update the requested targets
391-
# run_all will run all tasks, not just the ones that are stale
392-
# dry_run will only log what would be done, but not actually do it
402+
#
403+
# If `run_all` is true, run non-stale tasks too
404+
# If `dry_run` is true, only log what would be done, but don't do it
405+
# If `parallel` is true, run tasks in parallel
406+
# If `keep_going` is true, keep going even if a task fails
393407
def run_tasks(
394408
targets : Array(String),
395409
run_all : Bool = false,
396410
dry_run : Bool = false,
397-
parallel : Bool = false
411+
parallel : Bool = false,
412+
keep_going : Bool = false
398413
)
399414
mark_stale_inputs
400415
tasks = dependencies(targets)
401416
if parallel
402-
_run_tasks_parallel(tasks, run_all, dry_run)
417+
_run_tasks_parallel(tasks, run_all, dry_run, keep_going)
403418
else
404-
_run_tasks(tasks, run_all, dry_run)
419+
_run_tasks(tasks, run_all, dry_run, keep_going)
405420
end
406421
end
407422

408-
# Helper to run tasks
409-
def _run_tasks(outputs, run_all : Bool = false, dry_run : Bool = false)
423+
# Internal helper to run tasks serially
424+
def _run_tasks(
425+
outputs,
426+
run_all : Bool = false,
427+
dry_run : Bool = false,
428+
keep_going : Bool = false
429+
)
410430
finished = Set(Task).new
411431
outputs.each do |output|
412432
next unless tasks.has_key?(output)
@@ -415,13 +435,18 @@ module Croupier
415435
next unless run_all || t.stale? || t.@always_run
416436

417437
Log.debug { "Running task for #{output}" }
418-
t.run unless dry_run
438+
begin
439+
t.run unless dry_run
440+
rescue ex
441+
Log.error { "Error running task for #{output}: #{ex}" }
442+
raise ex unless keep_going
443+
end
419444
finished << t
420445
end
421446
save_run
422447
end
423448

424-
# Run all stale tasks as concurrently as possible.
449+
# Internal helper to run tasks concurrently
425450
#
426451
# Whenever a task is ready, launch it in a separate fiber
427452
# This is only concurrency, not parallelism, but on tests
@@ -431,18 +456,20 @@ module Croupier
431456
def _run_tasks_parallel(
432457
targets : Array(String) = [] of String,
433458
run_all : Bool = false,
434-
dry_run : Bool = false
459+
dry_run : Bool = false,
460+
keep_going : Bool = false # FIXME: implement
435461
)
436462
mark_stale_inputs
437463

438464
targets = tasks.keys if targets.empty?
439465
_tasks = dependencies(targets)
440466
finished_tasks = Set(Task).new
467+
failed_tasks = Set(Task).new
441468
errors = [] of String
442469

443470
loop do
444471
stale_tasks = (_tasks.map { |t| tasks[t] }).select(&.stale?).reject { |t|
445-
finished_tasks.includes?(t)
472+
finished_tasks.includes?(t) || failed_tasks.includes?(t)
446473
}
447474

448475
break if stale_tasks.empty?
@@ -456,6 +483,7 @@ module Croupier
456483
begin
457484
t.run unless dry_run
458485
rescue ex
486+
failed_tasks << t
459487
errors << ex.message.to_s
460488
ensure
461489
# Task is done, do not run again
@@ -466,7 +494,7 @@ module Croupier
466494
end
467495
sleep(0.001)
468496
end
469-
raise errors.join("\n") unless errors.empty?
497+
raise errors.join("\n") unless errors.empty? unless keep_going
470498
# FIXME It's losing outputs for some reason
471499
save_run
472500
end

0 commit comments

Comments
 (0)