Task executors
A task executor runs a TaskGraph. It runs
each task only after the tasks that it depends on, and it asks a task cache
which tasks must run. This page describes the three task executors of GAIn,
the functions that run a graph with one call, and the task caches.
Creating a task executor
GAIn has three task executors. Each one takes the keyword argument
task_cache, the task cache that decides which tasks must run. Without
it, the task executor runs every task.
SequentialExecutor()Runs the tasks one at a time, in the current process. It is in
gain.task_graph.sequential_executor.ProcessPoolTaskExecutor(max_workers=...)Runs the tasks in a pool of processes.
max_workersis the number of processes. Without it, the pool has one process for each CPU. It is ingain.task_graph.process_pool_executor.DaskExecutor(client)Runs the tasks on the dask cluster of
client. It is ingain.task_graph.dask_executor.
The three task executors derive from
TaskGraphExecutorBase, which also
takes these keyword arguments:
forceWhen true, the task executor runs every task and does not read the task cache. It still records the outcome of each task in the task cache.
task_log_dirThe directory of the task logs. Without it, the task executor writes them into
.task-login the current directory.verboseThe verbosity of the task logs.
fork_tasksWhen true, each task runs in a new process.
task_status_dirThe directory of the result files of the forked tasks. Without it, the task executor writes them into the current directory.
Only SequentialExecutor and DaskExecutor use task_log_dir,
verbose, fork_tasks and task_status_dir.
ProcessPoolTaskExecutor ignores them: it
creates the directory of the task logs, but it writes no task log and it
does not fork the tasks.
A process pool or a dask cluster pickles the function and the arguments of each task. Use module-level functions for the tasks, and pass arguments that pickle, for example a resource and not an open genome.
The lifecycle
A task executor is a context manager. The with block calls
close() when it ends, and
close releases the workers:
from gain.task_graph.sequential_executor import SequentialExecutor
with SequentialExecutor() as executor:
for task, result in executor.execute(graph):
print(task.task_id, result)
execute()
consumes the graph and yields each task with its result when the task
completes. The order is the order of completion, not the order of the
graph. The result of a failed task is its exception: execute does not
raise it. A task executor runs one graph at a time, so exhaust the
generator of a run before you start the next run.
DaskExecutor takes ownership of its client. Its close shuts down the
client and the cluster of the client, so do not use the client after the
with block.
Running a graph with one call
task_graph_run() and
task_graph_run_with_results(), in
gain.task_graph.cli_tools, run a graph with one call. Without a task
executor, they create a SequentialExecutor.
task_graph_run returns true when no task failed.
task_graph_run_with_results yields the result of each task, without the
task. Without keep_going, the first failed task raises its error and
ends the run. With keep_going=True, the run prints the error of each
failed task and continues.
from gain.task_graph.cli_tools import task_graph_run
with SequentialExecutor() as executor:
assert task_graph_run(graph, executor)
The task cache
A task cache decides which tasks must run and records the outcome of each
task that runs. The classes are in gain.task_graph.cache.
NoTaskCacheRuns every task and records nothing. It is the default of each task executor.
FileTaskCacheWrites one record file for each task into the task status directory,
cache_dir. The record holds the result of the task, or its exception. The modification times of the files of a task decide whether a computed record is still valid: an input file that is newer than the output files of the task makes the task run again.
TaskCache.create picks the
task cache for a set of options. It picks NoTaskCache when force is
true or the status mode, task_progress_mode, is off. Otherwise it picks
a FileTaskCache. Without cache_dir, the FileTaskCache uses the
current directory.
get_record() returns the
CacheRecord of a task. The type of the
record is a CacheRecordType:
NEEDS_COMPUTE, COMPUTED or ERROR. result holds the result of
a computed task.
Before a run, execute asks the task cache for the record of each task.
It removes each task that the task cache reports as computed from the
graph, and it does not yield that task. A task that depends on a task that
must run runs again, even when its own record is computed.
A running example, part 2
Part 1, on the page Graphs, builds a graph that counts the N
bases of a reference genome. The examples here run that graph. Each run
builds a new graph, because a run consumes the graph.
The first run is sequential. task_graph_run_with_results yields the
results without their tasks. The summary task depends on all the other
tasks, so it completes last, and the last result is the sum:
def run_sequentially(resource: GenomicResource) -> int:
"""Count the ``N`` bases of the genome, one task at a time."""
graph, _ = build_n_count_graph(resource)
with SequentialExecutor() as executor:
results = list(task_graph_run_with_results(graph, executor))
# The summary task depends on all the other tasks, so it completes last.
total: int = results[-1]
return total
The second run uses a pool of two processes. The task functions are module-level, so the pool can pickle them:
def run_in_process_pool(resource: GenomicResource) -> int:
"""Count the ``N`` bases of the genome in a pool of two processes."""
graph, _ = build_n_count_graph(resource)
with ProcessPoolTaskExecutor(max_workers=2) as executor:
results = list(task_graph_run_with_results(graph, executor))
total: int = results[-1]
return total
The third run gives the task executor a FileTaskCache over a task status
directory. The task cache writes a record file for each task. Then the
example builds a fresh graph and runs it with a new FileTaskCache over
the same directory. The record type of each task is COMPUTED, the record
of the summary task holds the sum, and execute yields nothing:
def run_with_file_task_cache(
resource: GenomicResource, status_dir: str,
) -> None:
"""Run the graph and write a record for each task into ``status_dir``."""
graph, _ = build_n_count_graph(resource)
task_cache = FileTaskCache(cache_dir=status_dir)
with SequentialExecutor(task_cache=task_cache) as executor:
assert task_graph_run(graph, executor)
def rerun_with_file_task_cache(
resource: GenomicResource, status_dir: str,
) -> tuple[list[tuple[Task, Any]], dict[str, CacheRecord]]:
"""Run a fresh graph over the records of an earlier run."""
graph, _ = build_n_count_graph(resource)
task_cache = FileTaskCache(cache_dir=status_dir)
records = {
task.task_id: task_cache.get_record(graph.get_task_desc(task))
for task in graph.tasks
}
with SequentialExecutor(task_cache=task_cache) as executor:
completed = list(executor.execute(graph))
return completed, records
API
Each class below is defined in the module that the section names.
task_graph_run and task_graph_run_with_results are defined in the
module gain.task_graph.cli_tools.
The interface of a task executor. |
|
Release the workers and the other resources of the task executor. |
|
|
Run the tasks of a task graph. |
|
The base of the task executors of GAIn. |
Run the tasks of a task graph that must run. |
|
A task executor that runs the tasks one at a time. |
Do nothing: a sequential task executor holds no workers. |
|
A task executor that runs the tasks in a pool of processes. |
Shut the pool down and wait for its processes to exit. |
|
A task executor that runs the tasks on a dask cluster. |
Shut down the dask client and its cluster. |
Inherited from TaskGraphExecutorBase by the three task executors:
Run the tasks of a task graph that must run. |
The interface of a task cache. |
|
|
Record the outcome of a task. |
|
Create the task cache for a set of options. |
|
Return the record of a task. |
|
A task cache that keeps one record file for each task. |
|
Write the record file of a task. |
|
Return the record of a task from its record file. |
A task cache that runs every task and records nothing. |
|
|
Do nothing: the task cache records no outcome. |
|
Return a record whose type is |
|
The record of one task in a task cache. |
The exception of a task whose record type is |
|
The result of a task whose record type is |
|
|
The state of a task in a task cache. |
Inherited from TaskCache by FileTaskCache and NoTaskCache:
|
Create the task cache for a set of options. |
|
Run a task graph with a task executor. |
|
Run a task graph and yield the result of each task. |
Task executors
- class TaskGraphExecutor[source]
The interface of a task executor.
A task executor runs a task graph. It is a context manager: the
withblock closes the task executor when it ends, and so releases its workers. A failure inclose()is logged and does not hide an error of thewithblock.The task executors of GAIn are
SequentialExecutor,ProcessPoolTaskExecutorandDaskExecutor.- abstractmethod close() None[source]
Release the workers and the other resources of the task executor.
The
withblock of a task executor calls this method when it ends.
- abstractmethod execute(graph: TaskGraph) Generator[tuple[Task, Any], None, None][source]
Run the tasks of a task graph.
The task executor consumes the graph: the graph has no tasks left when the run ends. Build a new graph for each run.
- Parameters:
graph (TaskGraph) – The task graph to run.
- Yields:
tuple[Task, Any] – Each task and its result, when the task completes. The result of a failed task is its exception. The order is not the order of the graph, and not the order in which the tasks run. A task is yielded only after it has finished.
- class TaskGraphExecutorBase(task_cache: TaskCache = {}, *, force: bool = False, **kwargs: Any)[source]
The base of the task executors of GAIn.
The base runs each task only after the tasks that it depends on, and it asks a task cache which tasks must run. It records the result or the error of each task that runs in the task cache.
- Parameters:
task_cache (TaskCache) – The task cache that decides which tasks must run. The default is a
NoTaskCache, which runs every task.force (bool) – When true, the task executor runs every task and does not read the task cache. It still writes the outcome of each task to it.
**kwargs – The options of the task executor.
task_log_dirnames the directory of the task logs. Without it, the task executor writes them into.task-login the current directory.verbosesets the verbosity of the task logs.fork_tasksruns each task in a new process, andtask_status_dirnames the directory of the result files of those processes.
- execute(graph: TaskGraph) Generator[tuple[Task, Any], None, None][source]
Run the tasks of a task graph that must run.
The method first asks the task cache which tasks are computed, and removes them from the graph. It does not yield them. Then it runs the other tasks, records the outcome of each one in the task cache and yields it. When the task cache reports each task as computed, the method yields nothing.
A task executor runs one graph at a time. Exhaust or close the generator of a run before you start the next run.
- Parameters:
graph (TaskGraph) – The task graph to run. The method consumes it.
- Yields:
tuple[Task, Any] – Each task that ran and its result, when the task completes. The result of a failed task is its exception.
- class SequentialExecutor(task_cache: TaskCache = {}, *, force: bool = False, **kwargs: Any)[source]
A task executor that runs the tasks one at a time.
A task runs only after the tasks that it depends on. The tasks run in the current process, unless the
fork_tasksoption is true: then each task runs in a new process. The constructor takes the keyword arguments ofTaskGraphExecutorBase:task_cache,forceand the options of the task executor.
- class ProcessPoolTaskExecutor(**kwargs: Any)[source]
A task executor that runs the tasks in a pool of processes.
The constructor creates the pool. The pool starts its worker processes when the task executor submits the first task. A task runs in a worker process only after the tasks that it depends on. The task executor pickles the function and the arguments of each task, so the function must be importable, for example a module-level function.
- Parameters:
**kwargs –
max_workersis the number of processes in the pool. Without it, the pool has one process for each CPU. The other keyword arguments are those ofTaskGraphExecutorBase:task_cache,forceand the options of the task executor. The process pool ignores the optionstask_log_dir,verboseandfork_tasks: it creates the directory of the task logs, but it writes no task log and it does not fork the tasks.
- class DaskExecutor(dask_client: Client, task_cache: TaskCache = {}, **kwargs: Any)[source]
A task executor that runs the tasks on a dask cluster.
The task executor takes ownership of the dask client:
close()shuts down the client and its cluster. A task runs on a worker of the cluster only after the tasks that it depends on.- Parameters:
dask_client (Client) – The dask client of the cluster that runs the tasks.
task_cache (TaskCache) – The task cache that decides which tasks must run. The default is a
NoTaskCache, which runs every task.**kwargs – The other keyword arguments of
TaskGraphExecutorBase:forceand the options of the task executor.
- Variables:
MAX_RUNNING_TASKS (int) – The task executor takes new tasks from the graph only while fewer than this number of its tasks are unfinished.
Running a graph
- task_graph_run(task_graph: TaskGraph, executor: TaskGraphExecutor | None = None, *, keep_going: bool = False) bool[source]
Run a task graph with a task executor.
- Parameters:
task_graph (TaskGraph) – The task graph to run. The run consumes it.
executor (TaskGraphExecutor | None) – The task executor that runs the graph.
Nonemeans a newSequentialExecutor.keep_going (bool) – When false, the first failed task raises its error and ends the run. When true, the run prints the error of each failed task to the standard error and continues.
- Returns:
True when no task failed.
- Return type:
bool
- Raises:
Exception – The error of the first failed task, when
keep_goingis false.
- task_graph_run_with_results(task_graph: TaskGraph, executor: TaskGraphExecutor | None = None, *, keep_going: bool = False) Generator[Any, None, None][source]
Run a task graph and yield the result of each task.
The function yields the results without their tasks, in the order in which the tasks complete. A task that the task cache reports as computed does not run, and the function yields no result for it.
- Parameters:
task_graph (TaskGraph) – The task graph to run. The run consumes it.
executor (TaskGraphExecutor | None) – The task executor that runs the graph.
Nonemeans a newSequentialExecutor.keep_going (bool) – When false, the first failed task raises its error and ends the run. When true, the function prints the error of each failed task to the standard error and yields the error as the result.
- Yields:
Any – The result of each task, or the exception of a failed task.
- Raises:
Exception – The error of the first failed task, when
keep_goingis false.
Task caches
- class TaskCache[source]
The interface of a task cache.
A task executor asks the task cache whether a task must run, and records the outcome of each task that runs in it.
- abstractmethod cache(task: Task, *, is_error: bool, result: Any) None[source]
Record the outcome of a task.
- Parameters:
task (Task) – The task that ran.
is_error (bool) – True when the task failed.
result (Any) – The result of the task, or its exception when it failed.
- static create(*, force: bool = False, task_progress_mode: bool = True, cache_dir: str | None = None) TaskCache[source]
Create the task cache for a set of options.
- Parameters:
force (bool) – When true, the method returns a
NoTaskCache.task_progress_mode (bool) – The status mode. When false, the method returns a
NoTaskCache.cache_dir (str | None) – The task status directory of a
FileTaskCache.Nonemeans the current directory.
- Returns:
A
NoTaskCachewhenforceis true or the status mode is off. Otherwise aFileTaskCacheovercache_dir.- Return type:
- abstractmethod get_record(task_desc: TaskDesc) CacheRecord[source]
Return the record of a task.
- Parameters:
task_desc (TaskDesc) – The description of the task, from
get_task_desc().- Returns:
The record of the task. The task must run unless the record type is
COMPUTED.NEEDS_COMPUTEmeans that the task cache has no valid record of the task.- Return type:
- class FileTaskCache(cache_dir: str)[source]
A task cache that keeps one record file for each task.
The record file of a task is
<task id>.flagin the task status directory. It holds the record type and the result or the exception of the task. The modification times of the files of a task decide whether a computed record is still valid.- Parameters:
cache_dir (str) – The task status directory. The task cache creates it when it writes the first record.
- cache(task: Task, *, is_error: bool, result: Any) None[source]
Write the record file of a task.
A failure to write the file is logged, and the run continues.
- Parameters:
task (Task) – The task that ran.
is_error (bool) – True when the task failed. The record type is then
ERROR, otherwiseCOMPUTED.result (Any) – The result of the task, or its exception when it failed. It must be picklable.
- get_record(task_desc: TaskDesc) CacheRecord[source]
Return the record of a task from its record file.
A computed record stays valid while the output files of the task exist and are not older than its input files. A missing input file makes the record invalid. A missing intermediate output file does not, because a later task can consume it. A task without output files compares its input files with its record file. A missing or unreadable record file means that the task must run.
- Parameters:
task_desc (TaskDesc) – The description of the task, with its input and output files.
- Returns:
The record of the task. Its type is
NEEDS_COMPUTEwhen the record file is missing or unreadable, or when the record is computed but no longer valid. Otherwise it is the record that the file holds,COMPUTEDorERROR.- Return type:
- class NoTaskCache[source]
A task cache that runs every task and records nothing.
- cache(task: Task, *, is_error: bool, result: Any) None[source]
Do nothing: the task cache records no outcome.
- Parameters:
task (Task) – The task that ran.
is_error (bool) – True when the task failed.
result (Any) – The result of the task, or its exception when it failed.
- get_record(task_desc: TaskDesc) CacheRecord[source]
Return a record whose type is
NEEDS_COMPUTE.- Parameters:
task_desc (TaskDesc) – The description of the task. The method does not read it.
- Returns:
A record whose type is
NEEDS_COMPUTEand that holds no result.- Return type:
- class CacheRecord(type: CacheRecordType, result_or_error: Any = None)[source]
The record of one task in a task cache.
- Variables:
type (CacheRecordType) – The state of the task.
result_or_error (Any) – The result of the task, the exception of a failed task, or
Nonewhen the task cache holds no outcome.
- property error: Any
The exception of a task whose record type is
ERROR.
- property result: Any
The result of a task whose record type is
COMPUTED.
- class CacheRecordType(*values)[source]
The state of a task in a task cache.
- Variables:
NEEDS_COMPUTE (CacheRecordType) – The task must run.
COMPUTED (CacheRecordType) – The task ran without an error, and its record is still valid.
ERROR (CacheRecordType) – The task ran and failed.