Sfoglia il codice sorgente

fix(queue): surface analysis pdf failures to Laravel failed_jobs

Align analysis PDF jobs with Laravel queue semantics by using tries/backoff,
throwing on failures, and final failed() handling. Also add afterCommit and
failure callbacks so task status/callbacks stay consistent with exam PDF flow.

Made-with: Cursor
yemeishu 2 settimane fa
parent
commit
371d3e585a

+ 29 - 37
app/Jobs/GenerateAnalysisPdfJob.php

@@ -9,6 +9,7 @@ use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use Illuminate\Support\Facades\Log;
+use Throwable;
 
 class GenerateAnalysisPdfJob implements ShouldQueue
 {
@@ -17,7 +18,10 @@ class GenerateAnalysisPdfJob implements ShouldQueue
     public string $paperId;
     public string $studentId;
     public ?string $recordId;
-    public int $maxAttempts = 3;
+    public int $tries = 3;
+    public int $timeout = 300;
+
+    public array $backoff = [5, 10, 20];
 
     public function __construct(string $paperId, string $studentId, ?string $recordId = null)
     {
@@ -27,6 +31,8 @@ class GenerateAnalysisPdfJob implements ShouldQueue
 
         // 指定使用 pdf 队列,由独立的 pdf-worker 容器处理
         $this->onQueue('pdf');
+        // 避免事务未提交时 worker 提前消费导致读到未提交数据
+        $this->afterCommit();
     }
 
     public function handle(ExamPdfExportService $pdfExportService): void
@@ -46,51 +52,37 @@ class GenerateAnalysisPdfJob implements ShouldQueue
                 $this->recordId
             );
 
-            if ($pdfUrl) {
-                Log::info('学情分析PDF生成成功', [
-                    'paper_id' => $this->paperId,
-                    'student_id' => $this->studentId,
-                    'record_id' => $this->recordId,
-                    'pdf_url' => $pdfUrl,
-                ]);
-            } else {
-                Log::error('学情分析PDF生成失败', [
-                    'paper_id' => $this->paperId,
-                    'student_id' => $this->studentId,
-                    'record_id' => $this->recordId,
-                ]);
-
-                // 如果失败且还有重试次数,则重试
-                if ($this->attempts() < $this->maxAttempts) {
-                    Log::info('将在5秒后重试PDF生成', [
-                        'paper_id' => $this->paperId,
-                        'student_id' => $this->studentId,
-                        'attempt' => $this->attempts(),
-                    ]);
-                    $this->release(5);
-                    return;
-                }
+            if (! $pdfUrl) {
+                throw new \RuntimeException('学情分析PDF生成失败:返回空URL');
             }
 
-        } catch (\Exception $e) {
+            Log::info('学情分析PDF生成成功', [
+                'paper_id' => $this->paperId,
+                'student_id' => $this->studentId,
+                'record_id' => $this->recordId,
+                'pdf_url' => $pdfUrl,
+            ]);
+
+        } catch (\Throwable $e) {
             Log::error('学情分析PDF生成队列任务失败', [
                 'paper_id' => $this->paperId,
                 'student_id' => $this->studentId,
                 'record_id' => $this->recordId,
                 'error' => $e->getMessage(),
                 'trace' => $e->getTraceAsString(),
+                'attempt' => $this->attempts(),
             ]);
-
-            // 如果是第一次失败且可能是临时错误,等待后重试
-            if ($this->attempts() < $this->maxAttempts) {
-                Log::info('检测到临时错误,将在10秒后重试', [
-                    'paper_id' => $this->paperId,
-                    'student_id' => $this->studentId,
-                    'attempt' => $this->attempts(),
-                ]);
-                $this->release(10);
-                return;
-            }
+            throw $e;
         }
     }
+
+    public function failed(Throwable $exception): void
+    {
+        Log::error('学情分析PDF生成队列任务最终失败', [
+            'paper_id' => $this->paperId,
+            'student_id' => $this->studentId,
+            'record_id' => $this->recordId,
+            'error' => $exception->getMessage(),
+        ]);
+    }
 }

+ 53 - 9
app/Jobs/ProcessAnalysisReportTaskJob.php

@@ -3,18 +3,22 @@
 namespace App\Jobs;
 
 use App\Services\ExamAnalysisService;
+use App\Services\TaskManager;
 use Illuminate\Bus\Queueable;
 use Illuminate\Contracts\Queue\ShouldQueue;
 use Illuminate\Foundation\Bus\Dispatchable;
 use Illuminate\Queue\InteractsWithQueue;
 use Illuminate\Queue\SerializesModels;
 use Illuminate\Support\Facades\Log;
