Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion Lib/asyncio/base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
from .log import logger


__all__ = 'BaseEventLoop','Server',
__all__ = 'BaseEventLoop','Server','crash_exception_handler'


# Minimum number of _scheduled timer handles before cleanup of
Expand Down Expand Up @@ -414,6 +414,21 @@ async def wait_closed(self):
await waiter


def crash_exception_handler(self, context):
exception = context.get("exception")
if exception is None:
message = context.get("message")
if message is None:
message = "Unhandled exception in event loop"
exception = RuntimeError(message)

if events._get_running_loop() is self:
self.crash(exception)
return

self.call_soon_threadsafe(self.crash, exception)


class BaseEventLoop(events.AbstractEventLoop):

def __init__(self):
Expand Down Expand Up @@ -447,6 +462,7 @@ def __init__(self):
self._asyncgens_shutdown_called = False
# Set to True when `loop.shutdown_default_executor` is called.
self._executor_shutdown_called = False
self._exceptions = []

def __repr__(self):
return (
Expand Down Expand Up @@ -682,6 +698,14 @@ def run_forever(self):
while True:
self._run_once()
if self._stopping:
if self._exceptions:
try:
raise BaseExceptionGroup(
"errors occured in asyncio callbacks",
self._exceptions,
)
finally:
self._exceptions = []
break
finally:
self._run_forever_cleanup()
Expand Down Expand Up @@ -724,6 +748,10 @@ def run_until_complete(self, future):

return future.result()

def crash(self, exception):
self._exceptions.append(exception)
self.stop()

def stop(self):
"""Stop running the event loop.

Expand Down
3 changes: 3 additions & 0 deletions Lib/asyncio/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,9 @@ def get_debug(self):
def set_debug(self, enabled):
raise NotImplementedError

def crash(self, exception):
raise NotImplementedError


class _AbstractEventLoopPolicy:
"""Abstract policy for accessing the event loop."""
Expand Down
37 changes: 37 additions & 0 deletions Lib/test/test_asyncio/test_base_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,43 @@ def handler(loop, context):
'Unhandled error in exception handler'),
exc_info=(AttributeError, MOCK_ANY, MOCK_ANY))

def test_set_exception_handler_crash_handler(self):
self.loop._process_events = mock.Mock()
self.loop.set_exception_handler(asyncio.crash_exception_handler)

def crash(e):
raise e

self.loop.call_soon(crash, RuntimeError("hello"))
self.loop.call_soon(crash, ValueError("world"))

with self.assertRaises(ExceptionGroup) as exc_info:
self.loop.run_forever()

self.assertIsInstance(exc_info.exception.exceptions[0], RuntimeError)
self.assertIsInstance(exc_info.exception.exceptions[1], ValueError)

def test_set_exception_handler_crash_handler_be(self):
self.loop._process_events = mock.Mock()
self.loop.set_exception_handler(asyncio.crash_exception_handler)

class MyBaseException(BaseException):
pass

def crash(e):
raise e

self.loop.call_soon(crash, RuntimeError("hello"))
self.loop.call_soon(crash, ValueError("world"))
self.loop.call_soon(crash, MyBaseException("mbe"))

with self.assertRaises(BaseExceptionGroup) as exc_info:
self.loop.run_forever()

self.assertIsInstance(exc_info.exception.exceptions[0], RuntimeError)
self.assertIsInstance(exc_info.exception.exceptions[1], ValueError)
self.assertIsInstance(exc_info.exception.exceptions[2], MyBaseException)

def test_default_exc_handler_broken(self):
_context = None

Expand Down
Loading