ApplicationRunner support to wait until Goodbye has gone out

We add asyncio and Twisted ApplicationRunner support to properly
wait until the Goodbye message has gone out. This makes the leave()
method return a Deferred that can be waited on, if you want, for
the underlying session to actually hit STATE_CLOSED
This commit is contained in:
meejah
2015-05-20 15:57:33 -06:00
parent 196520391f
commit c54d9c35a8
7 changed files with 46 additions and 10 deletions

View File

@@ -25,6 +25,7 @@
############################################################################### ###############################################################################
from __future__ import absolute_import from __future__ import absolute_import
import signal
from autobahn.wamp import protocol from autobahn.wamp import protocol
from autobahn.wamp.types import ComponentConfig from autobahn.wamp.types import ComponentConfig
@@ -158,8 +159,15 @@ class ApplicationRunner(object):
txaio.use_asyncio() txaio.use_asyncio()
txaio.config.loop = loop txaio.config.loop = loop
coro = loop.create_connection(transport_factory, host, port, ssl=ssl) coro = loop.create_connection(transport_factory, host, port, ssl=ssl)
loop.run_until_complete(coro) (transport, protocol) = loop.run_until_complete(coro)
loop.add_signal_handler(signal.SIGTERM, loop.stop)
# 4) now enter the asyncio event loop # 4) now enter the asyncio event loop
loop.run_forever() try:
loop.run_forever()
except KeyboardInterrupt:
# wait until we send Goodbye if user hit ctrl-c
# (done outside this except so SIGTERM gets the same handling)
pass
loop.run_until_complete(protocol._session.leave())
loop.close() loop.close()

View File

@@ -88,9 +88,9 @@ if os.environ.get('USE_TWISTED', False):
# shouldn't have actually connected to anything # shouldn't have actually connected to anything
# successfully, and the run() call shouldn't have inserted # successfully, and the run() call shouldn't have inserted
# any of its own call/errbacks. # any of its own call/errbacks. (except the cleanup handler)
self.assertFalse(d.called) self.assertFalse(d.called)
self.assertEqual(0, len(d.callbacks)) self.assertEqual(1, len(d.callbacks))
# neither reactor.run() NOR reactor.stop() should have been called # neither reactor.run() NOR reactor.stop() should have been called
# (just connectTCP() will have been called) # (just connectTCP() will have been called)

View File

@@ -207,6 +207,16 @@ class ApplicationRunner(object):
d = client.connect(transport_factory) d = client.connect(transport_factory)
# as the reactor shuts down, we wish to wait until we've sent
# out our "Goodbye" message; leave() returns a Deferred that
# fires when the transport gets to STATE_CLOSED
def cleanup(proto):
if hasattr(proto, '_session') and proto._session is not None:
return proto._session.leave()
# if we connect successfully, the arg is a WampWebSocketClientProtocol
d.addCallback(lambda proto: reactor.addSystemEventTrigger(
'before', 'shutdown', cleanup, proto))
# if the user didn't ask us to start the reactor, then they # if the user didn't ask us to start the reactor, then they
# get to deal with any connect errors themselves. # get to deal with any connect errors themselves.
if start_reactor: if start_reactor:

View File

@@ -303,6 +303,8 @@ class ISession(object):
:param message: An optional (human readable) closing message, intended for :param message: An optional (human readable) closing message, intended for
logging purposes. logging purposes.
:type message: str :type message: str
:return: may return a Future/Deferred that fires when we've disconnected
""" """
@abc.abstractmethod @abc.abstractmethod

View File

@@ -978,6 +978,8 @@ class ApplicationSession(BaseSession):
msg = wamp.message.Goodbye(reason=reason, message=log_message) msg = wamp.message.Goodbye(reason=reason, message=log_message)
self._transport.send(msg) self._transport.send(msg)
self._goodbye_sent = True self._goodbye_sent = True
# deferred that fires when transport actually hits CLOSED
return self._transport.is_closed
else: else:
raise SessionNotReady(u"Already requested to close the session") raise SessionNotReady(u"Already requested to close the session")

