issue #186: initial version of subtree detachment.

pull/244/head
David Wilson 6 years ago
parent 8bd34e1e28
commit 7f1060f54a

@ -445,6 +445,20 @@ also listen on the following handles:
In this way, the master need never re-send a module it has already sent to In this way, the master need never re-send a module it has already sent to
a direct descendant. a direct descendant.
.. currentmodule:: mitogen.core
.. data:: DETACHING
Sent to inform a parent that user code has invoked
:meth:`ExternalContext.detach` to decouple the lifecycle of a directly
connected context and its subtree from the running program.
A child usually shuts down immediately if it loses its parent connection,
and parents usually terminate any related Python/SSH subprocess on
disconnection. Receiving :data:`DETACHING` informs the parent the
connection will soon drop, but the process intends to continue life
independently, and to avoid terminating the related subprocess if that
subprocess is the child itself.
Additional handles are created to receive the result of every function call Additional handles are created to receive the result of every function call
triggered by :py:meth:`call_async() <mitogen.parent.Context.call_async>`. triggered by :py:meth:`call_async() <mitogen.parent.Context.call_async>`.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

@ -237,6 +237,17 @@ uptime')** without further need to capture or manage output.
18:17:56 I mitogen.ctx.k3: stdout: 17:37:10 up 562 days, 2:25, 5 users, load average: 1.24, 1.13, 1.14 18:17:56 I mitogen.ctx.k3: stdout: 17:37:10 up 562 days, 2:25, 5 users, load average: 1.24, 1.13, 1.14
Detached Subtrees
#################
.. image:: images/detached-subtree.png
It is possible to dynamically construct and decouple individual contexts from
the lifecycle of the running program without terminating them, while enabling
communication with any descendents in the subtree to be maintained. This is
intended to support implementing background tasks.
Blocking Code Friendly Blocking Code Friendly
###################### ######################

@ -75,6 +75,7 @@ DEL_ROUTE = 104
ALLOCATE_ID = 105 ALLOCATE_ID = 105
SHUTDOWN = 106 SHUTDOWN = 106
LOAD_MODULE = 107 LOAD_MODULE = 107
DETACHING = 108
IS_DEAD = 999 IS_DEAD = 999
PY3 = sys.version_info > (3,) PY3 = sys.version_info > (3,)
@ -953,6 +954,7 @@ class Context(object):
raise SystemError('Cannot making blocking call on broker thread') raise SystemError('Cannot making blocking call on broker thread')
receiver = Receiver(self.router, persist=persist, respondent=self) receiver = Receiver(self.router, persist=persist, respondent=self)
msg.dst_id = self.context_id
msg.reply_to = receiver.handle msg.reply_to = receiver.handle
_v and LOG.debug('%r.send_async(%r)', self, msg) _v and LOG.debug('%r.send_async(%r)', self, msg)
@ -1277,6 +1279,10 @@ class Router(object):
self.broker.start_receive(stream) self.broker.start_receive(stream)
listen(stream, 'disconnect', lambda: self.on_stream_disconnect(stream)) listen(stream, 'disconnect', lambda: self.on_stream_disconnect(stream))
def stream_by_id(self, dst_id):
return self._stream_by_id.get(dst_id,
self._stream_by_id.get(mitogen.parent_id))
def del_handler(self, handle): def del_handler(self, handle):
del self._handle_map[handle] del self._handle_map[handle]
@ -1501,6 +1507,8 @@ class Broker(object):
class ExternalContext(object): class ExternalContext(object):
detached = False
def _on_broker_shutdown(self): def _on_broker_shutdown(self):
self.channel.close() self.channel.close()
@ -1514,8 +1522,34 @@ class ExternalContext(object):
self.broker.shutdown() self.broker.shutdown()
def _on_parent_disconnect(self): def _on_parent_disconnect(self):
_v and LOG.debug('%r: parent stream is gone, dying.', self) if self.detached:
self.broker.shutdown() mitogen.parent_ids = []
mitogen.parent_id = None
LOG.info('Detachment complete')
else:
_v and LOG.debug('%r: parent stream is gone, dying.', self)
self.broker.shutdown()
def _sync(self, func):
latch = Latch()
self.broker.defer(lambda: latch.put(func()))
return latch.get()
def detach(self):
self.detached = True
stream = self.router.stream_by_id(mitogen.parent_id)
if stream: # not double-detach()'d
os.setsid()
self.parent.send_await(Message(handle=DETACHING))
LOG.info('Detaching from %r; parent is %s', stream, self.parent)
for x in range(20):
pending = self._sync(lambda: stream.pending_bytes())
if not pending:
break
time.sleep(0.05)
if pending:
LOG.error('Stream had %d bytes after 2000ms', pending)
self.broker.defer(stream.on_disconnect, self.broker)
def _setup_master(self, max_message_size, profiling, parent_id, def _setup_master(self, max_message_size, profiling, parent_id,
context_id, in_fd, out_fd): context_id, in_fd, out_fd):

@ -36,6 +36,8 @@ LOG = logging.getLogger(__name__)
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
container = None container = None
image = None image = None
username = None username = None

