runAll<T> method
Runs children side by side and stops the rest as soon as one of them
goes wrong.
Starts every child synchronously, in the order of the list, and returns their values in that same order. As soon as the body of a branch ends in anything but a value, the other branches are asked to stop with SiblingCancelReason, and the group throws a real failure if there is one, otherwise the first cancellation that arrived.
Admission errors are those of run, and the same handle twice is an
ArgumentError. A refusal found before the first start starts none of
the handles, the sound ones included; one only the adoption can give may
come after some have started, and stops them. A handle refused or never
started stays with the code that made it, its done waiting until that
code starts or cancels it.
A branch must not wait for another branch of the same group — for its
Job.value or Job.done, or for a lock or a slot of a pool it releases
in its cleanup. No branch unwinds before the group has decided, so the
group hangs, with no timer and no error. Take such a lock in a child of
the branch, which releases it when the child ends, still inside the body,
or use [a, b].wait, under which every branch unwinds on its own.
How this differs from .wait and Future.wait is in When one failure makes the rest pointless
on the groups page, the hang in Branches that wait for each other,
and the refusals in
When ctx.run refuses a child
on the children page.
Implementation
@override
Future<List<T>> runAll<T>(Iterable<ChildJob<T>> children) {
// Read once, by copy, before anything starts: an `Iterable` promises
// neither repeatability nor stability, and the bodies below start
// synchronously and may change the list this was made from. A
// generator that throws throws from here, before any child has run.
final branches = List<Job<T>>.of(children);
// By identity and not by `==`: a job of a domain may compare itself by
// a key, and two children of one queue are equal and still two jobs.
// Before the first start, so that a repeat is refused without half of
// the group already running.
// The places go into the message: a job with no key reads `Job()`, and
// two of those are told apart only by where they stand.
final seen = Map<Job<T>, int>.identity();
for (final (index, child) in branches.indexed) {
if (seen[child] case final first?) {
throw ArgumentError.value(
'$child',
'children',
'is in the group more than once, at $first and at $index',
);
}
seen[child] = index;
}
// Everything that needs no adoption, for every handle, before the first
// start: otherwise a handle refused at the end of the list leaves the
// branches before it started, their bodies already past their first
// `await`. An empty group is asked the same about the context: it is
// not a way around its checks.
_checkCanRun('run a group of children');
for (final child in branches) {
_checkChild(child, 'children');
}
if (_owner.pendingCancel case final pending?) {
// Every branch turned away, not the first one alone: the rest would be
// left `created`, and whoever awaits one of them would wait for good. An
// engine refusing the adoption is a different answer, and the one `run`
// would give: the first such refusal comes out instead of the
// cancellation, once the rest are turned away.
(Object, StackTrace)? refusal;
for (final child in branches) {
try {
startChild(child);
} on Object catch (error, stackTrace) {
if (!identical(error, pending)) {
refusal ??= (error, stackTrace);
}
}
}
if (refusal case (final error, final stackTrace)) {
Error.throwWithStackTrace(error, stackTrace);
}
throw pending;
}
if (branches.isEmpty) {
// A checkpoint all the same, as a group of children is once they have
// committed: a body that goes on after the call must find the rules
// of its domain still holding. Through the future, as theirs comes.
return Future<List<T>>.sync(() {
check();
return <T>[];
});
}
return _RunAllGroup<T>(this, branches).run();
}