add global exception handling

Support global exception handling to uasyncio according to CPython error handling: https://docs.python.org/3/library/asyncio-eventloop.html#error-handling-api

Adds the Loop methods (also changing all loop methods to be staticmethos or classmethods since there is only one loop) and a default exception handler. 

This is especially interesting since this new version of uasyncio doesn't throw exceptions to the caller of "loop.run_forever()" and therefore exceptions in other tasks are only printed to repl, where the user might never see it since the device will be deployed without logging the repl output. 
With a global exception handling a user can catch those exceptions and send them by mqtt or log them to a file on the device. 

The implementation preallocates a context dictionary so in case of an exception there shouldn't be any RAM allocations.

The used approach is compatible to CPython except for 2 problems:
1) There is no way to log the Exception traceback because "sys.print_exception" only prints the traceback to the repl. So there is no way to actually log the traceback, which would be very helpful. Hopefully this can be implemented. I understand that this might cause RAM allocation but a user might decide to use it anyway in a custom exception handler because it makes debugging a lot easier if you know in what file and line the error occured.
2) In CPython the exception handler is called once the task is finished which created the task that threw an uncaught exception, whereas in UPy the exception handler is called immediately when the exception is thrown. This makes a difference in the following testcase but is generally just a minor difference that shouldn't cause any abnormal behaviour.

```
async def test_catch_uncaught_exception():
    # can't work with a local return value because the exception handler runs after
    # the current coroutine is finished in CPython. Works in UPy though.
    res = False

    async def fail():
        raise TypeError("uncaught exception")

    def handle_exception(loop, context):
        # context["message"] will always be there; but context["exception"] may not
        print(context)
        print(context["message"])
        print(context["exception"])
        msg = context.get("exception", context["message"])
        if mp:
            print("Caught: {}{}".format(type(context["exception"]), msg))
        else:
            print("Caught: {}".format({msg}))
        nonlocal res
        print("res is", res)
        res = True
        print("done")

    t = asyncio.create_task(fail())
    loop = asyncio.get_event_loop()
    loop.set_exception_handler(handle_exception)
    await asyncio.sleep(0.1)
    await asyncio.sleep(0.1)
    print("coro done")
    return res
```


I'd be glad to discuss this PR.
pull/13/head
Kevin Köck 2020-01-04 16:45:08 +01:00 zatwierdzone przez GitHub
rodzic 4d2a52d109
commit b4395cbe27
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
1 zmienionych plików z 40 dodań i 7 usunięć

Wyświetl plik

@ -7,6 +7,7 @@ from time import ticks_ms as ticks, ticks_diff, ticks_add
import sys, select
type_genf = type((lambda: (yield))) # Type of a generator function upy iss #3241
_exc_message = 'Task exception was never retrieved'
################################################################################
# Primitive class embodies methods common to most synchronisation primitives
@ -390,6 +391,20 @@ async def start_server(cb, host, port, backlog=5):
################################################################################
# Main run loop
_context = {"message": _exc_message,
"exception": None,
"future": None}
def _exc_handler(loop, context):
print(context["message"])
print("future:",context["future"],"coro=",context["future"].coro)
# missing traceback
sys.print_exception(context["exception"])
# set default exception handler
exc_handler = _exc_handler
# Queue of Task instances
_queue = TQueue()
@ -443,11 +458,12 @@ def run_until_complete(main_task=None):
waiting = True
t.waiting = None # Free waiting queue head
_io_queue.remove(t) # Remove task from the IO queue (if it's on it)
t.coro = None # Indicate task is done
# Print out exception for detached tasks
if not waiting and not isinstance(er, excs_stop):
print('task raised exception:', t.coro)
sys.print_exception(er)
_context["exception"] = er
_context["future"] = t
Loop.call_exception_handler(_context)
t.coro = None # Indicate task is done
StreamReader = Stream
StreamWriter = Stream # CPython 3.8 compatibility
@ -469,17 +485,34 @@ Stream.awrite = stream_awrite
Stream.awritestr = stream_awrite # TODO explicitly convert to bytes?
class Loop:
def create_task(self, coro):
@staticmethod
def create_task(coro):
return create_task(coro)
def run_forever(self):
@staticmethod
def run_forever():
run_until_complete()
# TODO should keep running until .stop() is called, even if there're no tasks left
def run_until_complete(self, aw):
@staticmethod
def run_until_complete(aw):
return run_until_complete(_promote_to_task(aw))
@staticmethod
def close(self):
pass
@staticmethod
def set_exception_handler(handler):
global exc_handler
exc_handler = handler
@staticmethod
def get_exception_handler():
return exc_handler
@classmethod
def default_exception_handler(cls,context):
exc_handler(cls,context)
@classmethod
def call_exception_handler(cls, context):
exc_handler(cls,context)
def get_event_loop(runq_len=0, waitq_len=0):
return Loop()
version = (3, 0, 1)
version = (3, 0, 2)