@ -81,6 +81,8 @@ def handle_child_crash():
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = True
#: Reference to the importer, if any, recovered from the parent. #: Reference to the importer, if any, recovered from the parent.
importer = None importer = None

@ -36,6 +36,7 @@ LOG = logging.getLogger(__name__)
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
create_child_args = { create_child_args = {
'merge_stdio': True 'merge_stdio': True
} }

@ -36,6 +36,7 @@ LOG = logging.getLogger(__name__)
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
create_child_args = { create_child_args = {
# If lxc-attach finds any of stdin, stdout, stderr connected to a TTY, # If lxc-attach finds any of stdin, stdout, stderr connected to a TTY,
# to prevent input injection it creates a proxy pty, forcing all IO to # to prevent input injection it creates a proxy pty, forcing all IO to

@ -690,6 +690,11 @@ class Router(mitogen.parent.Router):
self.responder = ModuleResponder(self) self.responder = ModuleResponder(self)
self.log_forwarder = LogForwarder(self) self.log_forwarder = LogForwarder(self)
self.route_monitor = mitogen.parent.RouteMonitor(router=self) self.route_monitor = mitogen.parent.RouteMonitor(router=self)
self.add_handler( # TODO: cutpaste.
fn=self._on_detaching,
handle=mitogen.core.DETACHING,
persist=True,
)
def enable_debug(self): def enable_debug(self):
mitogen.core.enable_debug_logging() mitogen.core.enable_debug_logging()

@ -599,12 +599,23 @@ class Stream(mitogen.core.Stream):
) )
) )
#: If :data:`True`, indicates the subprocess managed by us should not be
#: killed during graceful detachment, as it the actual process implementing
#: the child context. In all other cases, the subprocess is SSH, sudo, or a
#: similar tool that should be reminded to quit during disconnection.
child_is_immediate_subprocess = True
detached = False
_reaped = False _reaped = False
def _reap_child(self): def _reap_child(self):
""" """
Reap the child process during disconnection. Reap the child process during disconnection.
""" """
if self.detached and self.child_is_immediate_subprocess:
LOG.debug('%r: immediate child is detached, won\'t reap it', self)
return
if self._reaped: if self._reaped:
# on_disconnect() may be invoked more than once, for example, if # on_disconnect() may be invoked more than once, for example, if
# there is still a pending message to be sent after the first # there is still a pending message to be sent after the first
@ -929,10 +940,22 @@ class Router(mitogen.core.Router):
importer=importer, importer=importer,
) )
self.route_monitor = RouteMonitor(self, parent) self.route_monitor = RouteMonitor(self, parent)
self.add_handler(
fn=self._on_detaching,
handle=mitogen.core.DETACHING,
persist=True,
)
def stream_by_id(self, dst_id): def _on_detaching(self, msg):
return self._stream_by_id.get(dst_id, if msg.is_dead:
self._stream_by_id.get(mitogen.parent_id)) return
stream = self.stream_by_id(msg.src_id)
if stream.remote_id != msg.src_id or stream.detached:
LOG.warning('bad DETACHING received on %r: %r', stream, msg)
return
LOG.debug('%r: marking as detached', stream)
stream.detached = True
msg.reply(None)
def add_route(self, target_id, stream): def add_route(self, target_id, stream):
LOG.debug('%r.add_route(%r, %r)', self, target_id, stream) LOG.debug('%r.add_route(%r, %r)', self, target_id, stream)

@ -105,6 +105,8 @@ def get_machinectl_pid(path, name):
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
child_is_immediate_subprocess = False
container = None container = None
username = None username = None
kind = None kind = None

@ -59,6 +59,7 @@ class HostKeyError(mitogen.core.StreamError):
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
child_is_immediate_subprocess = False
python_path = 'python2.7' python_path = 'python2.7'
#: Once connected, points to the corresponding TtyLogStream, allowing it to #: Once connected, points to the corresponding TtyLogStream, allowing it to

@ -46,6 +46,7 @@ class Stream(mitogen.parent.Stream):
# for hybrid_tty_create_child(), there just needs to be either a shell # for hybrid_tty_create_child(), there just needs to be either a shell
# snippet or bootstrap support for fixing things up afterwards. # snippet or bootstrap support for fixing things up afterwards.
create_child = staticmethod(mitogen.parent.tty_create_child) create_child = staticmethod(mitogen.parent.tty_create_child)
child_is_immediate_subprocess = False
#: Once connected, points to the corresponding TtyLogStream, allowing it to #: Once connected, points to the corresponding TtyLogStream, allowing it to
#: be disconnected at the same time this stream is being torn down. #: be disconnected at the same time this stream is being torn down.

@ -104,6 +104,7 @@ class PasswordError(mitogen.core.StreamError):
class Stream(mitogen.parent.Stream): class Stream(mitogen.parent.Stream):
create_child = staticmethod(mitogen.parent.hybrid_tty_create_child) create_child = staticmethod(mitogen.parent.hybrid_tty_create_child)
child_is_immediate_subprocess = False
#: Once connected, points to the corresponding TtyLogStream, allowing it to #: Once connected, points to the corresponding TtyLogStream, allowing it to
#: be disconnected at the same time this stream is being torn down. #: be disconnected at the same time this stream is being torn down.

Loading…
Cancel
Save