Ver Fonte

图片任务超时调整成480秒

lh há 1 semana atrás
pai
commit
f06e7fd8ef

+ 2 - 1
app/Console/Commands/CheckImageGenerationTasksCommand.php

@@ -36,7 +36,8 @@ class CheckImageGenerationTasksCommand extends Command
         // 将指定时间点之前的排队模型(火山API/image2)生成中图片任务设置为待生成以供重试
         // (防止 worker 被强杀后任务长期停留在 processing 变成死任务)
         // 同时写入 next_retry_at,使重置后的任务排到队列尾部,先让其它任务过一遍
-        $time_ago = date('Y-m-d H:i:s', strtotime('-10 minutes'));
+        // 阈值需大于 job 超时(540s),否则可能把仍在执行的请求误当成死任务重置
+        $time_ago = date('Y-m-d H:i:s', strtotime('-15 minutes'));
         $timeout_count = DB::table('mp_generate_pic_tasks')->whereNotNull('started_at')->where('started_at', '<', $time_ago)->where('status', 'processing')
             ->whereIn('model', array_merge(BaseConst::GPT_IMAGE2_MODELS, BaseConst::VOLC_PIC_MODELS))
             ->update([

+ 2 - 2
app/Jobs/ProcessGptImage2GenerationJob.php

@@ -24,12 +24,12 @@ class ProcessGptImage2GenerationJob implements ShouldQueue
     /**
      * 任务超时时间(秒)
      *
-     * 必须大于上游请求超时(AIImageGenerationService::REQUEST_TIMEOUT_SECONDS = 300s),
+     * 必须大于上游请求超时(AIImageGenerationService::REQUEST_TIMEOUT_SECONDS = 480s),
      * 让请求先超时、由业务层记录错误并重新排队;supervisor 的 stopwaitsecs 需不小于该值。
      *
      * @var int
      */
-    public $timeout = 360;
+    public $timeout = 600;
 
     /**
      * 任务ID

+ 2 - 2
app/Jobs/ProcessVolcImageGenerationJob.php

@@ -24,12 +24,12 @@ class ProcessVolcImageGenerationJob implements ShouldQueue
     /**
      * 任务超时时间(秒)
      *
-     * 必须大于上游请求超时(AIImageGenerationService::REQUEST_TIMEOUT_SECONDS = 300s),
+     * 必须大于上游请求超时(AIImageGenerationService::REQUEST_TIMEOUT_SECONDS = 480s),
      * 让请求先超时、由业务层记录错误并重新排队;supervisor 的 stopwaitsecs 需不小于该值。
      *
      * @var int
      */
-    public $timeout = 360;
+    public $timeout = 600;
 
     /**
      * 任务ID

+ 42 - 19
app/Services/AIGeneration/AIImageGenerationService.php

@@ -50,13 +50,13 @@ class AIImageGenerationService
     /**
      * 单次上游请求超时(秒)
      *
-     * 必须小于队列 job 的 timeout(360 秒),保证请求先超时、由业务层记录错误并重新排队,
+     * 必须小于队列 job 的 timeout(540 秒),保证请求先超时、由业务层记录错误并重新排队,
      * 而不是被 worker 强杀导致任务长期停留在 processing。
-     * 取值依据(近 3 天成功任务实测):gpt-image-2 生成耗时 p99 ≈ 261~286s、max ≈ 297s,
-     * 取 300s 基本不掐断正常请求(若取 240s 会掐掉 2.4%~5.4%,那部分请求上游往往已经生成
-     * 完成,重试等于重复生成、白花一次费用),并给 job 留 60s 收尾时间,故取 300s。
+     * 取值依据:4K 编辑(3840x2160 / 2160x3840)平均 148~152s、长尾超过 300s;
+     * 批量提交时上游排队会把单请求推到 300s 以上,故取 480s 给长尾留出余量,
+     * 并给 job 留 60s 收尾时间。
      */
-    const REQUEST_TIMEOUT_SECONDS = 300;
+    const REQUEST_TIMEOUT_SECONDS = 480;
     /** 连接超时(秒) */
     const REQUEST_CONNECT_TIMEOUT_SECONDS = 30;
     /** extra_params 中记录重试信息的键名 */
@@ -435,12 +435,15 @@ class AIImageGenerationService
      */
     public function submitTaskToGptImage2Api(MpGeneratePicTask $task): void
     {
-        try {
-            // 上游任务/请求 ID:百度渠道在响应头中返回,成功或失败都尽量记录到 result_json
-            $upstreamIds = [];
-            // 本次上游返回的响应体(失败/重试路径用于保留原始结果并补记 token)
-            $lastResponseData = null;
+        // 上游任务/请求 ID:百度渠道在响应头中返回,成功或失败都尽量记录到 result_json
+        $upstreamIds = [];
+        // 本次上游返回的响应体(失败/重试路径用于保留原始结果并补记 token)
+        $lastResponseData = null;
+        // 本次上游返回的响应头(编辑模式 cURL / 文生图 Guzzle 均采集),
+        // 超时或异常时写入 result_json,用于判断该次是否已拿到上游任务/请求 ID
+        $rawResponseHeaders = [];
 
+        try {
             // 渠道判定:baidu- 前缀走「百度生成模型服务V2」(gpt-image-2 / 2.5 系),其余走现有快快渠道
             $isBaiduChannel = strpos((string)$task->model, 'baidu-') === 0;
             // 上游模型名不带渠道前缀:表里存 baidu-gpt-image-2,调用上游用 gpt-image-2
@@ -490,13 +493,17 @@ class AIImageGenerationService
             
             // 创建专用客户端,优化配置
             $gptClient = new Client([
-                // 超时必须小于队列 job 的 timeout(300s):请求先超时并抛出异常,
+                // 超时必须小于队列 job 的 timeout(540s):请求先超时并抛出异常,
                 // 由业务层记录错误并重新排队,避免 worker 强杀导致任务长期悬挂
                 'timeout' => self::REQUEST_TIMEOUT_SECONDS,
                 'connect_timeout' => self::REQUEST_CONNECT_TIMEOUT_SECONDS,
                 'http_errors' => false, // 不自动抛出HTTP错误
                 'verify' => false, // 禁用SSL验证(如果有证书问题)
                 'allow_redirects' => true,
+                // 响应头到达即采集,body 卡住超时时也能记录到上游任务/请求 ID
+                'on_headers' => function (\Psr\Http\Message\ResponseInterface $response) use (&$rawResponseHeaders) {
+                    $rawResponseHeaders = $response->getHeaders();
+                },
             ]);
             
             // 根据是否有参考图选择不同的请求方式
@@ -617,9 +624,7 @@ class AIImageGenerationService
                 
                 $startTime = microtime(true);
                 
-                // 使用原生 cURL
-                // 采集响应头(百度上游任务 ID 在其中),成功与失败响应都需要
-                $rawResponseHeaders = [];
+                // 使用原生 cURL;响应头由方法上方统一声明的 $rawResponseHeaders 采集
                 $ch = curl_init();
                 curl_setopt_array($ch, [
                     CURLOPT_URL => $api_url,
@@ -633,7 +638,7 @@ class AIImageGenerationService
                         'Content-Length: ' . strlen($postFields),
                         'Expect:', // 移除 Expect: 100-continue,避免等待
                     ],
-                    CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT_SECONDS, // 与 job timeout(300s) 对齐,先超时再落状态
+                    CURLOPT_TIMEOUT => self::REQUEST_TIMEOUT_SECONDS, // 与 job timeout(540s) 对齐,先超时再落状态
                     CURLOPT_CONNECTTIMEOUT => self::REQUEST_CONNECT_TIMEOUT_SECONDS,
                     CURLOPT_SSL_VERIFYPEER => false,
                     CURLOPT_SSL_VERIFYHOST => false,
@@ -783,13 +788,13 @@ class AIImageGenerationService
 
             if ($this->isRetryableApiError($e)) {
                 // 超时/连接失败等可重试错误:先记录上游返回结果/token,再放回队列尾部重试
-                $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务失败重试,仅记录token不扣积分');
+                $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务失败重试,仅记录token不扣积分', $rawResponseHeaders);
                 $this->requeueTaskForRetry($task, $errorMessage, $failResultJson);
                 return;
             }
 
             // 最终失败:同样记录上游返回结果与 token(失败不扣积分)
-            $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务最终失败,仅记录token不扣积分');
+            $failResultJson = $this->captureFailedAttempt($task, $lastResponseData, $upstreamIds, '图片任务最终失败,仅记录token不扣积分', $rawResponseHeaders);
             $failData = ['error_message' => $errorMessage];
             if (!empty($failResultJson)) {
                 $failData['result_json'] = $failResultJson;
@@ -990,6 +995,11 @@ class AIImageGenerationService
             // 上游任务/请求 ID 随 result_json 一并落库,供月度消耗比对
             $cleanedResponseData = $this->mergeUpstreamIds($cleanedResponseData, $upstreamIds);
 
+            // 保留此前重试失败时留下的超时响应头,任务最终成功也能回溯该次是否拿到上游 ID
+            if (is_array($task->result_json) && !empty($task->result_json['raw_response_headers'])) {
+                $cleanedResponseData['raw_response_headers'] = $task->result_json['raw_response_headers'];
+            }
+
             // 图片生成成功统一处理(事务:状态更新 + 计费 + 分镜同步)
             $this->handleImageTaskSuccess($task, $result_urls, $cleanedResponseData);
         } catch (\Exception $e) {
@@ -2638,19 +2648,32 @@ class AIImageGenerationService
      * @param mixed $responseData 上游响应体(数组或 null)
      * @param array $upstreamIds 上游响应头中解析出的任务/请求 ID
      * @param string $remark 备注
+     * @param array $rawResponseHeaders 本次收到的原始响应头(超时/请求中断时留痕)
      * @return array 写入 result_json 的内容
      */
-    private function captureFailedAttempt(MpGeneratePicTask $task, $responseData, array $upstreamIds, string $remark): array
+    private function captureFailedAttempt(MpGeneratePicTask $task, $responseData, array $upstreamIds, string $remark, array $rawResponseHeaders = []): array
     {
         $resultJson = [];
         try {
             $resultJson = is_array($responseData) ? $this->removeB64JsonFromResponse($responseData) : [];
-            $resultJson = $this->mergeUpstreamIds($resultJson, $upstreamIds);
 
             // 本次没有响应体时,保留此前已记录的 result_json,避免用空值覆盖
             if (empty($resultJson)) {
                 $resultJson = is_array($task->result_json) ? $task->result_json : [];
             }
+
+            $resultJson = $this->mergeUpstreamIds($resultJson, $upstreamIds);
+
+            // 超时/请求中断时也留下本次收到的响应头;本次没拿到时沿用此前记录的,
+            // 便于事后判断超时那次到底有没有拿到上游 ID
+            $headersToKeep = !empty($rawResponseHeaders)
+                ? $rawResponseHeaders
+                : (is_array($task->result_json) && !empty($task->result_json['raw_response_headers'])
+                    ? $task->result_json['raw_response_headers']
+                    : []);
+            if (!empty($headersToKeep)) {
+                $resultJson['raw_response_headers'] = $headersToKeep;
+            }
         } catch (\Throwable $e) {
             dLog('generate')->warning('记录失败尝试上游结果异常(不影响token补记): ' . $e->getMessage(), [
                 'task_id' => $task->task_id ?? null,

+ 2 - 1
config/queue.php

@@ -66,7 +66,8 @@ return [
             'driver' => 'redis',
             'connection' => 'default',
             'queue' => env('REDIS_QUEUE', 'default'),
-            'retry_after' => 90,
+            // 图片 job 的 timeout 已提升到 540s,retry_after 必须大于它,避免执行中的 job 被重复投递
+            'retry_after' => 600,
             'block_for' => null,
             'after_commit' => false,
         ],