Iteration over links in flow interface

In addition to iteration over its children (atoms or subflows) each
pattern now provides iter_links() method that iterates over
dependency links. This allows engines to treat all patterns
in the same way instead knowing what structure each pattern expresses.

Change-Id: I52cb5b0b501eefc8eb56a9ef5303aeb318013e11
This commit is contained in:
Ivan A. Melnikov
2014-03-17 14:15:53 +04:00
parent 4252eb0277
commit 1011df951e
6 changed files with 80 additions and 82 deletions

View File

@@ -38,13 +38,6 @@ class Flow(object):
a flow is just a 'structuring' concept this is typically a behavior that a flow is just a 'structuring' concept this is typically a behavior that
should not be worried about (as it is not visible to the user), but it is should not be worried about (as it is not visible to the user), but it is
worth mentioning here. worth mentioning here.
Flows are expected to provide the following methods/properties:
- add
- __len__
- requires
- provides
""" """
def __init__(self, name, retry=None): def __init__(self, name, retry=None):
@@ -77,6 +70,21 @@ class Flow(object):
def __len__(self): def __len__(self):
"""Returns how many items are in this flow.""" """Returns how many items are in this flow."""
@abc.abstractmethod
def __iter__(self):
"""Iterates over the children of the flow."""
@abc.abstractmethod
def iter_links(self):
"""Iterates over dependency links between children of the flow.
Iterates over 3-tuples ``(A, B, meta)``, where
* ``A`` is a child (atom or subflow) link starts from;
* ``B`` is a child (atom or subflow) link points to; it is
said that ``B`` depends on ``A`` or ``B`` requires ``A``;
* ``meta`` is link metadata, a dictionary.
"""
def __str__(self): def __str__(self):
lines = ["%s: %s" % (reflection.get_class_name(self), self.name)] lines = ["%s: %s" % (reflection.get_class_name(self), self.name)]
lines.append("%s" % (len(self))) lines.append("%s" % (len(self)))

View File

@@ -153,13 +153,25 @@ class Flow(flow.Flow):
self._swap(tmp_graph) self._swap(tmp_graph)
return self return self
def _get_subgraph(self):
"""Get the active subgraph of _graph.
Descendants may override this to make only part of self._graph
visible.
"""
return self._graph
def __len__(self): def __len__(self):
return self.graph.number_of_nodes() return self._get_subgraph().number_of_nodes()
def __iter__(self): def __iter__(self):
for n in self.graph.nodes_iter(): for n in self._get_subgraph().nodes_iter():
yield n yield n
def iter_links(self):
for (u, v, e_data) in self._get_subgraph().edges_iter(data=True):
yield (u, v, e_data)
@property @property
def provides(self): def provides(self):
provides = set() provides = set()
@@ -176,10 +188,6 @@ class Flow(flow.Flow):
requires.update(subflow.requires) requires.update(subflow.requires)
return requires - self.provides return requires - self.provides
@property
def graph(self):
return self._graph
class TargetedFlow(Flow): class TargetedFlow(Flow):
"""Graph flow with a target. """Graph flow with a target.
@@ -227,8 +235,7 @@ class TargetedFlow(Flow):
self._subgraph = None self._subgraph = None
return self return self
@property def _get_subgraph(self):
def graph(self):
if self._subgraph is not None: if self._subgraph is not None:
return self._subgraph return self._subgraph
if self._target is None: if self._target is None:

View File

@@ -18,6 +18,10 @@ from taskflow import exceptions
from taskflow import flow from taskflow import flow
# TODO(imelnikov): add metadata describing link here
_LINK_METADATA = dict()
class Flow(flow.Flow): class Flow(flow.Flow):
"""Linear Flow pattern. """Linear Flow pattern.
@@ -71,6 +75,11 @@ class Flow(flow.Flow):
for child in self._children: for child in self._children:
yield child yield child
def iter_links(self):
for src, dst in zip(self._children[:-1],
self._children[1:]):
yield (src, dst, _LINK_METADATA.copy())
def __getitem__(self, index): def __getitem__(self, index):
return self._children[index] return self._children[index]

