You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Celery任务执行时暂停,及处理用户页面重载场景?

页面重载时Celery转录任务的处理方案

针对你遇到的页面重载中断转录任务的问题,结合Django+Celery(Redis)技术栈,给出具体实现方案和优化建议:

前端实现要点

1. 用beforeunload替代unload实现警告与暂停

unload事件触发时机太晚,浏览器可能已经开始卸载页面,导致请求无法发送完成。改用beforeunload事件,它会在页面卸载前触发,能稳定弹出警告并发送暂停请求:

// 全局状态变量
let isTranscribing = false;
let currentTaskId = '';
let pollInterval = null;
let isPausedByUnload = false;

// 监听页面卸载前事件
window.addEventListener('beforeunload', function(e) {
    if (isTranscribing) {
        // 同步发送暂停请求,确保浏览器在卸载前处理完
        const xhr = new XMLHttpRequest();
        xhr.open('POST', `/transcribe/pause_task/${currentTaskId}/`, false);
        xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
        xhr.send();
        
        isPausedByUnload = true;
        // 触发浏览器默认警告(多数浏览器会忽略自定义文本,返回空字符串也能触发)
        e.preventDefault();
        e.returnValue = '';
        return e.returnValue;
    }
});

// 用户取消卸载后恢复任务
window.addEventListener('focus', function() {
    if (isPausedByUnload && isTranscribing) {
        const xhr = new XMLHttpRequest();
        xhr.open('POST', `/transcribe/resume_task/${currentTaskId}/`, true);
        xhr.setRequestHeader('X-CSRFToken', getCookie('csrftoken'));
        xhr.onload = function() {
            if (xhr.status === 200) {
                isPausedByUnload = false;
            }
        };
        xhr.send();
    }
});

// 获取Django CSRF Token辅助函数
function getCookie(name) {
    let cookieValue = null;
    if (document.cookie && document.cookie !== '') {
        const cookies = document.cookie.split(';');
        for (let i = 0; i < cookies.length; i++) {
            const cookie = cookies[i].trim();
            if (cookie.substring(0, name.length + 1) === (name + '=')) {
                cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
                break;
            }
        }
    }
    return cookieValue;
}

2. 调整轮询函数适配暂停状态

原轮询函数需要保留暂停状态下的轮询,这样后端能感知用户未离开,自动恢复任务:

function pollTaskStatus(taskId) {
    currentTaskId = taskId;
    isTranscribing = true;
    console.log(currentTaskId);
    
    pollInterval = setInterval(() => {
        const xhr = new XMLHttpRequest();
        xhr.onload = function() {
            if (xhr.status == 200) {
                const response = JSON.parse(xhr.responseText);
                switch(response.status) {
                    case 'completed':
                        console.log('转录完成');
                        if (response.reload) window.location.reload();
                        clearInterval(pollInterval);
                        isTranscribing = false;
                        break;
                    case 'paused':
                        // 任务暂停,继续轮询等待恢复
                        break;
                    case 'in_progress':
                        // 正常执行,无操作
                        break;
                    case 'failed':
                        showError('任务执行失败');
                        clearInterval(pollInterval);
                        isTranscribing = false;
                        break;
                }
            } else {
                showError('请求出错,请稍后重试');
                clearInterval(pollInterval);
                isTranscribing = false;
            }
        };
        xhr.onerror = function() {
            showError('网络连接异常,请检查网络后重试');
            clearInterval(pollInterval);
            isTranscribing = false;
        };
        xhr.open('GET', `/transcribe/poll_task_status/${taskId}/`, true);
        xhr.send();
    }, 5000);
}

后端实现要点

1. 扩展数据库模型跟踪状态

给TranscribedDocument模型添加任务ID、最后轮询时间字段,方便状态管理:

# models.py
from django.db import models
from django.utils import timezone

class TranscribedDocument(models.Model):
    STATE_CHOICES = [
        ('not_started', '未开始'),
        ('in_progress', '执行中'),
        ('paused', '已暂停'),
        ('completed', '已完成'),
        ('failed', '失败'),
    ]
    state = models.CharField(max_length=20, choices=STATE_CHOICES, default='not_started')
    session_id = models.CharField(max_length=100, unique=True)
    task_id = models.CharField(max_length=100, blank=True, null=True)  # 存储Celery任务ID
    last_poll_time = models.DateTimeField(default=timezone.now)  # 最后轮询时间
    # 其他原有字段...

2. 新增暂停/恢复任务API

# views.py
from django.http import JsonResponse
from django.utils import timezone
from celery.result import AsyncResult
from django_redis import get_redis_connection
from .models import TranscribedDocument
from .tasks import transcribe_file_task

# 暂停任务
def pause_task(request, task_id):
    if request.method != 'POST':
        return JsonResponse({'status': 'error', 'msg': '仅支持POST请求'}, status=400)
    
    try:
        doc = TranscribedDocument.objects.get(task_id=task_id)
        if doc.state != 'in_progress':
            return JsonResponse({'status': 'error', 'msg': '任务当前状态不支持暂停'})
        
        doc.state = 'paused'
        doc.save()
        # 用Redis存储暂停标记
        r = get_redis_connection('default')
        r.set(f'task_paused:{task_id}', '1')
        return JsonResponse({'status': 'success'})
    except TranscribedDocument.DoesNotExist:
        return JsonResponse({'status': 'error', 'msg': '任务不存在'}, status=404)

