Source code for laserfarm.macro_pipeline

import logging
import sys
import traceback

from dask.distributed import Client, LocalCluster, SSHCluster, as_completed

from laserfarm.pipeline import Pipeline


logger = logging.getLogger(__name__)


[docs] class MacroPipeline(object): """ Class to setup macro pipeline workflows. Each MacroPipeline object entails multiple tasks that correspond to Pipeline instances. All the tasks are run in parallel using dask. We implement dask for embarrassingly parallel tasks, see https://examples.dask.org/applications/embarrassingly-parallel.html Example: >>> class Foo(Pipeline): ... def __init__(self): ... self.pipeline = ['task_info'] ... def task_info(self): ... print(os.getpid()) >>> macro = MacroPipeline() >>> one, two = Foo(), Foo() >>> macro.tasks = [one, two] >>> macro.run() # the default thread parallelism is used 61244 61245 """ def __init__(self): self._tasks = list() self.errors = list() self.outcome = list() self.client = None @property def tasks(self): """ List of tasks that need to be run. """ return self._tasks @tasks.setter def tasks(self, tasks): try: _ = iter(tasks) except TypeError: logger.error('The collection of tasks should be an iterable object.') raise for task in tasks: assert isinstance(task, Pipeline), \ 'Task {} is not a derived Pipeline object'.format(task) self._tasks = [task for task in tasks]
[docs] def add_task(self, task): """ Add pipeline instance to the collection of tasks to be executed. :param task: Pipeline instance. """ assert isinstance(task, Pipeline) self.tasks.append(task) return self
[docs] def set_labels(self, labels): labels_ = [labels]*len(self.tasks) if isinstance(labels, str) else labels try: _ = iter(labels_) except TypeError: logger.error('The labels provided should be an iterable object.') raise assert len(labels_) == len(self.tasks), ('labels length does not match' 'the number of pipelines!') for pipeline, label in zip(self.tasks, labels_): pipeline.label = label
@staticmethod def _run_task(f): try: f() except: traceback.print_exc() raise
[docs] def setup_cluster(self, mode='local', cluster=None, **kwargs): if self.client is not None: raise ValueError('Client is already set - call shutdown first!') if cluster is None: if mode == 'local': cluster = LocalCluster(**kwargs) elif mode == 'ssh': cluster = SSHCluster(**kwargs) elif mode == 'slurm': raise NotImplementedError('Slurm cluster is not implemented' ' in this version!') else: raise RuntimeError('Unknown mode of setup client ' '{}!'.format(mode)) self.client = Client(cluster)
[docs] def run(self): """ Run the macro pipeline. """ futures = [self.client.submit(self._run_task, task.run) for task in self.tasks] map_key_to_index = {future.key: n for n, future in enumerate(futures)} self.errors = [None] * len(self.tasks) self.outcome = [future.status for future in futures] for future, result in as_completed(futures, with_results=True, raise_errors=False): idx = map_key_to_index[future.key] self.outcome[idx] = future.status exc = future.exception() if exc is not None: self.errors[idx] = (type(exc), exc) future.release()
[docs] def print_outcome(self, to_file=None): """ Write outcome of the tasks run. If a file path is not specified, the log outcome is printed to the standard output. :param to_file: file path """ fd = sys.stdout if to_file is None else open(to_file, 'w') for nt, (out, err, task) in enumerate(zip(self.outcome, self.errors, self.tasks)): if err is None: outcome = out else: outcome = '{}: {}, {}'.format(out, err[0].__name__, err[1]) fd.write('{:03d} {:30s} {}\n'.format(nt+1, task.label, outcome)) if to_file is not None: fd.close()
[docs] def get_failed_pipelines(self): return [task for out, task in zip(self.outcome, self.tasks) if out != 'finished']
[docs] def shutdown(self): address = self.client.scheduler.address # get address self.client.close() Client(address).shutdown()