diff --git a/README.md b/README.md index dc02bae6..1e067a6a 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ ===================================== generator=datazen version=3.1.2 - hash=cde638b7567222966c038f6c0d5719e0 + hash=3bc7656b5df806d21148ec2b5b2c0082 ===================================== --> -# runtimepy ([1.5.3](https://pypi.org/project/runtimepy/)) +# runtimepy ([1.5.4](https://pypi.org/project/runtimepy/)) [![python](https://img.shields.io/pypi/pyversions/runtimepy.svg)](https://pypi.org/project/runtimepy/) ![Build Status](https://github.com/vkottler/runtimepy/workflows/Python%20Package/badge.svg) diff --git a/local/variables/package.yaml b/local/variables/package.yaml index fba43cfe..edaad82c 100644 --- a/local/variables/package.yaml +++ b/local/variables/package.yaml @@ -1,5 +1,5 @@ --- major: 1 minor: 5 -patch: 3 +patch: 4 entry: runtimepy diff --git a/pyproject.toml b/pyproject.toml index 73d905e1..0dc21004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta:__legacy__" [project] name = "runtimepy" -version = "1.5.3" +version = "1.5.4" description = "A framework for implementing Python services." readme = "README.md" requires-python = ">=3.7" diff --git a/runtimepy/__init__.py b/runtimepy/__init__.py index 04600dcb..2d6b5be3 100644 --- a/runtimepy/__init__.py +++ b/runtimepy/__init__.py @@ -1,7 +1,7 @@ # ===================================== # generator=datazen # version=3.1.2 -# hash=407407f958d18e14776eb5ad2afb0772 +# hash=d02bf554f7d30131decd07af8dbad01f # ===================================== """ @@ -10,4 +10,4 @@ DESCRIPTION = "A framework for implementing Python services." PKG_NAME = "runtimepy" -VERSION = "1.5.3" +VERSION = "1.5.4" diff --git a/runtimepy/net/udp/connection.py b/runtimepy/net/udp/connection.py index 8473073b..a4524b98 100644 --- a/runtimepy/net/udp/connection.py +++ b/runtimepy/net/udp/connection.py @@ -99,15 +99,24 @@ def sendto( self, data: bytes, addr: _Union[IpHost, _Tuple[str, int]] = None ) -> None: """Send to a specific address.""" - self._transport.sendto(data, addr=addr) + + try: + self._transport.sendto(data, addr=addr) + + # Catch a bug in the underlying event loop implementation - we try to + # send, but the underlying socket is gone (e.g. attribute is 'None'). + # This seems to be possible (but intermittent) when shutting down the + # application. + except AttributeError as exc: + self.disable(str(exc)) def send_text(self, data: str) -> None: """Enqueue a text message to send.""" - self._transport.sendto(data.encode(), addr=self.remote_address) + self.sendto(data.encode(), addr=self.remote_address) def send_binary(self, data: _BinaryMessage) -> None: """Enqueue a binary message tos end.""" - self._transport.sendto(data, addr=self.remote_address) + self.sendto(data, addr=self.remote_address) @classmethod async def create_connection( diff --git a/tests/net/udp/test_sendto_exceptions.py b/tests/net/udp/test_sendto_exceptions.py new file mode 100644 index 00000000..2934dded --- /dev/null +++ b/tests/net/udp/test_sendto_exceptions.py @@ -0,0 +1,65 @@ +""" +Test special cases for the 'net.udp.connection' module. +""" + +# built-in +from asyncio import Event, gather, sleep + +# third-party +from pytest import mark + +# module under test +from runtimepy.net.udp.connection import NullUdpConnection + + +@mark.asyncio +async def test_udp_connection_sendto_fail(): + """Test 'sendto' failures with a UDP connection.""" + + conn1, conn2 = await NullUdpConnection.create_pair() + + iterations = 0 + sleep_time = 0.05 + + signal = Event() + + async def send_spam() -> None: + """Send messages back and forth.""" + + nonlocal iterations + msg = "Hello, world!" + + while not conn1.disabled and not conn2.disabled: + conn1.send_text(msg) + conn2.send_text(msg) + iterations += 1 + await sleep(sleep_time) + + for _ in range(10): + conn1.send_text(msg) + conn2.send_text(msg) + iterations += 1 + await sleep(sleep_time) + + await signal.wait() + + # Send more (should cause problems). + for _ in range(10): + conn1.send_text(msg) + conn2.send_text(msg) + + async def closer() -> None: + """Close the two connections.""" + + while iterations < 5: + await sleep(sleep_time) + conn1.disable("nominal") + + while iterations < 10: + await sleep(sleep_time) + conn2.disable("nominal") + + signal.set() + + # Run everything. + await gather(send_spam(), closer(), conn1.process(), conn2.process())