1
import asyncio
from asgiref.sync import sync_to_async


@sync_to_async
def sync_func():
    print("sync_func() was CALLED (good!)")


async def async_func():
    print("async_func() was CALLED (good!)")


async def test(excep_type: type = asyncio.CancelledError):
    print(f"\nException Type: {excep_type}")
    try:
        raise excep_type
    except:
        await async_func()
        try:
            await sync_func()
        except asyncio.CancelledError:
            print("sync_func() was CANCELLED (bad!)")

asyncio.run(test(excep_type=ValueError))
asyncio.run(test())

Hi there. I have a simple example

  1. test is an async function which does nothing but raise a provided error type and then await both async_func and sync_func inside.
  2. sync_func is a synchronous function made asynchronous with sync_to_async of the asgiref package.

Running this gives the following output:

Exception Type: <class 'ValueError'>
async_func() was CALLED (good!)
sync_func() was CALLED (good!)

Exception Type: <class 'asyncio.exceptions.CancelledError'>
async_func() was CALLED (good!)
sync_func() was CANCELLED (bad!)

In the first case, ValueError is raised and inside the exception clause, I can await async_func and sync_func like normal.

However in the second case, I have behaviour I wouldn't expect. I should be able to await sync_func like normal, but it instead raises asyncio.CancelledError as indicated by the printout.

In reality I haven't changed anything between the two calls, except changed the type of error which was originally raised and caught by the exception clause. However the type of raised error seems to have had an effect.

What this means real-world, is that I cannot use an asynchronous function wrapped by sync_to_async in any clean-up activity of cancelled tasks

It seems this is caused by the sync_to_async decorator, as async_func runs fine.

What am I doing wrong?

Silversonic
  • 1,289
  • 2
  • 11
  • 26
  • 1
    That looks like an issue with the decorator. When I omit the decorator and replace `await sync_func()` with `await asyncio.get_event_loop().run_in_executor(None, sync_func)`, your code runs just fine, so maybe you should avoid the decorator and just use the built-in asyncio functionality. – user4815162342 Mar 25 '21 at 08:58
  • 1
    Thanks @user4815162342. This appears to be a bug with asgiref 3.3.1 which I've logged: https://github.com/django/asgiref/issues/247 – Silversonic Mar 25 '21 at 16:32

1 Answers1

1

This is a bug in sync_to_async introduced in asgiref 3.3.1.

https://github.com/django/asgiref/issues/247

This can be resolved using the built-in asyncio functionality or, if that is not possible, wrapping the failing coroutine in create_task.

Silversonic
  • 1,289
  • 2
  • 11
  • 26