wip: refactoring context to share base.

This commit is contained in:
Romain Dorgueil
2018-07-11 17:09:53 +02:00
parent 4c2287ebf0
commit 8b3215ad25
4 changed files with 110 additions and 11 deletions

View File

@ -99,9 +99,6 @@ class Lifecycle:
self._stopped = True self._stopped = True
if self._stopped: # Stopping twice has no effect
return
def kill(self): def kill(self):
if not self.started: if not self.started:
raise RuntimeError('Cannot kill an unstarted context.') raise RuntimeError('Cannot kill an unstarted context.')
@ -136,3 +133,12 @@ class BaseContext(Lifecycle, Wrapper):
Lifecycle.__init__(self) Lifecycle.__init__(self)
Wrapper.__init__(self, wrapped) Wrapper.__init__(self, wrapped)
self.parent = parent self.parent = parent
@property
def xstatus(self):
"""
UNIX-like exit status, only coherent if the context has stopped.
"""
if self._defunct:
return 70
return 0

View File

@ -4,12 +4,17 @@ from time import sleep
from bonobo.config import create_container from bonobo.config import create_container
from bonobo.constants import BEGIN, END from bonobo.constants import BEGIN, END
from bonobo.execution import events from bonobo.execution import events
from bonobo.execution.contexts.base import BaseContext
from bonobo.execution.contexts.node import NodeExecutionContext from bonobo.execution.contexts.node import NodeExecutionContext
from bonobo.execution.contexts.plugin import PluginExecutionContext from bonobo.execution.contexts.plugin import PluginExecutionContext
from whistle import EventDispatcher from whistle import EventDispatcher
class GraphExecutionContext: class GraphExecutionContext(BaseContext):
"""
Stores the actual state of a graph execution, and manages its lifecycle.
"""
NodeExecutionContextType = NodeExecutionContext NodeExecutionContextType = NodeExecutionContext
PluginExecutionContextType = PluginExecutionContext PluginExecutionContextType = PluginExecutionContext
@ -17,17 +22,24 @@ class GraphExecutionContext:
@property @property
def started(self): def started(self):
if not len(self.nodes):
return super(GraphExecutionContext, self).started
return any(node.started for node in self.nodes) return any(node.started for node in self.nodes)
@property @property
def stopped(self): def stopped(self):
if not len(self.nodes):
return super(GraphExecutionContext, self).stopped
return all(node.started and node.stopped for node in self.nodes) return all(node.started and node.stopped for node in self.nodes)
@property @property
def alive(self): def alive(self):
if not len(self.nodes):
return super(GraphExecutionContext, self).alive
return any(node.alive for node in self.nodes) return any(node.alive for node in self.nodes)
def __init__(self, graph, plugins=None, services=None, dispatcher=None): def __init__(self, graph, *, plugins=None, services=None, dispatcher=None):
super(GraphExecutionContext, self).__init__(graph)
self.dispatcher = dispatcher or EventDispatcher() self.dispatcher = dispatcher or EventDispatcher()
self.graph = graph self.graph = graph
self.nodes = [self.create_node_execution_context_for(node) for node in self.graph] self.nodes = [self.create_node_execution_context_for(node) for node in self.graph]
@ -74,6 +86,8 @@ class GraphExecutionContext:
self.dispatcher.dispatch(name, events.ExecutionEvent(self)) self.dispatcher.dispatch(name, events.ExecutionEvent(self))
def start(self, starter=None): def start(self, starter=None):
super(GraphExecutionContext, self).start()
self.register_plugins() self.register_plugins()
self.dispatch(events.START) self.dispatch(events.START)
self.tick(pause=False) self.tick(pause=False)
@ -89,13 +103,16 @@ class GraphExecutionContext:
if pause: if pause:
sleep(self.TICK_PERIOD) sleep(self.TICK_PERIOD)
def kill(self): def loop(self):
self.dispatch(events.KILL) while self.should_loop:
for node_context in self.nodes: self.tick()
node_context.kill() for node in self.nodes:
self.tick() if node.should_loop:
node.step()
def stop(self, stopper=None): def stop(self, stopper=None):
super(GraphExecutionContext, self).stop()
self.dispatch(events.STOP) self.dispatch(events.STOP)
for node_context in self.nodes: for node_context in self.nodes:
if stopper is None: if stopper is None:
@ -106,6 +123,14 @@ class GraphExecutionContext:
self.dispatch(events.STOPPED) self.dispatch(events.STOPPED)
self.unregister_plugins() self.unregister_plugins()
def kill(self):
super(GraphExecutionContext, self).kill()
self.dispatch(events.KILL)
for node_context in self.nodes:
node_context.kill()
self.tick()
def register_plugins(self): def register_plugins(self):
for plugin_context in self.plugins: for plugin_context in self.plugins:
plugin_context.register() plugin_context.register()

View File

@ -21,6 +21,15 @@ UnboundArguments = namedtuple('UnboundArguments', ['args', 'kwargs'])
class NodeExecutionContext(BaseContext, WithStatistics): class NodeExecutionContext(BaseContext, WithStatistics):
"""
Stores the actual context of a node, within a given graph execution, accessed as `self.parent`.
A special case exist, mostly for testing purpose, where there is no parent context.
Can be used as a context manager, also very convenient for testing nodes that requires some external context (like
a service implementation, or a value holder).
"""
def __init__(self, wrapped, *, parent=None, services=None, _input=None, _outputs=None): def __init__(self, wrapped, *, parent=None, services=None, _input=None, _outputs=None):
""" """
Node execution context has the responsibility fo storing the state of a transformation during its execution. Node execution context has the responsibility fo storing the state of a transformation during its execution.
@ -170,7 +179,7 @@ class NodeExecutionContext(BaseContext, WithStatistics):
if self._stack: if self._stack:
try: try:
self._stack.teardown() self._stack.teardown()
except: except Exception as exc:
self.fatal(sys.exc_info()) self.fatal(sys.exc_info())
super().stop() super().stop()

View File

@ -0,0 +1,59 @@
from bonobo import Graph
from bonobo.execution.contexts import GraphExecutionContext
def raise_an_error(*args, **kwargs):
raise Exception('Careful, man, there\'s a beverage here!')
def raise_an_unrecoverrable_error(*args, **kwargs):
raise Exception('You are entering a world of pain!')
def test_lifecycle_of_empty_graph():
graph = Graph()
with GraphExecutionContext(graph) as context:
assert context.started
assert context.alive
assert not context.stopped
assert context.started
assert not context.alive
assert context.stopped
assert not context.xstatus
def test_lifecycle_of_nonempty_graph():
graph = Graph([1, 2, 3], print)
with GraphExecutionContext(graph) as context:
assert context.started
assert context.alive
assert not context.stopped
assert context.started
assert not context.alive
assert context.stopped
assert not context.xstatus
def test_lifecycle_of_graph_with_recoverable_error():
graph = Graph([1, 2, 3], raise_an_error, print)
with GraphExecutionContext(graph) as context:
assert context.started
assert context.alive
assert not context.stopped
assert context.started
assert not context.alive
assert context.stopped
assert not context.xstatus
def test_lifecycle_of_graph_with_unrecoverable_error():
graph = Graph([1, 2, 3], raise_an_unrecoverrable_error, print)
with GraphExecutionContext(graph) as context:
assert context.started
assert context.alive
assert not context.stopped
context.loop()
assert context.started
assert not context.alive
assert context.stopped
assert not context.xstatus