# 恢复任务
def resume_task(request, task_id):
    if request.method != 'POST':
        return JsonResponse({'status': 'error', 'msg': '仅支持POST请求'}, status=400)
    
    try:
        doc = TranscribedDocument.objects.get(task_id=task_id)
        if doc.state != 'paused':
            return JsonResponse({'status': 'error', 'msg': '任务当前状态不支持恢复'})
        
        doc.state = 'in_progress'
        doc.save()
        # 清除Redis暂停标记
        r = get_redis_connection('default')
        r.delete(f'task_paused:{task_id}')
        return JsonResponse({'status': 'success'})
    except TranscribedDocument.DoesNotExist:
        return JsonResponse({'status': 'error', 'msg': '任务不存在'}, status=404)

# 修改轮询视图,更新最后轮询时间
def poll_task_status(request, task_id):
    try:
        doc = TranscribedDocument.objects.get(task_id=task_id)
        # 更新最后轮询时间,标记用户仍在页面
        doc.last_poll_time = timezone.now()
        doc.save()
        
        task_result = AsyncResult(task_id)
        if task_result.state == 'SUCCESS':
            doc.state = 'completed'
            doc.save()
            return JsonResponse({'status': 'completed', 'reload': True})
        elif task_result.state == 'FAILURE':
            doc.state = 'failed'
            doc.save()
            return JsonResponse({'status': 'failed'})
        else:
            # 返回数据库中存储的状态(可能是暂停/执行中)
            return JsonResponse({'status': doc.state})
    except TranscribedDocument.DoesNotExist:
        return JsonResponse({'status': 'error', 'msg': '任务不存在'}, status=404)

# 修改提交视图,存储Celery任务ID
def transcribe_submit(request):
    if request.method == 'POST':
        file_name = request.POST.get('file_name')
        file_path = request.POST.get('file_path')
        session_id = request.session.session_key
        
        if not file_name or not file_path:
            return redirect('transcribeSubmit')
        
        try:
            doc = TranscribedDocument.objects.get(session_id=session_id)
            if doc.state == 'not_started':
                doc.state = 'in_progress'
                audio_lang = request.POST.get('audio_language')
                output_type = request.POST.get('output_file_type')
                task = transcribe_file_task.delay(file_path, audio_lang, output_type, 'ai_transcribe_output', session_id)
                doc.task_id = task.id
                doc.save()
            return JsonResponse({'task_id': task.id})
        except TranscribedDocument.DoesNotExist:
            # 处理文档不存在的逻辑
            return redirect('transcribeSubmit')
    # GET请求处理逻辑...

3. 修改Celery任务支持暂停/自动终止

任务内部定期检查暂停标记和用户轮询时间,实现软暂停和自动终止:

# tasks.py
from celery import shared_task
from django.utils import timezone
from django_redis import get_redis_connection
import time
from .models import TranscribedDocument
import os
from django.conf import settings

@shared_task(bind=True)
def transcribe_file_task(self, path, audio_language, output_file_type, dest_dir, session_id):
    r = get_redis_connection('default')
    doc = TranscribedDocument.objects.get(session_id=session_id)
    task_id = self.request.id
    
    try:
        # 初始化输出路径
        local_output_dir = '/tmp/'
        filename = os.path.splitext(os.path.basename(path))[0]
        local_output_file = os.path.join(local_output_dir, f"{filename}.{output_file_type}")

        # 模拟分块处理(实际转录逻辑需要拆分为可中断的步骤)
        if audio_language == 'detect':
            model = load_model()
            audio = load_audio(path)
            audio_chunks = split_audio_into_chunks(audio)  # 自定义拆分音频的函数
            
            for chunk in audio_chunks:
                # 检查是否被暂停
                while r.get(f'task_paused:{task_id}'):
                    time.sleep(1)
                    # 检查用户是否超过15秒未轮询(3次轮询间隔),若超过则终止任务
                    doc.refresh_from_db()
                    time_diff = timezone.now() - doc.last_poll_time
                    if time_diff.total_seconds() > 15:
                        doc.state = 'failed'
                        doc.save()
                        r.delete(f'task_paused:{task_id}')
                        return
                
                # 处理当前音频块
                chunk_result = transcribe(chunk, verbose=False)
                # 可选:保存临时结果
            
            # 合并所有块结果并生成文件
            final_result = combine_chunk_results(chunk_results)  # 自定义合并函数
            writer = get_writer(output_file_type, local_output_dir)
            writer(final_result, path)
            
            # 存储转录结果的逻辑...
            doc.state = 'completed'
            doc.save()
            
    except Exception as e:
        doc.state = 'failed'
        doc.save()
        # 可选:重试任务
        raise self.retry(exc=e, countdown=5)

更优方案建议

  1. 替换轮询为WebSocket:用Django Channels实现WebSocket通信,后端可以主动推送任务状态,用户断开连接时能立即感知,无需依赖轮询超时判断,效率更高,状态更实时。
  2. Celery任务软中断:避免直接用revoke强制终止任务,通过Redis标记实现软暂停/终止,保证任务能完成当前步骤,避免数据损坏。
  3. 任务状态持久化:所有状态都存在数据库中,不依赖Celery的结果存储,即使Celery重启,任务状态也不会丢失。

内容的提问来源于stack exchange,提问作者tthheemmaannii

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 10:47:02