ProcessBatchEpisodeGenerationCommand.php 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. <?php
  2. namespace App\Console\Commands;
  3. use Illuminate\Console\Command;
  4. use Illuminate\Support\Facades\DB;
  5. use Symfony\Component\Process\Process;
  6. /**
  7. * 批量生成分集定时任务(调度器)
  8. * 每次执行:找出有 pending 任务的 anime_id,为最多 N 个(默认5)空闲 anime 各拉起一个后台子进程,
  9. * 由子进程独占处理对应 anime 的待生成剧集。
  10. */
  11. class ProcessBatchEpisodeGenerationCommand extends Command
  12. {
  13. /**
  14. * The name and signature of the console command.
  15. *
  16. * @var string
  17. */
  18. protected $signature = 'batch:generate-episodes';
  19. /**
  20. * The console command description.
  21. *
  22. * @var string
  23. */
  24. protected $description = '批量生成分集定时任务:并发调度不同anime_id的子进程处理,并发数默认5';
  25. /**
  26. * Execute the console command.
  27. *
  28. * @return int
  29. */
  30. public function handle()
  31. {
  32. dLog('command')->info('开始执行批量生成分集调度任务...');
  33. $concurrency = (int) env('BATCH_EPISODE_GENERATION_CONCURRENCY', 5);
  34. if ($concurrency < 1) {
  35. $concurrency = 1;
  36. }
  37. // 找出所有有待处理任务的 anime_id
  38. $animeIds = DB::table('mp_batch_episode_generation_details')
  39. ->where('status', 'pending')
  40. ->distinct()
  41. ->pluck('anime_id')
  42. ->map(function ($animeId) {
  43. return (int) $animeId;
  44. })
  45. ->sort()
  46. ->values()
  47. ->toArray();
  48. if (empty($animeIds)) {
  49. dLog('command')->info('没有待处理的任务,本次调度结束');
  50. return 0;
  51. }
  52. $spawned = 0;
  53. foreach ($animeIds as $animeId) {
  54. if ($spawned >= $concurrency) {
  55. dLog('command')->info("已达到最大并发数 {$concurrency},停止调度");
  56. break;
  57. }
  58. $lockName = ProcessBatchAnimeEpisodeGenerationCommand::LOCK_PREFIX . $animeId;
  59. // 仅当该 anime 的处理锁空闲时才拉起子进程;
  60. // 真正的加锁由子进程完成,避免锁挂在调度进程的连接上
  61. $isFree = DB::selectOne('SELECT IS_FREE_LOCK(?) AS is_free', [$lockName]);
  62. if (!$isFree || (int) $isFree->is_free !== 1) {
  63. dLog('command')->info("anime_id: {$animeId} 正在被其他进程处理,跳过");
  64. continue;
  65. }
  66. try {
  67. $this->spawnWorker($animeId);
  68. $spawned++;
  69. dLog('command')->info("已拉起子进程处理 anime_id: {$animeId}");
  70. } catch (\Throwable $e) {
  71. dLog('command')->error("拉起子进程失败 anime_id: {$animeId}: " . $e->getMessage());
  72. logDB('command', 'error', '批量分集调度:拉起子进程失败', [
  73. 'anime_id' => $animeId,
  74. 'error' => $e->getMessage(),
  75. 'trace' => $e->getTraceAsString()
  76. ]);
  77. }
  78. }
  79. dLog('command')->info("批量生成分集调度完成,本次拉起子进程数: {$spawned}");
  80. return 0;
  81. }
  82. /**
  83. * 拉起一个后台子进程,独占处理指定 anime_id 的待生成剧集
  84. *
  85. * @param int $animeId
  86. * @return void
  87. */
  88. private function spawnWorker($animeId)
  89. {
  90. $process = new Process([
  91. PHP_BINARY,
  92. base_path('artisan'),
  93. 'batch:generate-episodes:anime',
  94. (string) $animeId,
  95. ]);
  96. $process->setTimeout(null);
  97. $process->setIdleTimeout(null);
  98. $process->disableOutput();
  99. $process->start();
  100. }
  101. }