View File

@@ -102,3 +102,8 @@ class Flow(flow.Flow):
def __iter__(self): def __iter__(self):
for child in self._children: for child in self._children:
yield child yield child
def iter_links(self):
# NOTE(imelnikov): children in unordered flow have no dependencies
# betwean each other by construction.
return iter(())

View File

@@ -54,11 +54,12 @@ class GraphFlowTest(test.TestCase):
provides=[], provides=[],
requires=['c']) requires=['c'])
wf.add(test_1, test_2, test_3) wf.add(test_1, test_2, test_3)
self.assertTrue(wf.graph.has_edge(test_1, test_2)) self.assertEqual(3, len(wf))
self.assertTrue(wf.graph.has_edge(test_2, test_3))
self.assertEqual(3, len(wf.graph)) edges = [(src, dst) for src, dst, _meta in wf.iter_links()]
self.assertEqual([test_1], list(gu.get_no_predecessors(wf.graph))) self.assertIn((test_1, test_2), edges)
self.assertEqual([test_3], list(gu.get_no_successors(wf.graph))) self.assertIn((test_2, test_3), edges)
self.assertEqual(2, len(edges))
def test_basic_edge_reasons(self): def test_basic_edge_reasons(self):
wf = gw.Flow("the-test-action") wf = gw.Flow("the-test-action")
@@ -69,17 +70,17 @@ class GraphFlowTest(test.TestCase):
provides=['c'], provides=['c'],
requires=['a', 'b']) requires=['a', 'b'])
wf.add(test_1, test_2) wf.add(test_1, test_2)
self.assertTrue(wf.graph.has_edge(test_1, test_2)) edges = list(wf.iter_links())
self.assertEqual(len(edges), 1)
from_task, to_task, edge_attrs = edges[0]
self.assertIs(from_task, test_1)
self.assertIs(to_task, test_2)
edge_attrs = gu.get_edge_attrs(wf.graph, test_1, test_2)
self.assertTrue(len(edge_attrs) > 0) self.assertTrue(len(edge_attrs) > 0)
self.assertIn('reasons', edge_attrs) self.assertIn('reasons', edge_attrs)
self.assertEqual(set(['a', 'b']), edge_attrs['reasons']) self.assertEqual(set(['a', 'b']), edge_attrs['reasons'])
# 2 -> 1 should not be linked, and therefore have no attrs
no_edge_attrs = gu.get_edge_attrs(wf.graph, test_2, test_1)
self.assertFalse(no_edge_attrs)
def test_linked_edge_reasons(self): def test_linked_edge_reasons(self):
wf = gw.Flow("the-test-action") wf = gw.Flow("the-test-action")
test_1 = utils.ProvidesRequiresTask('test-1', test_1 = utils.ProvidesRequiresTask('test-1',
@@ -89,11 +90,16 @@ class GraphFlowTest(test.TestCase):
provides=[], provides=[],
requires=[]) requires=[])
wf.add(test_1, test_2) wf.add(test_1, test_2)
self.assertFalse(wf.graph.has_edge(test_1, test_2)) self.assertEqual(len(list(wf.iter_links())), 0)
wf.link(test_1, test_2)
self.assertTrue(wf.graph.has_edge(test_1, test_2)) wf.link(test_1, test_2)
edges = list(wf.iter_links())
self.assertEqual(len(edges), 1)
from_task, to_task, edge_attrs = edges[0]
self.assertIs(from_task, test_1)
self.assertIs(to_task, test_2)
edge_attrs = gu.get_edge_attrs(wf.graph, test_1, test_2)
self.assertTrue(len(edge_attrs) > 0) self.assertTrue(len(edge_attrs) > 0)
self.assertTrue(edge_attrs.get('manual')) self.assertTrue(edge_attrs.get('manual'))
@@ -176,11 +182,11 @@ class TargetedGraphFlowTest(test.TestCase):
provides=[], requires=['a']) provides=[], requires=['a'])
wf.add(test_1) wf.add(test_1)
wf.set_target(test_1) wf.set_target(test_1)
self.assertEqual(1, len(wf.graph)) self.assertEqual(1, len(wf))
test_2 = utils.ProvidesRequiresTask('test-2', test_2 = utils.ProvidesRequiresTask('test-2',
provides=['a'], requires=[]) provides=['a'], requires=[])
wf.add(test_2) wf.add(test_2)
self.assertEqual(2, len(wf.graph)) self.assertEqual(2, len(wf))
def test_recache_on_add_no_deps(self): def test_recache_on_add_no_deps(self):
wf = gw.TargetedFlow("test") wf = gw.TargetedFlow("test")
@@ -188,11 +194,11 @@ class TargetedGraphFlowTest(test.TestCase):
provides=[], requires=[]) provides=[], requires=[])
wf.add(test_1) wf.add(test_1)
wf.set_target(test_1) wf.set_target(test_1)
self.assertEqual(1, len(wf.graph)) self.assertEqual(1, len(wf))
test_2 = utils.ProvidesRequiresTask('test-2', test_2 = utils.ProvidesRequiresTask('test-2',
provides=[], requires=[]) provides=[], requires=[])
wf.add(test_2) wf.add(test_2)
self.assertEqual(1, len(wf.graph)) self.assertEqual(1, len(wf))
def test_recache_on_link(self): def test_recache_on_link(self):
wf = gw.TargetedFlow("test") wf = gw.TargetedFlow("test")
@@ -202,7 +208,8 @@ class TargetedGraphFlowTest(test.TestCase):
provides=[], requires=[]) provides=[], requires=[])
wf.add(test_1, test_2) wf.add(test_1, test_2)
wf.set_target(test_1) wf.set_target(test_1)
self.assertEqual(1, len(wf.graph)) self.assertEqual(1, len(wf))
wf.link(test_2, test_1) wf.link(test_2, test_1)
self.assertEqual(2, len(wf.graph)) self.assertEqual(2, len(wf))
self.assertEqual([(test_2, test_1)], list(wf.graph.edges())) edges = [(src, dst) for src, dst, _meta in wf.iter_links()]
self.assertEqual([(test_2, test_1)], edges)

