error('实时推送未启用(ADMIN_WS_ENABLED=false)'); return self::FAILURE; } $owner = Str::random(16); // 上个进程被强杀时锁要等 LOCK_TTL 才过期,这里等待而不是立即退出,避免 Supervisor 连续重启判定失败 for ($waited = 0; !Cache::add(self::LOCK, $owner, self::LOCK_TTL); $waited++) { if ($waited >= self::LOCK_TTL + 5 || $this->option('once')) { $this->error('已有 relay 实例在运行'); return self::FAILURE; } sleep(1); } DB::connection()->disableQueryLog(); $this->trap(fn() => [SIGTERM, SIGINT, SIGQUIT], function () { $this->running = false; }); $deadline = time() + max(60, (int) $this->option('max-seconds')); $pollMs = max(200, (int) config('admin_realtime.poll_ms', 1000)); $cursor = AdminRealtimeFeed::startCursor(); $nextSweep = 0; $this->info("relay 启动,游标 {$cursor}"); try { while ($this->running) { if (Cache::get(self::LOCK) !== $owner) { $this->error('relay 锁已失效,退出以免重复广播'); return self::FAILURE; } Cache::put(self::LOCK, $owner, self::LOCK_TTL); $hasMore = false; try { $batch = AdminRealtimeFeed::next($cursor); $cursor = $batch['cursor']; $hasMore = $batch['has_more']; foreach ($batch['events'] as $message) { $this->broadcast($message); } if (time() >= $nextSweep) { AdminRealtimeAccess::terminateExpiredSessions(); $nextSweep = time() + self::SESSION_SWEEP_SECONDS; } } catch (\Throwable $e) { // 数据库等临时故障:记录后稍后重试,已分配 seq 的事件前端可凭补发接口追回 Log::channel('realtime')->error('admin_realtime_relay_failed', ['cursor' => $cursor, 'error' => $e->getMessage()]); sleep(min(10, (int) ceil($pollMs * 5 / 1000))); } if ($this->option('once') || time() >= $deadline) { break; } if (!$hasMore) { usleep($pollMs * 1000); } } } finally { if (Cache::get(self::LOCK) === $owner) { Cache::forget(self::LOCK); } } return self::SUCCESS; } /** 单条广播失败(Reverb 不可用、消息过大)只记录,前端重连后经补发接口按 seq 追回 */ private function broadcast(array $message): void { try { Broadcast::queue(new AdminRealtimeBroadcast($message)); } catch (\Throwable $e) { Log::channel('realtime')->error('admin_realtime_broadcast_failed', [ 'seq' => $message['seq'], 'channel' => $message['channel'], 'error' => $e->getMessage(), ]); } } }