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 тысяч пользователей, я бы посоветовал вам придерживаться опроса.

Обзор высокого уровня:
  1. Клиент делает запрос.

  2. Сервер создает запись "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
  1. Клиент использует этот идентификатор для опроса результатов. Пока 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,
      },
    })
  1. Когда рабочий redis-queue завершает работу, он должен обновить запись Job с окончательным состоянием: finished или failed
  2. .
j = Job.objects.get(id=job_id)
j.state = Job.FINISHED
j.result_path = '...'
j.save()
  1. Наконец, при следующем запросе опроса ваш клиент будет знать о завершении работы и сможет загрузить результат, сообщение об ошибке и т.д.
  2. .

(примечание: весь код является псевдокодом, здесь ничего не тестировалось)

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