Exercises

1. The method add-dependency in Task permits the creation of cycles in the dependency graph. That is, if you follow dependencies, you can eventually return to the original Task. Show how to create a graph with cycles and explain why the perform method of a Task whose dependencies contain a cycle would never terminate successfully.

Answer: You can create two tasks, and then "short-circuit" them with add-dependency:

    my $a = Task.new({ say 'A' });
    my $b = Task.new({ say 'B' }, $a);
    $a.add-dependency($b);

The perform method will never terminate because the first thing the method does is to call all the perform methods of its dependencies. Because $a and $b are dependencies of each other, none of them would ever get around to calling their callbacks. The program will exhaust memory before it ever prints 'A' or 'B'.

2. Is there a way to detect the presence of a cycle during the course of a perform call? Is there a way to prevent cycles from ever forming through add-dependency?

Answer: To detect the presence of a cycle during a perform call, keep track of which Tasks have started; prevent a Task from starting twice before finishing:

    augment class Task {
        has Bool $!started = False;

        method perform() {
            if $!started++ && !$!done {
                die "Cycle detected, aborting";
            }

            unless $!done {
                .perform() for @!dependencies;
                &!callback();
                $!done = True;
            }
        }
    }

Another approach is to stop cycles from forming during add-dependency by checking whether there's already a dependency running in the other direction. (This is the only situation in which a cycle can occur.) This requires the addition of a helper method depends-on, which checks whether a task depends on another one, either directly or transitively. Note the use of » and [||] to write succinctly what would otherwise have involved looping over all the dependencies of the Task:

    augment class Task {
        method depends-on(Task $some-task) {
            $some-task === any(@!dependencies)
            [||] @!dependencies».depends-on($some-task)
        }

        method add-dependency(Task $dependency) {
            if $dependency.depends-on(self) {
                warn 'Cannot add that task, since it would introduce a cycle.';
                return;
            }
            push @!dependencies, $dependency;
        }
    }

3. How could Task objects execute their dependencies in parallel? (Think especially about how to avoid collisions in "diamond dependencies", where a Task has two different dependencies which in turn have the same dependency.)

Answer: Enabling parallelism is easy; change the line .perform() for @!dependencies; into @!dependencies».perform(). However, there may be race conditions in the case of diamond dependencies, wherein Tasks A starts B and C in parallel, and both start a copy of D, making D run twice. The solution to this is the same as with the cycle-detection in Question 2: introducing an attribute $!started. Note that it's impolite to die if a Task has started but not yet finished, because this time it might be due to parallelism rather than cycles:

    augment class Task {
        has Bool $!started = False;

        method perform() {
            unless $!started++ {
                @!dependencies».perform();
                &!callback();
                $!done = True;
            }
        }
    }