Django Redis-Queue передает результат фонового задания в html
Мой небольшой Django-проект имеет только одно представление. Пользователь может ввести некоторые параметры и нажать submit, таким образом коммитируя работу, которая занимает >3 минуты. Я использую redis-queue для выполнения заданий через фоновый процесс - веб-процесс просто возвращается (и рендерит .html). Все это работает. Вопрос в том, как я могу передать результат фонового процесса (имя загружаемого файла) после завершения обратно в html, чтобы я мог отобразить кнопку загрузки?
Полагаю, что мне придется поместить в .html небольшой Java / jQueryскрипт, который будет опрашивать, завершилось ли задание или нет (возможно, используя функции RQ on_success / on_failure?). Однако я не уверен в этом подходе. Поиск по этому вопросу не принес успеха (simular issue). Может ли кто-нибудь подсказать мне, какой способ является правильным? Я не собираюсь использовать Celery. Идеальным решением будет то, которое предоставит какой-нибудь код или четкий путь. Вот урезанный пример:
view.py
from django.shortcuts import render
from django.contrib import messages
from django.views.decorators.csrf import csrf_exempt
from django.http import HttpResponse
from .forms import ScriptParamForm
from .queue_job import my_job
@csrf_exempt
def home(request):
context = {}
if request.method == 'POST':
if request.POST.get("btn_submit"):
req_dict = dict(request.POST)
form = ScriptParamForm(request.POST)
context['form'] = form
if form.is_valid():
req_dict = dict(request.POST)
job = my_job(outdir=tmp_dir, **req_dict)
context['job'] = job
return render(request, 'webapp/home.html', context)
elif request.POST.get("btn_download"):
serve_file = request.POST.get("btn_download")
with open(serve_file, 'rb') as fp:
response = HttpResponse(fp, headers={
'Content-Type': 'application/zip',
'Content-Disposition': f'attachment; filename="{serve_file}"'
})
return response
else:
form = ScriptParamForm()
context['form'] = form
return render(request, 'webapp/home.html', context)
home.html
{% extends 'base.html' %}
{% block content %}
<form method="POST" autocomplete="on">
{% csrf_token %}
<!-- FORM FIELDS -->
...
<!-- SUBMIT / DOWNLOAD BUTTONS -->
<div>
<button type="submit" name="btn_submit" value="submit" class="btn btn-primary" id="btn-submit">Submit</button>
{% if serve_file %}
<button type="submit" name="btn_download" value={{serve_mode}} class="btn btn-primary">Download</button>
{% endif %}
<span class="not-visible" id="calc-msg"> This may take a few, relax! :) </span>
</div>
</form>
{% endblock content %}
main.js
var x = document.getElementById("btn-submit")
console.log(x)
x.onclick = function () {
$("#calc-msg").removeClass("not-visible");
setInterval(function(){blink()}, 1000);
};
function blink() {
$("#calc-msg").fadeTo(100, 0.1).fadeTo(200, 1.0);
}
Существует несколько способов, каждый из которых имеет свои преимущества и недостатки.
Самый простой подход - это именно то, что вы упомянули: опрос. Недостатками опроса являются:
- это не реальное время (вы получите результат не раньше, чем при следующем запросе на опрос)
- он не очень хорошо масштабируется. Это проблема только в том случае, если у вас много клиентов, ожидающих результатов в одно и то же время. Например: 100k клиентов, опрашивающих раз в 5 секунд = 20k запросов в секунду к серверу.
Основным преимуществом является простота реализации.
Альтернативами для реального времени являются long polling
, websockets
и webrtc data channels
. Первый практически устарел, второй - то, что я рекомендую, последний - своего рода злоупотребление протоколом, который существует, но не предназначен для этой цели (тем не менее, он работает!). Websockets
и webrtc
обеспечивают двустороннюю связь между вашим браузером и сервером. Сервер может push
передать сообщение вашему браузеру, который слушает и реагирует на эти события. Из-за синхронной модели req/res в Django, он не способен использовать ни то, ни другое из коробки, поэтому вам нужно будет изучить django-channels
или даже полностью заменить django чем-то вроде aiohttp
или fastapi
.
Предполагая, что вам не нужна обратная связь в реальном времени и что вы не обслуживаете свое приложение для 10 тысяч пользователей, я бы посоветовал вам придерживаться опроса.
Обзор высокого уровня:
Клиент делает запрос.
Сервер создает запись "Job" с уникальным идентификатором и немедленно возвращает этот идентификатор клиенту, а также проталкивает ваше сообщение reqis-queue. Предположим, что "Работа" имеет 3 возможных состояния:
pending
/finished
/failed
.
class Job(models.Model):
PENDING = 0
FINISHED = 1
FAILED = 2
state = models.IntegerField(default=Job.PENDING)
result_path = models.CharField(default=None, null=True)
В своем представлении создайте экземпляр и передайте идентификатор в очередь:
job_instance = Job.objects.create()
req_dict = dict(request.POST)
job = my_job(outdir=tmp_dir, job_id=job_instance.id, **req_dict)
context['job'] = job
context['job_id'] = job_instance.id
- Клиент использует этот идентификатор для опроса результатов. Пока
Job
все ещеpending
, он должен продолжать запрашивать обновления на сервере.
let myInterval = setInterval(() => {
fetch('your-url.com/job-poll/{job_id}.json')
.then(response => response.json())
.then(data => {
if (data.job.state === 1) { // finished
// download {data.job.result_path} and
clearInterval(myInterval);
} else if (data.job.state === 2) { // failed
// tell the user
clearInterval(myInterval);
} else { // pending
// job is still pending, do nothing.
}
})
}, 3000)
Новое представление для обработки опроса:
from django.http import JsonResponse
def job_poll(request, job_id):
j = Job.objects.get(id=job_id)
return JsonResponse({
"job": {
"state": j.state,
"result_path": j.result_path,
},
})
- Когда рабочий redis-queue завершает работу, он должен обновить запись Job с окончательным состоянием:
finished
илиfailed
.
j = Job.objects.get(id=job_id)
j.state = Job.FINISHED
j.result_path = '...'
j.save()
- Наконец, при следующем запросе опроса ваш клиент будет знать о завершении работы и сможет загрузить результат, сообщение об ошибке и т.д. .
(примечание: весь код является псевдокодом, здесь ничего не тестировалось)