Skip to content

cancellation error in extmod/uasyncio #5820

Closed

Description

@nazsolti

Hi!

I was used to using the uasyncio implementation from micropython-lib. I saw you're moving to an implementation within extmod, so I thought I start using that instead. My issue is I don't know the proper way to cancel a task.

If I call cancel() on a uasyncio.Task instance, I get the following error:

RuntimeError: cannot cancel self

Thanks for the help!

Zsolt

Activity

  1. changed the title [-]cancellation in extmod/uasyncio[/-] [+]cancellation error in extmod/uasyncio[/+] on Mar 27, 2020
  2. tve commented on Mar 27, 2020

    @tve
    Contributor

    The error says that a task cannot cancel itself.
    If you think that that's not what your code is attempting, please post a test case.

  3. nazsolti commented on Mar 27, 2020

    @nazsolti
    Author

    I'm not trying to cancel a task from within itself, rather I'm calling task.cancel()

    The code is:

    import uasyncio
    
    _loop = uasyncio.get_event_loop()
    _tasks = {}
    
    def create_task(name, coro, *args):
        if str(name) not in _tasks:
            _tasks[name] = _loop.create_task(coro(*args))
            return True
        return False
    
    
    def destroy_task(name):
        if str(name) in _tasks:
            _tasks[name].cancel()  # this is the line raising the error
            del(_tasks[name])
            return True
        return False

    Assume there's a task created with create_task('mytask', mycoro()) and I'm calling destroy_task('mytask') when the error is raised.

  4. tve commented on Mar 27, 2020

    @tve
    Contributor

    In the future please post complete test cases that a reviewer can run!

    Here's my test case:

    import uasyncio
    
    _loop = uasyncio.get_event_loop()
    _tasks = {}
    
    def create_task(name, coro, *args):
        if str(name) not in _tasks:
            _tasks[name] = _loop.create_task(coro(*args))
            return True
        return False
    
    
    def destroy_task(name):
        if str(name) in _tasks:
            _tasks[name].cancel()  # this is the line raising the error
            del(_tasks[name])
            return True
        return False
    
    async def coro1():
        print("coro1 start")
        await uasyncio.sleep(2)
        print("coro1 done")
    async def main():
        create_task("coro1", coro1)
        await uasyncio.sleep(0.2)
        print("destroy 1")
        destroy_task("coro1")
        await uasyncio.sleep(3)
        print("main exiting")
    
    uasyncio.run(main())
    

    Transcript:

    > ../../tools/pyboard.py  --device /dev/ttyUSB1 foo.py
    coro1 start
    destroy 1
    main exiting
    >
    

    Problem?

  5. nazsolti commented on Mar 27, 2020

    @nazsolti
    Author

    Sorry for not showing the whole test case. I have tried yours, which works, but the fact that it works, doesn't solve the error that I get. I will try to strip down my case to the relevant parts and share it. Hopefully I'll find out the cause of the error.

  6. nazsolti commented on Mar 28, 2020

    @nazsolti
    Author

    Try it with:

    import uasyncio
    
    
    _loop = uasyncio.get_event_loop()
    _tasks = {}
    
    
    async def dummy_task():
        while True:
            print('dummy')
            await uasyncio.sleep(0)
    
    
    def create_task(name, coro, *args):
        if str(name) not in _tasks:
            _tasks[name] = _loop.create_task(coro(*args))
            return True
        return False
    
    
    def destroy_task(name):
        if str(name) in _tasks:
            _tasks[name].cancel()  # this is the line raising the error
            del(_tasks[name])
            return True
        return False
    
    
    def stop():
        for task_name in _tasks:
            print('destroy', task_name)
            destroy_task(task_name)
    
    
    def run():
        create_task('dummy', dummy_task)
        _loop.run_forever()
    
    
    try:
        run()
    except KeyboardInterrupt:
        stop()

    (it will spam a lot of 'dummy' 😄 )

    I suspect the problem is that the KeyboardInterrupt is raised within the coroutine itself, therefore it is considered to be still executing, and thus can not be cancelled. Is this a desired behavior?

  7. tve commented on Mar 28, 2020

    @tve
    Contributor

    I have not read up on how exceptions like this mesh with tasks. I also don't know what is still executing by the time you hit your call to stop()... Might want to read the spec... How does CPython behave?

  8. kevinkk525 commented on Mar 28, 2020

    @kevinkk525
    Contributor

    This code doesn't even run on CPython correctly because stop() iterates over a dictionary whose size gets changed:

    Traceback (most recent call last):
      File "R:/test.py", line 43, in <module>
        stop()
      File "R:/test.py", line 30, in stop
        for task_name in _tasks:
    RuntimeError: dictionary changed size during iteration
    

    But,I now know where the "problem" is.
    uasyncio.cur_task is being set in

    cur_task = t

    and when a task is cancelled, it checks if it is uasyncio.cur_task and if so, throws a runtime error. If you exit the loop by interrupt, that variable will still stay the same although no task is running anymore because uasyncio exited.

    However, this is not a problem because what @antariandel is trying to do, doesn't even work on CPython. You simply can't cancel tasks after exiting the asyncio loop because the CancelledErrors won't be raised to the tasks anymore becauase asyncio is not running anymore.

    See this minimal testcase and run it on Cpython, you'll see that the 2nd task will never catch any exception (or ever even see that it got cancelled):

    try:
        import asyncio as uasyncio
    except:
        import uasyncio
    
    
    _loop = uasyncio.get_event_loop()
    _tasks=[]
    
    
    async def dummy_task():
        while True:
            try:
                print('dummy')
                await uasyncio.sleep(0)
            except uasyncio.CancelledError as e:
                print("Task cancelled",e)
                raise
            except Exception as e:
                print("Task exception",e)
                raise
            except BaseException as e:
                print("Task base exception",e)
                raise
    
    
    def create_task(name, coro, *args):
        if str(name) not in _tasks:
            _tasks[name] = _loop.create_task(coro(*args))
            return True
        return False
    
    def run():
        global _task
        loop=uasyncio.get_event_loop()
        _tasks.append(loop.create_task(dummy_task()))
        _tasks.append(loop.create_task(dummy_task()))
        try:
            loop.run_forever()
        except Exception as e:
            print("Run exception",e)
            raise
        except BaseException as e:
            print("Run base exception",e)
            raise
    
    def stop():
        for t in _tasks:
            t.cancel()
    
    try:
        run()
    except KeyboardInterrupt:
        stop()
    
  9. nazsolti commented on Mar 28, 2020

    @nazsolti
    Author

    It would possible to live without this, if there was a way to clean up the loop and start fresh. In CPython this isn't a problem, because you can have more loops, but in uasyncio, the event loop is a singleton.

  10. dpgeorge commented on Mar 28, 2020

    @dpgeorge
    Member

    if there was a way to clean up the loop and start fresh

    To clean up the event loop would need at least one of the following:

    1. before run_until_complete returns make sure it clears the task and IO queues
    2. add a Loop.close() method which clears the task and IO queues

    @antariandel you could try the following to see if it works for you:

    def loop_close():
        from uasyncio import core
        core._task_queue = core.TaskQueue()
        core._io_queue = core.IOQueue()
  11. nazsolti commented on Apr 3, 2020

    @nazsolti
    Author

    @dpgeorge thanks, this works for me!

  12. andrewleech commented on Apr 7, 2020

    @andrewleech
    SponsorContributor

    @dpgeorge I see uasyncio.core.Loop.close() is currently just a pass.
    Should your suggested close function above be added there?

  13. dpgeorge commented on Apr 7, 2020

    @dpgeorge
    Member

    Should your suggested close function above be added there?

    Yes that's probably a good solution for now.

  14. peterhinch commented on Apr 7, 2020

    @peterhinch
    Contributor

    This sounds like a very worthwhile improvement.

  15. dpgeorge commented on Apr 9, 2020

    @dpgeorge
    Member

    The functionality to reset the event loop is added in PR #5890 (using new_event_loop() rather than close()).

  16. added a commit that references this issue on Jan 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions