diff --git a/app/Console/Commands/HorizonManage.php b/app/Console/Commands/HorizonManage.php new file mode 100644 index 000000000..611b516b0 --- /dev/null +++ b/app/Console/Commands/HorizonManage.php @@ -0,0 +1,127 @@ + 'Pending Jobs', + 'running' => 'Running Jobs', + 'workers' => 'Workers', + 'failed' => 'Failed Jobs', + 'failed-delete' => 'Failed Jobs - Delete', + ] + ); + + if ($action === 'pending') { + $pendingJobs = app(JobRepository::class)->getPending(); + $pendingJobsTable = []; + if (count($pendingJobs) === 0) { + $this->info('No pending jobs found.'); + + return; + } + foreach ($pendingJobs as $pendingJob) { + $pendingJobsTable[] = [ + 'id' => $pendingJob->id, + 'name' => $pendingJob->name, + 'status' => $pendingJob->status, + 'reserved_at' => $pendingJob->reserved_at ? now()->parse($pendingJob->reserved_at)->format('Y-m-d H:i:s') : null, + ]; + } + table($pendingJobsTable); + } + + if ($action === 'failed') { + $failedJobs = app(JobRepository::class)->getFailed(); + $failedJobsTable = []; + if (count($failedJobs) === 0) { + $this->info('No failed jobs found.'); + + return; + } + foreach ($failedJobs as $failedJob) { + $failedJobsTable[] = [ + 'id' => $failedJob->id, + 'name' => $failedJob->name, + 'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null, + ]; + } + table($failedJobsTable); + } + + if ($action === 'failed-delete') { + $failedJobs = app(JobRepository::class)->getFailed(); + $failedJobsTable = []; + foreach ($failedJobs as $failedJob) { + $failedJobsTable[] = [ + 'id' => $failedJob->id, + 'name' => $failedJob->name, + 'failed_at' => $failedJob->failed_at ? now()->parse($failedJob->failed_at)->format('Y-m-d H:i:s') : null, + ]; + } + app(MetricsRepository::class)->clear(); + if (count($failedJobsTable) === 0) { + $this->info('No failed jobs found.'); + + return; + } + $jobIds = multiselect( + label: 'Which job to delete?', + options: collect($failedJobsTable)->mapWithKeys(fn ($job) => [$job['id'] => $job['id'].' - '.$job['name']])->toArray(), + ); + foreach ($jobIds as $jobId) { + Artisan::queue('horizon:forget', ['id' => $jobId]); + } + } + + if ($action === 'running') { + $redisJobRepository = app(CustomJobRepository::class); + $runningJobs = $redisJobRepository->getReservedJobs(); + $runningJobsTable = []; + if (count($runningJobs) === 0) { + $this->info('No running jobs found.'); + + return; + } + dump($runningJobs); + foreach ($runningJobs as $runningJob) { + $runningJobsTable[] = [ + 'id' => $runningJob->id, + 'name' => $runningJob->name, + ]; + } + table($runningJobsTable); + } + + if ($action === 'workers') { + $redisJobRepository = app(CustomJobRepository::class); + $workers = $redisJobRepository->getHorizonWorkers(); + $workersTable = []; + foreach ($workers as $worker) { + $workersTable[] = [ + 'name' => $worker->name, + ]; + } + table($workersTable); + } + } +} diff --git a/app/Contracts/CustomJobRepositoryInterface.php b/app/Contracts/CustomJobRepositoryInterface.php new file mode 100644 index 000000000..689b1897f --- /dev/null +++ b/app/Contracts/CustomJobRepositoryInterface.php @@ -0,0 +1,24 @@ +app->singleton(JobRepository::class, CustomJobRepository::class); + $this->app->singleton(CustomJobRepositoryInterface::class, CustomJobRepository::class); + } + + /** + * Bootstrap services. */ public function boot(): void { - parent::boot(); - - // Horizon::routeSmsNotificationsTo('15556667777'); - // Horizon::routeMailNotificationsTo('example@example.com'); - // Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel'); - - Horizon::night(); - } - - /** - * Register the Horizon gate. - * - * This gate determines who can access Horizon in non-local environments. - */ - protected function gate(): void - { - Gate::define('viewHorizon', function ($user) { - $root_user = User::find(0); - - return in_array($user->email, [ - $root_user->email, - ]); - }); + // } } diff --git a/app/Repositories/CustomJobRepository.php b/app/Repositories/CustomJobRepository.php new file mode 100644 index 000000000..ef492e3ba --- /dev/null +++ b/app/Repositories/CustomJobRepository.php @@ -0,0 +1,71 @@ +all(); + } + + public function getReservedJobs(): Collection + { + return $this->getJobsByStatus('reserved'); + } + + /** + * Get all jobs with a specific status. + */ + public function getJobsByStatus(string $status, ?string $worker = null): Collection + { + $jobs = new Collection; + + $this->getRecent()->each(function ($job) use ($jobs, $status, $worker) { + if ($job->status === $status) { + if ($worker) { + dump($job); + if ($job->worker !== $worker) { + return; + } + } + $jobs->push($job); + } + }); + + return $jobs; + } + + /** + * Get the count of jobs with a specific status. + */ + public function countJobsByStatus(string $status): int + { + return $this->getJobsByStatus($status)->count(); + } + + /** + * Get jobs that have been running longer than a specified duration in seconds. + */ + public function getLongRunningJobs(int $seconds): Collection + { + $jobs = new Collection; + + $this->getRecent()->each(function ($job) use ($jobs, $seconds) { + if ($job->status === 'reserved' && + isset($job->reserved_at) && + (time() - strtotime($job->reserved_at)) > $seconds) { + $jobs->push($job); + } + }); + + return $jobs; + } +}