Browse Source

Add disconnect_final client event, triggered after giving up on reconnections

pull/1236/head
Miguel Grinberg 2 years ago
parent
commit
ebca06eddd
Failed to extract signature
  1. 28
      docs/client.rst
  2. 17
      src/socketio/asyncio_client.py
  3. 20
      src/socketio/client.py
  4. 53
      tests/asyncio/test_asyncio_client.py
  5. 40
      tests/common/test_client.py

28
docs/client.rst

@ -87,12 +87,12 @@ Asyncio clients can also use a coroutine::
A catch-all event handler receives the event name as a first argument. The
remaining arguments are the same as for a regular event handler.
Connect, Connect Error and Disconnect Event Handlers
----------------------------------------------------
Connect, Connect Error, Disconnect and Disconnect Final Event Handlers
----------------------------------------------------------------------
The ``connect``, ``connect_error`` and ``disconnect`` events are special; they
are invoked automatically when a client connects or disconnects from the
server::
The ``connect``, ``connect_error``, ``disconnect`` and ``disconnect_final``
events are special; they are invoked automatically when a client connects or
disconnects from the server::
@sio.event
def connect():
@ -104,7 +104,11 @@ server::
@sio.event
def disconnect():
print("I'm disconnected!")
print("I'm disconnected (reconnection may be attempted)!")
@sio.event
def disconnect_final():
print("I'm disconnected (no more reconnection attempts will be made)!")
The ``connect_error`` handler is invoked when a connection attempt fails. If
the server provides arguments, these are passed on to the handler. The server
@ -116,10 +120,14 @@ server initiated disconnects, or accidental disconnects, for example due to
networking failures. In the case of an accidental disconnection, the client is
going to attempt to reconnect immediately after invoking the disconnect
handler. As soon as the connection is re-established the connect handler will
be invoked once again.
The ``connect``, ``connect_error`` and ``disconnect`` events have to be
defined explicitly and are not invoked on a catch-all event handler.
be invoked once again. When the client decides that no more reconnection
attempts will be issued, it invokes the ``disconnect_final`` handler. This can
happen when reconnections are disabled, or when the configured number of
attempts is reached without success.
The ``connect``, ``connect_error``, ``disconnect`` and ``disconnect_final``
events have to be defined explicitly and are not invoked on a catch-all event
handler.
Connecting to a Server
----------------------

17
src/socketio/asyncio_client.py

@ -143,9 +143,10 @@ class AsyncClient(client.Client):
transports=transports,
engineio_path=socketio_path)
except engineio.exceptions.ConnectionError as exc:
await self._trigger_event(
'connect_error', '/',
exc.args[1] if len(exc.args) > 1 else exc.args[0])
for n in self.connection_namespaces:
await self._trigger_event(
'connect_error', n,
exc.args[1] if len(exc.args) > 1 else exc.args[0])
raise exceptions.ConnectionError(exc.args[0]) from None
if wait:
@ -369,6 +370,7 @@ class AsyncClient(client.Client):
return
namespace = namespace or '/'
await self._trigger_event('disconnect', namespace=namespace)
await self._trigger_event('disconnect_final', namespace=namespace)
if namespace in self.namespaces:
del self.namespaces[namespace]
if not self.namespaces:
@ -469,6 +471,8 @@ class AsyncClient(client.Client):
try:
await asyncio.wait_for(self._reconnect_abort.wait(), delay)
self.logger.info('Reconnect task aborted')
for n in self.connection_namespaces:
await self._trigger_event('disconnect_final', namespace=n)
break
except (asyncio.TimeoutError, asyncio.CancelledError):
pass
@ -490,6 +494,8 @@ class AsyncClient(client.Client):
attempt_count >= self.reconnection_attempts:
self.logger.info(
'Maximum reconnection attempts reached, giving up')
for n in self.connection_namespaces:
await self._trigger_event('disconnect_final', namespace=n)
break
client.reconnecting_clients.remove(self)
@ -533,15 +539,18 @@ class AsyncClient(client.Client):
async def _handle_eio_disconnect(self):
"""Handle the Engine.IO disconnection event."""
self.logger.info('Engine.IO connection dropped')
will_reconnect = self.reconnection and self.eio.state == 'connected'
if self.connected:
for n in self.namespaces:
await self._trigger_event('disconnect', namespace=n)
if not will_reconnect:
await self._trigger_event('disconnect_final', namespace=n)
self.namespaces = {}
self.connected = False
self.callbacks = {}
self._binary_packet = None
self.sid = None
if self.eio.state == 'connected' and self.reconnection:
if will_reconnect:
self._reconnect_task = self.start_background_task(
self._handle_reconnect)