View File

@@ -21,15 +21,13 @@ import networkx as nx
from taskflow import exceptions from taskflow import exceptions
from taskflow import flow from taskflow import flow
from taskflow.patterns import graph_flow as gf
from taskflow.patterns import linear_flow as lf
from taskflow.patterns import unordered_flow as uf
from taskflow import retry from taskflow import retry
from taskflow import task from taskflow import task
from taskflow.utils import graph_utils as gu from taskflow.utils import graph_utils as gu
from taskflow.utils import lock_utils as lu from taskflow.utils import lock_utils as lu
from taskflow.utils import misc from taskflow.utils import misc
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
# Use the 'flatten' attribute as the need to add an edge here, which is useful # Use the 'flatten' attribute as the need to add an edge here, which is useful
@@ -78,12 +76,8 @@ class Flattener(object):
def _find_flattener(self, item): def _find_flattener(self, item):
"""Locates the flattening function to use to flatten the given item.""" """Locates the flattening function to use to flatten the given item."""
if isinstance(item, lf.Flow): if isinstance(item, flow.Flow):
return self._flatten_linear return self._flatten_flow
elif isinstance(item, uf.Flow):
return self._flatten_unordered
elif isinstance(item, gf.Flow):
return self._flatten_graph
elif isinstance(item, task.BaseTask): elif isinstance(item, task.BaseTask):
return self._flatten_task return self._flatten_task
elif isinstance(item, retry.Retry): elif isinstance(item, retry.Retry):
@@ -107,43 +101,13 @@ class Flattener(object):
if n != retry and 'retry' not in graph.node[n]: if n != retry and 'retry' not in graph.node[n]:
graph.add_node(n, {'retry': retry}) graph.add_node(n, {'retry': retry})
def _flatten_linear(self, flow):
"""Flattens a linear flow."""
graph = nx.DiGraph(name=flow.name)
previous_nodes = []
for item in flow:
subgraph = self._flatten(item)
graph = gu.merge_graphs([graph, subgraph])
# Find nodes that have no predecessor, make them have a predecessor
# of the previous nodes so that the linearity ordering is
# maintained. Find the ones with no successors and use this list
# to connect the next subgraph (if any).
self._add_new_edges(graph,
previous_nodes,
list(gu.get_no_predecessors(subgraph)))
# There should always be someone without successors, otherwise we
# have a cycle A -> B -> A situation, which should not be possible.
previous_nodes = list(gu.get_no_successors(subgraph))
return graph
def _flatten_unordered(self, flow):
"""Flattens a unordered flow."""
graph = nx.DiGraph(name=flow.name)
for item in flow:
# NOTE(harlowja): we do *not* connect the graphs together, this
# retains that each item (translated to subgraph) is disconnected
# from each other which will result in unordered execution while
# running.
graph = gu.merge_graphs([graph, self._flatten(item)])
return graph
def _flatten_task(self, task): def _flatten_task(self, task):
"""Flattens a individual task.""" """Flattens a individual task."""
graph = nx.DiGraph(name=task.name) graph = nx.DiGraph(name=task.name)
graph.add_node(task) graph.add_node(task)
return graph return graph
def _flatten_graph(self, flow): def _flatten_flow(self, flow):
"""Flattens a graph flow.""" """Flattens a graph flow."""
graph = nx.DiGraph(name=flow.name) graph = nx.DiGraph(name=flow.name)
# Flatten all nodes into a single subgraph per node. # Flatten all nodes into a single subgraph per node.
@@ -153,15 +117,15 @@ class Flattener(object):
subgraph_map[item] = subgraph subgraph_map[item] = subgraph
graph = gu.merge_graphs([graph, subgraph]) graph = gu.merge_graphs([graph, subgraph])
# Reconnect all node edges to there corresponding subgraphs. # Reconnect all node edges to there corresponding subgraphs.
for (u, v) in flow.graph.edges_iter(): for (u, v, u_v_attrs) in flow.iter_links():
# Retain and update the original edge attributes.
u_v_attrs = gu.get_edge_attrs(flow.graph, u, v)
# Connect the ones with no predecessors in v to the ones with no # Connect the ones with no predecessors in v to the ones with no
# successors in u (thus maintaining the edge dependency). # successors in u (thus maintaining the edge dependency).
self._add_new_edges(graph, self._add_new_edges(graph,
list(gu.get_no_successors(subgraph_map[u])), list(gu.get_no_successors(subgraph_map[u])),
list(gu.get_no_predecessors(subgraph_map[v])), list(gu.get_no_predecessors(subgraph_map[v])),
edge_attrs=u_v_attrs) edge_attrs=u_v_attrs)
if flow.retry is not None:
self._connect_retry(flow.retry, graph)
return graph return graph
def _pre_item_flatten(self, item): def _pre_item_flatten(self, item):
@@ -174,8 +138,6 @@ class Flattener(object):
def _post_item_flatten(self, item, graph): def _post_item_flatten(self, item, graph):
"""Called before a item is flattened; any post-flattening actions.""" """Called before a item is flattened; any post-flattening actions."""
if isinstance(item, flow.Flow) and item.retry:
self._connect_retry(item.retry, graph)
LOG.debug("Finished flattening '%s'", item) LOG.debug("Finished flattening '%s'", item)
# NOTE(harlowja): this one can be expensive to calculate (especially # NOTE(harlowja): this one can be expensive to calculate (especially
# the cycle detection), so only do it if we know debugging is enabled # the cycle detection), so only do it if we know debugging is enabled