jobs->getRecent(); return response()->json([ 'success' => true, 'total' => $this->jobs->countRecent(), 'data' => $this->formatJobs($jobs, 'completed'), ]); } /** * GET /api/queue/jobs/pending * Jobs currently waiting in the queue (per-queue workload + Horizon pending list). */ public function pending(): JsonResponse { $jobs = $this->jobs->getPending(); $queues = $this->workload->get(); $workload = collect($queues)->map(fn (array $q) => [ 'queue' => $q['name'], 'length' => $q['length'], 'wait' => $q['wait'], 'processes' => $q['processes'], ])->values(); return response()->json([ 'success' => true, 'total' => $this->jobs->countPending(), 'workload' => $workload, 'data' => $this->formatJobs($jobs, 'pending'), ]); } /** * GET /api/queue/jobs/failed * Failed jobs tracked by Horizon. */ public function failed(): JsonResponse { $jobs = $this->jobs->getFailed(); return response()->json([ 'success' => true, 'total' => $this->jobs->countFailed(), 'data' => $this->formatJobs($jobs, 'failed'), ]); } /** * POST /api/queue/jobs/failed/{id}/retry * Re-queue a failed job by its Horizon job ID. */ public function retry(string $id): JsonResponse { abort_if(!$this->jobs->findFailed($id), 404, "Can't find failed job #{$id}"); dispatch(new RetryFailedJob($id)); return response()->json(['success' => true, 'message' => 'Job đã được đưa lại vào hàng đợi']); } /** * GET /api/queue/jobs/metrics * Queue throughput, runtime, and per-queue/per-job breakdown. */ public function metrics(): JsonResponse { $measuredQueues = $this->metrics->measuredQueues(); $measuredJobs = $this->metrics->measuredJobs(); $queueMetrics = collect($measuredQueues)->map(fn (string $queue) => [ 'queue' => $queue, 'throughput' => $this->metrics->throughputForQueue($queue), 'runtime_ms' => round($this->metrics->runtimeForQueue($queue), 2), ])->values(); $jobMetrics = collect($measuredJobs)->map(fn (string $job) => [ 'job' => $job, 'throughput' => $this->metrics->throughputForJob($job), 'runtime_ms' => round($this->metrics->runtimeForJob($job), 2), ])->values(); return response()->json([ 'success' => true, 'data' => [ 'throughput' => $this->metrics->throughput(), 'jobs_processed_per_minute' => $this->metrics->jobsProcessedPerMinute(), 'counts' => [ 'recent' => $this->jobs->countRecent(), 'pending' => $this->jobs->countPending(), 'failed' => $this->jobs->countFailed(), 'completed' => $this->jobs->countCompleted(), 'recently_failed' => $this->jobs->countRecentlyFailed(), ], 'queues' => $queueMetrics, 'jobs' => $jobMetrics, ], ]); } /** * Normalize a collection of Horizon job objects into a consistent array shape. * * @param \Illuminate\Support\Collection $jobs * @param string $defaultStatus * @return \Illuminate\Support\Collection */ private function formatJobs(\Illuminate\Support\Collection $jobs, string $defaultStatus): \Illuminate\Support\Collection { return $jobs->map(function (object $job) use ($defaultStatus) { $payload = isset($job->payload) ? json_decode($job->payload, true) : []; return [ 'id' => $job->id ?? null, 'name' => $payload['displayName'] ?? $job->name ?? null, 'queue' => $job->queue ?? null, 'status' => $job->status ?? $defaultStatus, 'attempts' => $job->attempts ?? 0, 'started_at' => $this->formatTimestamp($job->reserved_at ?? null), 'completed_at' => $this->formatTimestamp($job->completed_at ?? null), 'failed_at' => $this->formatTimestamp($job->failed_at ?? null), 'exception' => $job->exception ?? null, ]; })->values(); } /** * Convert a Unix timestamp to ISO-8601 string, or return null. */ private function formatTimestamp(int|float|string|null $ts): ?string { if ($ts === null || $ts === 0) { return null; } return \Carbon\Carbon::createFromTimestamp((int) $ts)->toIso8601String(); } }