20
src/socketio/client.py

@ -92,7 +92,8 @@ class Client(object):
fatal errors are logged even when
``engineio_logger`` is ``False``.
"""
reserved_events = ['connect', 'connect_error', 'disconnect']
reserved_events = ['connect', 'connect_error', 'disconnect',
'disconnect_final']
def __init__(self, reconnection=True, reconnection_attempts=0,
reconnection_delay=1, reconnection_delay_max=5,
@ -332,9 +333,10 @@ class Client(object):
transports=transports,
engineio_path=socketio_path)
except engineio.exceptions.ConnectionError as exc:
self._trigger_event(
'connect_error', '/',
exc.args[1] if len(exc.args) > 1 else exc.args[0])
for n in self.connection_namespaces:
self._trigger_event(
'connect_error', n,
exc.args[1] if len(exc.args) > 1 else exc.args[0])
raise exceptions.ConnectionError(exc.args[0]) from None
if wait:
@ -569,6 +571,7 @@ class Client(object):
return
namespace = namespace or '/'
self._trigger_event('disconnect', namespace=namespace)
self._trigger_event('disconnect_final', namespace=namespace)
if namespace in self.namespaces:
del self.namespaces[namespace]
if not self.namespaces:
@ -654,6 +657,8 @@ class Client(object):
delay))
if self._reconnect_abort.wait(delay):
self.logger.info('Reconnect task aborted')
for n in self.connection_namespaces:
self._trigger_event('disconnect_final', namespace=n)
break
attempt_count += 1
try:
@ -673,6 +678,8 @@ class Client(object):
attempt_count >= self.reconnection_attempts:
self.logger.info(
'Maximum reconnection attempts reached, giving up')
for n in self.connection_namespaces:
self._trigger_event('disconnect_final', namespace=n)
break
reconnecting_clients.remove(self)
@ -716,15 +723,18 @@ class Client(object):
def _handle_eio_disconnect(self):
"""Handle the Engine.IO disconnection event."""
self.logger.info('Engine.IO connection dropped')
will_reconnect = self.reconnection and self.eio.state == 'connected'
if self.connected:
for n in self.namespaces:
self._trigger_event('disconnect', namespace=n)
if not will_reconnect:
self._trigger_event('disconnect_final', namespace=n)
self.namespaces = {}
self.connected = False
self.callbacks = {}
self._binary_packet = None
self.sid = None
if self.eio.state == 'connected' and self.reconnection:
if will_reconnect:
self._reconnect_task = self.start_background_task(
self._handle_reconnect)

53
tests/asyncio/test_asyncio_client.py

@ -631,12 +631,15 @@ class TestAsyncClient(unittest.TestCase):
c.connected = True
c._trigger_event = AsyncMock()
_run(c._handle_disconnect('/'))
c._trigger_event.mock.assert_called_once_with(
c._trigger_event.mock.assert_any_call(
'disconnect', namespace='/'
)
c._trigger_event.mock.assert_any_call(
'disconnect_final', namespace='/'
)
assert not c.connected
_run(c._handle_disconnect('/'))
assert c._trigger_event.mock.call_count == 1
assert c._trigger_event.mock.call_count == 2
def test_handle_disconnect_namespace(self):
c = asyncio_client.AsyncClient()
@ -644,11 +647,23 @@ class TestAsyncClient(unittest.TestCase):
c.namespaces = {'/foo': '1', '/bar': '2'}
c._trigger_event = AsyncMock()
_run(c._handle_disconnect('/foo'))
c._trigger_event.mock.assert_called_once_with(
c._trigger_event.mock.assert_any_call(
'disconnect', namespace='/foo'
)
c._trigger_event.mock.assert_any_call(
'disconnect_final', namespace='/foo'
)
assert c.namespaces == {'/bar': '2'}
assert c.connected
_run(c._handle_disconnect('/bar'))
c._trigger_event.mock.assert_any_call(
'disconnect', namespace='/bar'
)
c._trigger_event.mock.assert_any_call(
'disconnect_final', namespace='/bar'
)
assert c.namespaces == {}
assert not c.connected
def test_handle_disconnect_unknown_namespace(self):
c = asyncio_client.AsyncClient()
@ -656,9 +671,12 @@ class TestAsyncClient(unittest.TestCase):
c.namespaces = {'/foo': '1', '/bar': '2'}
c._trigger_event = AsyncMock()
_run(c._handle_disconnect('/baz'))
c._trigger_event.mock.assert_called_once_with(
c._trigger_event.mock.assert_any_call(
'disconnect', namespace='/baz'
)
c._trigger_event.mock.assert_any_call(
'disconnect_final', namespace='/baz'
)
assert c.namespaces == {'/foo': '1', '/bar': '2'}
assert c.connected
@ -668,7 +686,9 @@ class TestAsyncClient(unittest.TestCase):
c.namespaces = {'/foo': '1', '/bar': '2'}
c._trigger_event = AsyncMock()
_run(c._handle_disconnect('/'))
c._trigger_event.mock.assert_called_with('disconnect', namespace='/')
c._trigger_event.mock.assert_any_call('disconnect', namespace='/')
c._trigger_event.mock.assert_any_call('disconnect_final',
namespace='/')
assert c.namespaces == {'/foo': '1', '/bar': '2'}
assert c.connected
@ -929,7 +949,9 @@ class TestAsyncClient(unittest.TestCase):
@mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5])
def test_handle_reconnect_max_attempts(self, random, wait_for):
c = asyncio_client.AsyncClient(reconnection_attempts=2, logger=True)
c.connection_namespaces = ['/']
c._reconnect_task = 'foo'
c._trigger_event = AsyncMock()
c.connect = AsyncMock(
side_effect=[ValueError, exceptions.ConnectionError, None]
)
@ -940,6 +962,8 @@ class TestAsyncClient(unittest.TestCase):
1.5,
]
assert c._reconnect_task == 'foo'
c._trigger_event.mock.assert_called_once_with('disconnect_final',
namespace='/')
@mock.patch(
'asyncio.wait_for',
@ -949,7 +973,9 @@ class TestAsyncClient(unittest.TestCase):
@mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5])
def test_handle_reconnect_aborted(self, random, wait_for):
c = asyncio_client.AsyncClient(logger=True)
c.connection_namespaces = ['/']
c._reconnect_task = 'foo'
c._trigger_event = AsyncMock()
c.connect = AsyncMock(
side_effect=[ValueError, exceptions.ConnectionError, None]
)
@ -960,6 +986,8 @@ class TestAsyncClient(unittest.TestCase):
1.5,
]
assert c._reconnect_task == 'foo'
c._trigger_event.mock.assert_called_once_with('disconnect_final',
namespace='/')
def test_handle_eio_connect(self):
c = asyncio_client.AsyncClient()
@ -1057,10 +1085,11 @@ class TestAsyncClient(unittest.TestCase):
_run(c._handle_eio_message('9'))
def test_eio_disconnect(self):
c = asyncio_client.AsyncClient(reconnection=False)
c = asyncio_client.AsyncClient()
c.namespaces = {'/': '1'}
c.connected = True
c._trigger_event = AsyncMock()
c.start_background_task = mock.MagicMock()
c.sid = 'foo'
c.eio.state = 'connected'
_run(c._handle_eio_disconnect())
@ -1099,7 +1128,19 @@ class TestAsyncClient(unittest.TestCase):
def test_eio_disconnect_no_reconnect(self):
c = asyncio_client.AsyncClient(reconnection=False)
c.namespaces = {'/': '1'}
c.connected = True
c._trigger_event = AsyncMock()
c.start_background_task = mock.MagicMock()
c.sid = 'foo'
c.eio.state = 'connected'
_run(c._handle_eio_disconnect())
c._trigger_event.mock.assert_any_call(
'disconnect', namespace='/'
)
c._trigger_event.mock.assert_any_call(
'disconnect_final', namespace='/'
)
assert c.sid is None
assert not c.connected
c.start_background_task.assert_not_called()

40
tests/common/test_client.py

@ -752,10 +752,11 @@ class TestClient(unittest.TestCase):
c.connected = True
c._trigger_event = mock.MagicMock()
c._handle_disconnect('/')
c._trigger_event.assert_called_once_with('disconnect', namespace='/')
c._trigger_event.assert_any_call('disconnect', namespace='/')
c._trigger_event.assert_any_call('disconnect_final', namespace='/')
assert not c.connected
c._handle_disconnect('/')
assert c._trigger_event.call_count == 1
assert c._trigger_event.call_count == 2
def test_handle_disconnect_namespace(self):
c = client.Client()
@ -763,15 +764,21 @@ class TestClient(unittest.TestCase):
c.namespaces = {'/foo': '1', '/bar': '2'}
c._trigger_event = mock.MagicMock()
c._handle_disconnect('/foo')
c._trigger_event.assert_called_once_with(
c._trigger_event.assert_any_call(
'disconnect', namespace='/foo'
)
c._trigger_event.assert_any_call(
'disconnect_final', namespace='/foo'
)
assert c.namespaces == {'/bar': '2'}
assert c.connected
c._handle_disconnect('/bar')
c._trigger_event.assert_called_with(
c._trigger_event.assert_any_call(
'disconnect', namespace='/bar'
)
c._trigger_event.assert_any_call(
'disconnect_final', namespace='/bar'
)
assert c.namespaces == {}
assert not c.connected
@ -781,9 +788,12 @@ class TestClient(unittest.TestCase):
c.namespaces = {'/foo': '1', '/bar': '2'}
c._trigger_event = mock.MagicMock()
c._handle_disconnect('/baz')
c._trigger_event.assert_called_once_with(
c._trigger_event.assert_any_call(
'disconnect', namespace='/baz'
)
c._trigger_event.assert_any_call(
'disconnect_final', namespace='/baz'
)
assert c.namespaces == {'/foo': '1', '/bar': '2'}
assert c.connected
@ -793,7 +803,9 @@ class TestClient(unittest.TestCase):
c.namespaces = {'/foo': '1', '/bar': '2'}
c._trigger_event = mock.MagicMock()
c._handle_disconnect('/')
c._trigger_event.assert_called_with('disconnect', namespace='/')
print(c._trigger_event.call_args_list)
c._trigger_event.assert_any_call('disconnect', namespace='/')
c._trigger_event.assert_any_call('disconnect_final', namespace='/')
assert c.namespaces == {'/foo': '1', '/bar': '2'}
assert c.connected
@ -1023,9 +1035,11 @@ class TestClient(unittest.TestCase):
@mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5])
def test_handle_reconnect_max_attempts(self, random):
c = client.Client(reconnection_attempts=2)
c.connection_namespaces = ['/']
c._reconnect_task = 'foo'
c._reconnect_abort = c.eio.create_event()
c._reconnect_abort.wait = mock.MagicMock(return_value=False)
c._trigger_event = mock.MagicMock()
c.connect = mock.MagicMock(
side_effect=[ValueError, exceptions.ConnectionError, None]
)
@ -1036,13 +1050,17 @@ class TestClient(unittest.TestCase):
mock.call(1.5),
]
assert c._reconnect_task == 'foo'
c._trigger_event.assert_called_once_with('disconnect_final',
namespace='/')
@mock.patch('socketio.client.random.random', side_effect=[1, 0, 0.5])
def test_handle_reconnect_aborted(self, random):
c = client.Client()
c.connection_namespaces = ['/']
c._reconnect_task = 'foo'
c._reconnect_abort = c.eio.create_event()
c._reconnect_abort.wait = mock.MagicMock(side_effect=[False, True])
c._trigger_event = mock.MagicMock()
c.connect = mock.MagicMock(side_effect=exceptions.ConnectionError)
c._handle_reconnect()
assert c._reconnect_abort.wait.call_count == 2
@ -1051,6 +1069,8 @@ class TestClient(unittest.TestCase):
mock.call(1.5),
]
assert c._reconnect_task == 'foo'
c._trigger_event.assert_called_once_with('disconnect_final',
namespace='/')
def test_handle_eio_connect(self):
c = client.Client()
@ -1189,7 +1209,15 @@ class TestClient(unittest.TestCase):
def test_eio_disconnect_no_reconnect(self):
c = client.Client(reconnection=False)
c.namespaces = {'/': '1'}
c.connected = True
c._trigger_event = mock.MagicMock()
c.start_background_task = mock.MagicMock()
c.sid = 'foo'
c.eio.state = 'connected'
c._handle_eio_disconnect()
c._trigger_event.assert_any_call('disconnect', namespace='/')
c._trigger_event.assert_any_call('disconnect_final', namespace='/')
assert c.sid is None
assert not c.connected
c.start_background_task.assert_not_called()

Loading…
Cancel
Save