Как я могу выполнить 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()
показался наиболее удобным. Если у вас есть другие предложения, пожалуйста, поделитесь ими!