【问题标题】:Django Redis-Queue pass result from background job into htmlDjango Redis-Queue 将后台作业的结果传递到 html
【发布时间】:2021-11-22 04:46:11
【问题描述】:

我的小型 Django 项目只有一个视图。用户可以输入一些选项并按提交,因此提交需要>3 分钟的作业。我使用redis-queue 通过后台进程执行作业——Web 进程只是返回(并呈现.html)。这一切都有效。 问题是,如何在完成后将后台的结果(确实下载文件的文件名)传递回 html,以便显示下载按钮?

我想我必须将一个小的 Java / jQuery 脚本放入 .html 中,该脚本会持续轮询工作是否完成(也许利用 RQ 的 on_success / on_failure 函数?)。但是,我不确定这种方法。搜索此事没有成功(simular issue)。谁能指导我这样做的正确方法是什么?我不打算使用芹菜。理想的解决方案是提供一些代码或明确路径的解决方案。这是一个剥离的例子:

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);
}

【问题讨论】:

    标签: python django asynchronous redis


    【解决方案1】:

    有几种方法,各有不同的优缺点。

    最简单的方法正是您提到的:轮询。轮询的缺点是:

    • 不是实时的(您将在下一次投票请求之前获得结果)
    • 不能很好地扩展。这只是一个问题,因为您有许多客户都在同时等待他们的结果。例如:10 万个客户端每 5 秒轮询一次 = 每秒向服务器发送 2 万个请求。

    主要优点是实现简单。

    实时替代方案是long pollingwebsocketswebrtc data channels。第一个几乎已经过时了,第二个是我推荐的,最后一个是对现有协议的滥用,但不是为了这个目的(它仍然有效!)。 Websocketswebrtc 都提供浏览器和服务器之间的双向通信。服务器可以push 向您的浏览器发送一条消息,该浏览器会监听这些事件的反应。由于 Django 的同步 req/res 模型,它不能开箱即用,因此您需要查看 django-channels 甚至将 django 完全替换为 aiohttpfastapi 之类的东西。

    假设您不需要实时反馈,并且您没有为成千上万的用户提供您的应用程序,我建议您坚持投票。

    高级概述:
    1. 客户提出请求。

    2. 服务器创建一个带有唯一标识符的“作业”记录,并立即将此标识符返回给客户端,并推送您的 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 worker 完成后,它应该用最终状态更新 Job 记录:finishedfailed
    j = Job.objects.get(id=job_id)
    j.state = Job.FINISHED
    j.result_path = '...'
    j.save()
    
    1. 最后,在下一个轮询请求中,您的客户端现在将知道已完成的工作,现在可以下载结果、错误消息等。

    (注意:所有代码都是伪代码,这里没有测试)

    【讨论】:

    • 哇,感谢您的精彩解释,非常感谢!我将尝试实施解决方案,并在适当的情况下将答案标记为正确。
    • 我尝试了解决方案,但无法弄清楚如何正确使用 java sn-p。我必须以某种方式从 HTML 显式调用 myInterval 吗?
    猜你喜欢
    • 2012-08-23
    • 2018-03-24
    • 1970-01-01
    • 1970-01-01
    • 2021-02-08
    • 2022-06-23
    • 2023-01-13
    • 2019-05-11
    • 1970-01-01
    相关资源
    最近更新 更多