View File

@@ -117,7 +117,7 @@ else:
ApplicationRunner. ApplicationRunner.
''' '''
loop = Mock() loop = Mock()
loop.create_connection = Mock() loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop): with patch.object(asyncio, 'get_event_loop', return_value=loop):
ssl = {} ssl = {}
runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm', runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm',
@@ -132,7 +132,7 @@ else:
ApplicationRunner and the websocket URL starts with "ws:". ApplicationRunner and the websocket URL starts with "ws:".
''' '''
loop = Mock() loop = Mock()
loop.create_connection = Mock() loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop): with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm') runner = ApplicationRunner('ws://127.0.0.1:8080/ws', 'realm')
runner.run('_unused_') runner.run('_unused_')
@@ -145,7 +145,7 @@ else:
ApplicationRunner and the websocket URL starts with "wss:". ApplicationRunner and the websocket URL starts with "wss:".
''' '''
loop = Mock() loop = Mock()
loop.create_connection = Mock() loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop): with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('wss://127.0.0.1:8080/wss', 'realm') runner = ApplicationRunner('wss://127.0.0.1:8080/wss', 'realm')
runner.run('_unused_') runner.run('_unused_')
@@ -157,7 +157,7 @@ else:
but only a "ws:" URL. but only a "ws:" URL.
''' '''
loop = Mock() loop = Mock()
loop.create_connection = Mock() loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop): with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm', runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm',
ssl=True) ssl=True)
@@ -190,7 +190,7 @@ else:
context = ssl.create_default_context() context = ssl.create_default_context()
loop = Mock() loop = Mock()
loop.create_connection = Mock() loop.run_until_complete = Mock(return_value=(Mock(), Mock()))
with patch.object(asyncio, 'get_event_loop', return_value=loop): with patch.object(asyncio, 'get_event_loop', return_value=loop):
runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm', runner = ApplicationRunner('ws://127.0.0.1:8080/wss', 'realm',
ssl=context) ssl=context)

View File

@@ -660,6 +660,10 @@ class WebSocketProtocol(object):
Configuration attributes specific to clients. Configuration attributes specific to clients.
""" """
def __init__(self):
#: a Future/Deferred that fires when we hit STATE_CLOSED
self.is_closed = txaio.create_future()
def onOpen(self): def onOpen(self):
""" """
Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onOpen` Implements :func:`autobahn.websocket.interfaces.IWebSocketChannel.onOpen`
@@ -967,7 +971,12 @@ class WebSocketProtocol(object):
if self.debugCodePaths: if self.debugCodePaths:
self.factory._log("dropping connection") self.factory._log("dropping connection")
self.droppedByMe = True self.droppedByMe = True
# this code-path will be hit (*without* hitting
# _connectionLost) in some timeout scenarios (unit-tests
# cover these). However, sometimes we hit both.
self.state = WebSocketProtocol.STATE_CLOSED self.state = WebSocketProtocol.STATE_CLOSED
txaio.resolve(self.is_closed, self)
self._closeConnection(abort) self._closeConnection(abort)
else: else:
@@ -1218,7 +1227,12 @@ class WebSocketProtocol(object):
self.autoPingTimeoutCall.cancel() self.autoPingTimeoutCall.cancel()
self.autoPingTimeoutCall = None self.autoPingTimeoutCall = None
self.state = WebSocketProtocol.STATE_CLOSED # check required here because in some scenarios dropConnection
# will already have resolved the Future/Deferred.
if self.state != WebSocketProtocol.STATE_CLOSED:
self.state = WebSocketProtocol.STATE_CLOSED
txaio.resolve(self.is_closed, self)
if self.wasServingFlashSocketPolicyFile: if self.wasServingFlashSocketPolicyFile:
if self.debug: if self.debug:
self.factory._log("connection dropped after serving Flash Socket Policy File") self.factory._log("connection dropped after serving Flash Socket Policy File")