+use Throwable;
 
 class ProcessAnalysisReportTaskJob implements ShouldQueue
 {
     use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
 
     public int $tries = 3;
+    public int $timeout = 300;
+    public array $backoff = [5, 10, 20];
 
     public function __construct(
         public string $taskId,
@@ -24,24 +28,64 @@ class ProcessAnalysisReportTaskJob implements ShouldQueue
     ) {
         // 与 PDF 相关重流程统一走 pdf 队列
         $this->onQueue('pdf');
+        // 避免事务未提交时 worker 提前消费导致读到旧数据
+        $this->afterCommit();
     }
 
     public function handle(ExamAnalysisService $examAnalysisService): void
     {
-        Log::info('ProcessAnalysisReportTaskJob: 开始处理学情报告任务', [
+        try {
+            Log::info('ProcessAnalysisReportTaskJob: 开始处理学情报告任务', [
+                'task_id' => $this->taskId,
+                'paper_id' => $this->paperId,
+                'student_id' => $this->studentId,
+                'record_id' => $this->recordId,
+                'attempt' => $this->attempts(),
+            ]);
+
+            $examAnalysisService->processReportGenerationTask(
+                $this->taskId,
+                $this->paperId,
+                $this->studentId,
+                $this->recordId
+            );
+        } catch (Throwable $e) {
+            Log::error('ProcessAnalysisReportTaskJob: 处理失败,将交由队列重试/失败落库', [
+                'task_id' => $this->taskId,
+                'paper_id' => $this->paperId,
+                'student_id' => $this->studentId,
+                'record_id' => $this->recordId,
+                'attempt' => $this->attempts(),
+                'error' => $e->getMessage(),
+            ]);
+            throw $e;
+        }
+    }
+
+    public function failed(Throwable $exception): void
+    {
+        try {
+            $taskManager = app(TaskManager::class);
+            $taskManager->markTaskFailed(
+                $this->taskId,
+                '学情报告生成失败:'.$exception->getMessage()
+            );
+            // 与成功路径保持一致:最终失败也发回调,避免调用方只等待回调而无结果。
+            $taskManager->sendCallback($this->taskId);
+        } catch (Throwable $innerException) {
+            Log::error('ProcessAnalysisReportTaskJob: failed回调更新任务失败', [
+                'task_id' => $this->taskId,
+                'error' => $innerException->getMessage(),
+            ]);
+        }
+
+        Log::error('ProcessAnalysisReportTaskJob: 队列任务最终失败', [
             'task_id' => $this->taskId,
             'paper_id' => $this->paperId,
             'student_id' => $this->studentId,
             'record_id' => $this->recordId,
-            'attempt' => $this->attempts(),
+            'error' => $exception->getMessage(),
         ]);
-
-        $examAnalysisService->processReportGenerationTask(
-            $this->taskId,
-            $this->paperId,
-            $this->studentId,
-            $this->recordId
-        );
     }
 }
 

+ 21 - 33
app/Services/ExamAnalysisService.php

@@ -121,45 +121,33 @@ class ExamAnalysisService
      */
     public function processReportGenerationTask(string $taskId, string $paperId, string $studentId, ?string $recordId): void
     {
-        try {
-            // 分析数据组装、渲染与 exam_analysis_results / student_reports 落库均在
-            // ExamPdfExportService::generateAnalysisReportPdf 内完成;此处不再重复 getAnalysisData,
-            // 避免 pdf worker 内双倍内存与 DB 压力(与 GenerateAnalysisPdfJob 路径一致)。
-            $this->taskManager->updateTaskProgress($taskId, 10, '正在生成学情分析PDF...');
-
-            $pdfUrl = $this->pdfExportService->generateAnalysisReportPdf($paperId, $studentId, $recordId);
+        // 分析数据组装、渲染与 exam_analysis_results / student_reports 落库均在
+        // ExamPdfExportService::generateAnalysisReportPdf 内完成;此处不再重复 getAnalysisData,
+        // 避免 pdf worker 内双倍内存与 DB 压力(与 GenerateAnalysisPdfJob 路径一致)。
+        $this->taskManager->updateTaskProgress($taskId, 10, '正在生成学情分析PDF...');
 
-            if (!$pdfUrl) {
-                throw new \Exception('PDF生成失败');
-            }
+        $pdfUrl = $this->pdfExportService->generateAnalysisReportPdf($paperId, $studentId, $recordId);
 
-            $this->taskManager->updateTaskProgress($taskId, 90, '正在完成任务...');
-
-            // 标记任务完成
-            $this->taskManager->markTaskCompleted($taskId, [
-                'pdf_url' => $pdfUrl,
-            ]);
+        if (!$pdfUrl) {
+            throw new \RuntimeException('PDF生成失败');
+        }
 
-            Log::info('ExamAnalysisService: 学情报告生成完成', [
-                'task_id' => $taskId,
-                'paper_id' => $paperId,
-                'student_id' => $studentId,
-                'pdf_url' => $pdfUrl,
-            ]);
+        $this->taskManager->updateTaskProgress($taskId, 90, '正在完成任务...');
 
-            // 发送回调通知
-            $this->taskManager->sendCallback($taskId);
+        // 标记任务完成
+        $this->taskManager->markTaskCompleted($taskId, [
+            'pdf_url' => $pdfUrl,
+        ]);
 
-        } catch (\Exception $e) {
-            Log::error('ExamAnalysisService: 报告生成失败', [
-                'task_id' => $taskId,
-                'paper_id' => $paperId,
-                'student_id' => $studentId,
-                'error' => $e->getMessage(),
-            ]);
+        Log::info('ExamAnalysisService: 学情报告生成完成', [
+            'task_id' => $taskId,
+            'paper_id' => $paperId,
+            'student_id' => $studentId,
+            'pdf_url' => $pdfUrl,
+        ]);
 
-            $this->taskManager->markTaskFailed($taskId, $e->getMessage());
-        }
+        // 发送回调通知
+        $this->taskManager->sendCallback($taskId);
     }
 
     /**