Как я могу выполнить ModelAdmin.save_model асинхронно?

Веб-сайт на основе Django, который я создаю, может иногда нуждаться в выполнении команд shell по инициативе административного пользователя. Предвидя это, я решил, что будет хорошей идеей написать эти процессы асинхронными, учитывая, что asyncio поддерживает shells прямо из коробки. Я выполняю эти процессы из функции save_model() в ModelAdmin, не асинхронного представления.

Моя проблема в том, что когда выполняются длительные процессы, якобы "асинхронный" сервер полностью зависает на них и не отвечает на другие запросы от клиентов, пока они не завершатся. Я на 100% уверен, что это происходит потому, что я запускаю эти процессы в функции синхронизации, хотя и асинхронно (см. ниже).

Мне нужно иметь возможность сообщить административному пользователю, что процесс, который он запускает, завершился успешно или неудачно. В настоящее время я делаю это через API сообщений, предоставляемый Django.

Вот функция, выполняющая команды оболочки (Linux):

import asyncio
import sys

async def run_subprocess(cmd: str) -> Tuple[int, bytes, bytes]:
    """Run a subprocess command asynchronously.

    Notes:
        This will execute the subprocess in a shell under the current
        user.

    Args:
        cmd (`str`):
            The command to run.

    Returns:
        exec_info (`tuple`):
            `(retcode, stdout, stderr)` from the subprocess for
            processing.
    """

    # Create an execute the future
    try:
        proc = await asyncio.create_subprocess_shell(
            cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE
        )
        retcode = await proc.wait()
        out, err = await proc.communicate()
    except RuntimeError:
        # Raised when we are on a Windows machine
        if sys.platform == 'win32':
            raise RuntimeError(
                "Cannot execute async subprocess shells from a WindowsSelectorEvent loop"
            )
        else:
            raise

    return retcode, out, err

Вот saved_model() функция, которую я сейчас имею.

from asgiref.sync import async_to_sync, sync_to_async
from django.contrib import admin, messages
from loguru import logger

def save_model(
    self,
    request,
    obj,
    form,
    change,
) -> None:

    def add_error_message(message: str) -> None:
        logger.error(message)
        messages.add_message(
            request,
            messages.ERROR,
            message
        )
        messages.add_message(
            request,
            messages.WARNING,
            "The server will rerun the process later."
        )

    async def wrapper() -> None:
        # Run the process
        if True:
            logger.info("Administrative user is attempting to run processes for 'object'")
            # Simulate long-running process
            try:
                ret, stdout, stderr = await run_subprocess('sleep 20')
                online = (ret == 0 and "0% packet loss" in stdout.decode())
            except RuntimeError:
                logger.exception("Unexpected error:\n")
                add_error_message("The process failed to run")

    async_to_sync(wrapper)()

    return super().save_model(request, obj, form, change)

Я бы предпочел сообщать пользователю статус завершения этих процессов после их завершения, но до того, как Django завершит запрос и ответ. Это означает, что я хотел бы избежать планирования run_subprocess с помощью asyncio.create_task или loop.call_later, так как они, очевидно, решат проблему блокировки, но также заставят Django вернуться до завершения процессов.

РЕДАКТИРОВАНИЕ:

Обратите внимание, что я не обязан save_model() по какой-либо конкретной причине. Мне просто нужно иметь возможность проверять модель, сохраняемую ModelAdmin'ом, чтобы определить, какие процессы нужно запустить. На данный момент save_model() показался наиболее удобным. Если у вас есть другие предложения, пожалуйста, поделитесь ими!

Вернуться на верх