[Django] SynchronousOnlyOperation 解決方法

在使用 Django Rest Framework 開發的時候出現 SynchronousOnlyOperation 的錯誤,本篇展示一個範例紀錄筆者使用 sync_to_async 的解決方法,一開始看 sync_to_async 說明的時候並不好理解要怎麼使用,參考範例可以比較容易了解,關於 django 的一些基本介紹可以參考 [Django] 用 Python 寫網頁?

SynchronousOnlyOperation: You cannot call this from an async context - use a thread or sync_to_async.

查詢 django 關於 SynchronousOnlyOperation 給出的說明是因為:

The SynchronousOnlyOperation exception is raised when code that is only allowed in synchronous Python code is called from an asynchronous context (a thread with a running asynchronous event loop). These parts of Django are generally heavily reliant on thread-safety to function and don’t work correctly under coroutines sharing the same thread.

仔細查了一下有關這個 error 的一些討論發現這與 async code 中是否有使用到 SQL database 有關係,例如你有以下一個程式碼,在 upload 的函式裡面,利用 aiofiles.open 去平行上傳多個檔案,所以這時候 upload 就是一個 async 函式,假設在 get_access_token 這一個函式裡面有使用到 SQL database 就會報以上的錯誤。

async def upload(self, account: str, path: str, *files) -> str:
    token = self.get_access_token()
    
    for file in files:
        name = os.path.basename(file)
        async with aiofiles.open(file, mode="rb") as f:
            data = await f.read()
        response = self.request_retry(method='put', data=data)
        response.raise_for_status()
    return url
解決方法一:

要解決這個報錯就是要 sync_to_async 將一個 sync 的函式轉成 async 能夠接受的形式,最簡單的解決方法就是將 get_access_token 裝飾成 sync_to_async 如以下的程式碼,以下參考

from asgiref.sync import async_to_sync

@async_to_sync
async def get_access_token(...):

但是這樣做有一個缺點,就是所有使用 get_access_token 的其他地方都相應也要變成 async 這有時候會受限。

解決方法二:

另外也可以在使用的當下再將 get_access_token 的函式轉變形式,如以下的方式:

async def upload_blob(self, account: str, path: str, *files) -> str:
    token = await sync_to_async(self.get_access_token)()

    for file in files:
        name = os.path.basename(file)
        async with aiofiles.open(file, mode="rb") as f:
            data = await f.read()
        response = self.request_retry(method='put', data=data)
        response.raise_for_status()
    return url