From 0f29b3d0e2c2fd98fcc4a418270f5f31cbb6c94e Mon Sep 17 00:00:00 2001 From: Anders Ingemann Date: Sun, 23 Jun 2013 17:03:55 +0200 Subject: [PATCH] Cycle detection and topological sorting --- base/task.py | 12 ++----- base/tasklist.py | 86 +++++++++++++++++++++++++++++++++++++++++--- common/exceptions.py | 7 ++-- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/base/task.py b/base/task.py index 8592fed..7725865 100644 --- a/base/task.py +++ b/base/task.py @@ -1,7 +1,6 @@ -from functools import total_ordering +from common.exceptions import TaskListError -@total_ordering class Task(object): description = None @@ -42,15 +41,10 @@ class Task(object): msg = ("The task {self} is specified as running before {other}, " "but its phase {phase} lies after the phase {other_phase}" .format(self, other, self.phase, other.phase)) - raise TaskOrderError(msg) + raise TaskListError(msg) for task in self.after: if self.phase < task.phase: msg = ("The task {self} is specified as running after {other}, " "but its phase {phase} lies before the phase {other_phase}" .format(self=self, other=other, phase=self.phase, other_phase=other.phase)) - raise TaskOrderError(msg) - conflict = next(iter(set(self.before) & set(self.after)), None) - if conflict is not None: - msg = ("The task {self} is specified as running both before and after {conflict}" - .format(self=self, conflict=conflict)) - raise TaskOrderError(msg) + raise TaskListError(msg) diff --git a/base/tasklist.py b/base/tasklist.py index 55660b6..0be9299 100644 --- a/base/tasklist.py +++ b/base/tasklist.py @@ -1,3 +1,4 @@ +from common.exceptions import TaskListError import logging log = logging.getLogger(__name__) @@ -17,14 +18,89 @@ class TaskList(object): self.remove(task) self.add(replacement) - def get(self, task): + def get(self, ref): return next(task for task in self.tasks if type(task) is ref) def run(self, bootstrap_info): - log.debug('Tasklist before:\n{list}'.format(list=',\n'.join(str(task) for task in self.tasks) )) - task_list = sorted(self.tasks) - log.debug('Tasklist:\n{list}'.format(list=',\n'.join(str(task) for task in task_list) )) - return + task_list = self.create_list() for task in tasks: log.info(task) task.run(bootstrap_info) + + def create_list(self): + graph = {} + for task in self.tasks: + graph[task] = [self.get(succ) for succ in task.before] + graph[task].extend([succ for succ in self.tasks if type(task) in succ.after]) + + components = self.strongly_connected_components(graph) + cycles_found = 0 + for component in components: + if len(component) > 1: + cycles_found += 1 + log.debug('Cycle: {list}\n'.format(list=', '.join(str(task) for task in component))) + if cycles_found > 0: + msg = ('{0} cycles were found in the tasklist, ' + 'consult the logfile for more information.'.format(cycles_found)) + raise TaskListError(msg) + + sorted_tasks = self.topological_sort(graph) + log.debug('Tasklist:\n\t{list}\n'.format(list='\n\t'.join(str(task) for task in sorted_tasks))) + + + def strongly_connected_components(self, graph): + # Source: http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py + # Find the strongly connected components in a graph using Tarjan's algorithm. + # graph should be a dictionary mapping node names to lists of successor nodes. + + result = [ ] + stack = [ ] + low = { } + + def visit(node): + if node in low: return + + num = len(low) + low[node] = num + stack_pos = len(stack) + stack.append(node) + + for successor in graph[node]: + # print successor + visit(successor) + low[node] = min(low[node], low[successor]) + + if num == low[node]: + component = tuple(stack[stack_pos:]) + del stack[stack_pos:] + result.append(component) + for item in component: + low[item] = len(graph) + + for node in graph: + visit(node) + + return result + + def topological_sort(self, graph): + # Source: http://www.logarithmic.net/pfh-files/blog/01208083168/sort.py + count = { } + for node in graph: + count[node] = 0 + for node in graph: + for successor in graph[node]: + count[successor] += 1 + + ready = [ node for node in graph if count[node] == 0 ] + + result = [ ] + while ready: + node = ready.pop(-1) + result.append(node) + + for successor in graph[node]: + count[successor] -= 1 + if count[successor] == 0: + ready.append(successor) + + return result diff --git a/common/exceptions.py b/common/exceptions.py index 0245a0c..b4b76cb 100644 --- a/common/exceptions.py +++ b/common/exceptions.py @@ -9,9 +9,8 @@ class ManifestError(Exception): return "Error in manifest {0}: {1}".format(self.manifest.path, self.message) -class TaskOrderError(Exception): - def __init__(self, message, task): +class TaskListError(Exception): + def __init__(self, message): self.message = message - self.task = task def __str__(self): - return "Error in task order: {1}".format(self.message) + return "Error in tasklist: {0}".format(self.message)