RelayAdminRealtimeEvents.php 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118
  1. <?php
  2. namespace App\Console\Commands;
  3. use App\Events\AdminRealtimeBroadcast;
  4. use App\Services\AdminRealtime\AdminRealtimeAccess;
  5. use App\Services\AdminRealtime\AdminRealtimeFeed;
  6. use Illuminate\Console\Command;
  7. use Illuminate\Support\Facades\Broadcast;
  8. use Illuminate\Support\Facades\Cache;
  9. use Illuminate\Support\Facades\DB;
  10. use Illuminate\Support\Facades\Log;
  11. use Illuminate\Support\Str;
  12. /**
  13. * 常驻进程:把 admin_realtime_events 中的事件解析成列表行并广播到 Reverb,
  14. * 同时扫描会员上下线、断开 JWT 已过期的后台连接。需由 Supervisor 守护,全局只运行一个实例。
  15. */
  16. class RelayAdminRealtimeEvents extends Command
  17. {
  18. private const LOCK = 'admin_realtime:relay';
  19. private const LOCK_TTL = 30;
  20. private const SESSION_SWEEP_SECONDS = 5;
  21. protected $signature = 'admin-realtime:relay
  22. {--once : 只处理一批后退出,用于排查}
  23. {--max-seconds=3600 : 运行指定秒数后正常退出,由 Supervisor 拉起新进程释放内存}';
  24. protected $description = '后台实时推送:读取事件表并广播到 Reverb';
  25. private bool $running = true;
  26. public function handle(): int
  27. {
  28. if (!config('admin_realtime.enabled')) {
  29. $this->error('实时推送未启用(ADMIN_WS_ENABLED=false)');
  30. return self::FAILURE;
  31. }
  32. $owner = Str::random(16);
  33. // 上个进程被强杀时锁要等 LOCK_TTL 才过期,这里等待而不是立即退出,避免 Supervisor 连续重启判定失败
  34. for ($waited = 0; !Cache::add(self::LOCK, $owner, self::LOCK_TTL); $waited++) {
  35. if ($waited >= self::LOCK_TTL + 5 || $this->option('once')) {
  36. $this->error('已有 relay 实例在运行');
  37. return self::FAILURE;
  38. }
  39. sleep(1);
  40. }
  41. DB::connection()->disableQueryLog();
  42. $this->trap(fn() => [SIGTERM, SIGINT, SIGQUIT], function () {
  43. $this->running = false;
  44. });
  45. $deadline = time() + max(60, (int) $this->option('max-seconds'));
  46. $pollMs = max(200, (int) config('admin_realtime.poll_ms', 1000));
  47. $cursor = AdminRealtimeFeed::startCursor();
  48. $nextSweep = 0;
  49. $this->info("relay 启动,游标 {$cursor}");
  50. try {
  51. while ($this->running) {
  52. if (Cache::get(self::LOCK) !== $owner) {
  53. $this->error('relay 锁已失效,退出以免重复广播');
  54. return self::FAILURE;
  55. }
  56. Cache::put(self::LOCK, $owner, self::LOCK_TTL);
  57. $hasMore = false;
  58. try {
  59. if (time() >= $nextSweep) {
  60. AdminRealtimeAccess::terminateExpiredSessions();
  61. $nextSweep = time() + self::SESSION_SWEEP_SECONDS;
  62. }
  63. $batch = AdminRealtimeFeed::next($cursor);
  64. foreach ($batch['events'] as $message) {
  65. if (Cache::get(self::LOCK) !== $owner) {
  66. throw new \RuntimeException('relay 锁已失效,停止发送当前批次');
  67. }
  68. Cache::put(self::LOCK, $owner, self::LOCK_TTL);
  69. $this->broadcast($message);
  70. }
  71. AdminRealtimeFeed::acknowledge($batch['ids']);
  72. $cursor = $batch['cursor'];
  73. $hasMore = $batch['has_more'];
  74. } catch (\Throwable $e) {
  75. // 未确认的批次留在数据库中;稍后或进程重启后先重试,不越过失败事件。
  76. Log::channel('realtime')->error('admin_realtime_relay_failed', ['cursor' => $cursor, 'error' => $e->getMessage()]);
  77. if ($this->option('once')) return self::FAILURE;
  78. sleep(min(10, (int) ceil($pollMs * 5 / 1000)));
  79. }
  80. if ($this->option('once') || time() >= $deadline) {
  81. break;
  82. }
  83. if (!$hasMore) {
  84. usleep($pollMs * 1000);
  85. }
  86. }
  87. } finally {
  88. if (Cache::get(self::LOCK) === $owner) {
  89. Cache::forget(self::LOCK);
  90. }
  91. }
  92. return self::SUCCESS;
  93. }
  94. /** 广播失败必须中断批次,保留其发送状态供重试。 */
  95. private function broadcast(array $message): void
  96. {
  97. try {
  98. Broadcast::queue(new AdminRealtimeBroadcast($message));
  99. } catch (\Throwable $e) {
  100. Log::channel('realtime')->error('admin_realtime_broadcast_failed', [
  101. 'seq' => $message['seq'], 'channel' => $message['channel'], 'error' => $e->getMessage(),
  102. ]);
  103. throw $e;
  104. }
  105. }
  106. }