NAME
App::Actions::Runner::Parallel - run the jobs of one matrix beside each other
SYNOPSIS
my $parallel = App::Actions::Runner::Parallel->new(
capacity => 4,
temp_path => $temp_path,
run_job => sub { my ($entry) = @_; ...; return $result },
on_result => sub { my ($result) = @_; ... },
);
my $outcomes = $parallel->run(\@plans, $fail_fast);
if (!$outcomes) { ... run them one at a time ... }
Count the failures, and say where the logs are:
my $outcomes = $parallel->run(\@plans, 0) or return;
my @failed = grep { $_->{result}{result} eq 'failure' } grep { $_->{started} } @{$outcomes};
print scalar(@failed), " jobs failed\n";
print "$_->{name}: $_->{path}\n" for @{ $parallel->logs };
DESCRIPTION
In short: this module runs several jobs of one matrix at the same time, each in its own process, and shows their output as it happens with the job name in front of every line. It is a part of App::Actions::Runner; you rarely use it on its own.
Each job runs in a child process of its own, with its own runner directory, its output written to a log, and its result written to a file the parent reads when it exits. The parent shows each log as it grows, a line at a time, with the job's name in front of every line - [test (n=one)] - so the lines of jobs working at the same moment can be told apart. With fail-fast, a failed job stops further jobs from starting; the ones already running finish.
METHODS
new
ARGUMENTS
capacity- how many jobs may run at once.temp_path- the run's temporary directory; each job getsjob-Nunder it, withjob-N.logandjob-N.jsonbeside it.run_job- a code reference that runs one job in the current process and returns its result hash; it is called in the child.on_result- optional; called in the parent with each result read back, so the caller can take what the child learned (the images it pulled).quiet- optional; show nothing of the jobs' logs.
RETURNS
An App::Actions::Runner::Parallel object.
SIDE EFFECTS
None.
EXAMPLE
See the "SYNOPSIS".
API SPECIFICATION
INPUT
{
capacity => { type => 'integer', min => 1 },
temp_path => { type => 'string', min => 1 },
run_job => { type => 'coderef' },
on_result => { type => 'coderef', optional => 1 },
quiet => { type => 'boolean', optional => 1 },
}
OUTPUT
{ type => 'object', isa => 'App::Actions::Runner::Parallel' }
MESSAGES
+---------------------------+------------------------------+-----------------------------+
| Message | Meaning | Resolution |
+---------------------------+------------------------------+-----------------------------+
| Invalid arguments to | An argument is missing or | Pass the documented |
| ...Parallel::new: REASON | has the wrong type | arguments |
+---------------------------+------------------------------+-----------------------------+
run
my $outcomes = $parallel->run(\@plans, $fail_fast);
Runs one matrix group's jobs beside each other.
ARGUMENTS
\@plans- the runnable plans, each withdisplay_nameandmax_parallel, in the order the jobs should start.$fail_fast- whether a failure stops the jobs not yet started.
RETURNS
undef when the group is better run one at a time - a single job, a capacity below two once max-parallel is applied, or a host without a reliable fork() - so the caller can fall back to its simple loop. Otherwise an array reference of { plan, started, result }, one per plan in plan order.
SIDE EFFECTS
Forks; prints the jobs' output, prefixed; writes their logs and results under the temporary directory. Kills the jobs still running if one could not report or the parent is interrupted, and then dies.
EXAMPLE
my $outcomes = $parallel->run(\@plans, 1) or return $self->_run_serially(\@plans);
for my $outcome (@{$outcomes}) {
print "$outcome->{plan}{display_name}: ", ($outcome->{started} ? $outcome->{result}{result} : 'skipped'), "\n";
}
API SPECIFICATION
INPUT
{
plans => { type => 'arrayref' },
fail_fast => { type => 'boolean', optional => 1 },
}
OUTPUT
{ type => 'arrayref', optional => 1 }
MESSAGES
+-----------------------------+------------------------------+-----------------------------+
| Message | Meaning | Resolution |
+-----------------------------+------------------------------+-----------------------------+
| job 'X' did not run | The job's process died | Read its log, shown above |
| | before it reported a result | |
| action-run: E (in a log) | The job itself died | Read the message |
+-----------------------------+------------------------------+-----------------------------+
PSEUDOCODE
work out the capacity: the configured one, capped by max-parallel and
by the number of plans
give up (return undef) for fewer than two plans, a capacity below two,
or Windows
start jobs until the capacity is reached
until no job is running:
show what each running job has written since the last look
when a job exits, read its result and show the rest of its log
a job with no result stops new starts and is an error
a failed job under fail-fast stops new starts
otherwise start more jobs
on an error, kill the running jobs and die with it
return the outcomes
logs
Returns an array reference of the logs the jobs wrote: { slot, name, file, path }, where file is a name that says which job the log belongs to once the numbered directory is gone.
EXAMPLE
print "$_->{name}: $_->{path}\n" for @{ $parallel->logs };
API SPECIFICATION
INPUT
{}
OUTPUT
{ type => 'arrayref' }
MESSAGES
None.
LIMITATIONS
Windows has no reliable
fork(), so its jobs run one at a time.A run that keeps its containers, a dry run and an explain run are not run in parallel; the caller decides that, because only it knows why.
The logs are polled rather than watched, twenty times a second.
A killed job's containers are not removed: only the process that started them knew their names.
COMMON PITFALLS
runmay returnundef. That is not an error: it means "run these one at a time". Always have a fallback.run_jobruns in a child process. Changes it makes to variables are not seen by the parent. Return everything the parent needs in the result.max-parallelwins. The workflow'sstrategy.max-parallelcan lowercapacity, never raise it.Fail-fast does not stop running jobs. It only stops jobs that have not started yet.
Output lines can be delayed. The logs are read 20 times a second, and a line is shown only when it is complete.
A killed parent leaves containers. If the parent process is killed with SIGKILL, the children's containers are not removed. Use
docker ps --filter name=action-run-to find them.
ENCODING
Job output is copied from the log files to your terminal as bytes, unchanged, so it may contain any characters the job printed. Windows line ends (\r\n) are shown as \n. Job names in the line prefix are Perl character strings and may contain non-ASCII characters and emoji; log file names are made from them with unsafe characters replaced, so they are ASCII.
AUTHOR
Nigel Horne, njh@nigelhorne.com
FORMAL SPECIFICATION
This section describes each routine in the Z notation, a precise mathematical language for describing programs. You do not need to read it to use the module: everything here is also said in plain words in the sections above. It is for readers who want to check exactly what a routine promises.
How to read it: a box names a routine. The lines above the middle rule declare the values it works with. A name ending in ? is an input, and a name ending in ! is an output. ΞState means the routine changes nothing, and ΔState means it can change the state. The lines below the rule are the conditions that are always true when the routine returns.
new
┌─ Parallel ────────────────────────────────────────
│ capacity : ℕ₁
│ slots : ℕ -- jobs started so far, numbering their files
│ logs : seq LOG
├───────────────────────────────────────────────────
│ #logs = slots
└───────────────────────────────────────────────────
run
┌─ RunParallel ─────────────────────────────────────
│ ΔParallel
│ plans? : seq PLAN
│ failFast? : 𝔹
│ out! : optional seq OUTCOME
├───────────────────────────────────────────────────
│ #plans? < 2 ∨ cap < 2 ∨ os = MSWin32 ⇒ out! = ⊥
│ otherwise #out! = #plans? ∧
│ (∀ t : TIME • #{o : ran out! | running(o, t)} ≤ cap) ∧
│ (failFast? ∧ (∃ o : ran out! • failed(o)) ⇒ no plan after the
│ first failure is started once it is seen)
│ where cap = min(capacity, maxParallel(head plans?), #plans?)
└───────────────────────────────────────────────────
logs
┌─ Logs ────────────────────────────────────────────
│ ΞParallel
│ l! : seq LOG
├───────────────────────────────────────────────────
│ l! = logs
└───────────────────────────────────────────────────
STATE DIAGRAM
The parent process goes through these states for one call to "run"; each child goes through the states on the right:
run(\@plans, $fail_fast)
|
v
one plan, capacity < 2, or no reliable fork()? --yes--> return undef
| no (caller runs them serially)
v
+------------------+ Child (one per job):
| STARTING | fork while fewer +--------------------+
| | than capacity run --->| RUNNING |
+------------------+ | own runner folder; |
| | output -> log file |
v +--------------------+
+------------------+ |
| WATCHING | every 1/20 s: new complete | run_job returns
| | log lines printed as v
| | "[job name] line" +--------------------+
+------------------+ | RESULT WRITTEN |
| | | | to a file; exit |
| | | +--------------------+
| | | a child exits
| | v
| | +------------------+ result file missing or unreadable:
| | | COLLECTING | kill the others -> die
| | | on_result(...) |
| | +------------------+
| | |
| | | failure and fail-fast: start no more jobs
| | | otherwise: back to STARTING if plans are left
| |
| | SIGINT / SIGTERM: kill the children -> die
|
| all children done
v
return [ { plan, started, result }, ... ] (logs() lists the log files)
LICENSE AND COPYRIGHT
Copyright 2026 Nigel Horne.
This program is released under the GNU General Public License, version 2. See the LICENSE file that comes with this distribution. If you use it, please let me know.