从 Django 视图启动长时间运行的进程

59 阅读2分钟

在 Django 视图中,我们需要运行一个可能需要数小时才能完成的进程。我们不需要知道进程的状态或与之通信,但我们需要在启动进程后立即重定向视图。

我们尝试了使用 subprocess.Popen,在新的 threading.Thread、multiprocessing.Process 中使用它。但是,父进程会一直挂起,直到子进程终止。唯一几乎可以完成的方法是使用 fork。显然这不是一个好方法,因为它会在父进程终止之前留下一个僵尸进程。

这是我们尝试使用 fork 时所做的事情:

if os.fork() == 0:
    subprocess.Popen(["/usr/bin/python", script_path, "-v"])
else:
    return HttpResponseRedirect(reverse('view_to_redirect'))

那么,有没有一种方法可以从 Django 视图中运行一个完全独立的进程,同时造成的损失最小?或者我们做错了什么?

2. 解决方案

2.1 使用任务队列

一种解决方案是使用任务队列。当视图被调用时,它会在任务中输入一条新记录并愉快地重定向。然后,任务由 cron 定期执行,独立于 Django。

cron 调用相关的(和自定义的)Django 命令来执行任务。

2.2 使用 Celery

另一个解决方案是使用 Celery,这是一个用于 Django 的任务队列。它基于 RabbitMQ,并提供了一个简单的 API 来创建和管理任务。

2.3 使用 django-command-extensions

django-command-extensions 是一个 Django 库,它提供了扩展 Django 管理命令的功能,包括在后台运行命令。

我们可以使用 django-command-extensions 来创建一个作业,然后在视图中启动作业。

2.4 使用 kronos.py

kronos.py 是一个 Python 库,它提供了一个轻量级的框架来创建和管理长时间运行的进程。

我们可以使用 kronos.py 来创建一个进程,然后在视图中启动进程。

代码例子

使用任务队列

from django.contrib import admin
from django.db import models

class Task(models.Model):
    name = models.CharField(max_length=255)
    status = models.CharField(max_length=255)

    def __str__(self):
        return self.name


class TaskAdmin(admin.ModelAdmin):
    list_display = ('name', 'status')


admin.site.register(Task, TaskAdmin)


from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def start_task(request):
    task = Task(name="My Task", status="New")
    task.save()

    return redirect('task_detail', task_id=task.id)


@csrf_exempt
def task_detail(request, task_id):
    task = Task.objects.get(id=task_id)

    context = {
        'task': task,
    }

    return render(request, 'task_detail.html', context)

使用 Celery

from celery import Celery

app = Celery('tasks', broker='amqp://guest@localhost//')


@app.task
def long_running_task():
    # Do something that takes a long time

    return "Task completed"


from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def start_task(request):
    task = long_running_task.delay()

    return redirect('task_detail', task_id=task.id)


@csrf_exempt
def task_detail(request, task_id):
    task = long_running_task.AsyncResult(task_id)

    context = {
        'task': task,
    }

    return render(request, 'task_detail.html', context)

使用 django-command-extensions

from django_command_extensions.management.commands import job

class LongRunningJob(job.AbstractJob):
    def run(self):
        # Do something that takes a long time

        return "Task completed"


from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def start_task(request):
    job = LongRunningJob()
    job.start()

    return redirect('task_detail', task_id=job.id)


@csrf_exempt
def task_detail(request, task_id):
    job = LongRunningJob.objects.get(id=task_id)

    context = {
        'task': job,
    }

    return render(request, 'task_detail.html', context)

使用 kronos.py

import kronos

class LongRunningProcess(kronos.Job):
    def run(self):
        # Do something that takes a long time

        return "Task completed"


from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def start_task(request):
    process = LongRunningProcess()
    process.start()

    return redirect('task_detail', task_id=process.id)


@csrf_exempt
def task_detail(request, task_id):
    process = LongRunningProcess.objects.get(id=task_id)

    context = {
        'task': process,
    }

    return render(request, 'task_detail.html', context)