master

laravel/framework

Last updated at: 29/12/2023 09:25

RetryCommand.php

TLDR

The RetryCommand.php file is a class located in the Illuminate\Queue\Console namespace. It is a command-line command used to retry failed jobs in a queue. The command is registered as queue:retry. The file implements the handle method to execute the command, and it also includes several helper methods to retrieve the job IDs, retry the job, reset payload attempts, and refresh the "retry until" timestamp.

Methods

handle

Executes the queue:retry command. It retrieves the job IDs to be retried, pushes them back onto the queue, and dispatches the JobRetryRequested event for each job. It also outputs the progress and status of each retry.

getJobIds

Retrieves the job IDs to be retried based on the provided arguments and options. It can fetch all job IDs, IDs for a specific queue, or IDs within a specified range.

getJobIdsByQueue

Retrieves the job IDs that belong to a specific queue. It checks if the queue.failer object has an ids method to directly retrieve the IDs or uses all to get all failed jobs and filter them by the queue.

getJobIdsByRanges

Retrieves the job IDs within the provided range arguments. It checks the format of each range and generates an array of IDs based on the range.

retryJob

Retries a specific failed job by pushing it to the queue. It retrieves the connection, payload, and queue from the job object and uses the queue object to push the job back onto the queue.

resetAttempts

Resets the payload attempts for a job. It receives the payload as a string, decodes it into an associative array, and checks if the attempts key exists. If it does, the attempts value is set to 0. The modified payload is then encoded back into a string.

refreshRetryUntil

Refreshes the "retry until" timestamp for a job. It receives the payload as a string, decodes it into an associative array, and checks if the command exists in the payload data. If it does, it tries to unserialize the command into an instance. If successful, it calls the retryUntil method on the instance to obtain the retry timestamp or value. The retry value is then added to the payload and encoded back into a string.

Classes

None

<?php

namespace Illuminate\Queue\Console;

use DateTimeInterface;
use Illuminate\Console\Command;
use Illuminate\Contracts\Encryption\Encrypter;
use Illuminate\Queue\Events\JobRetryRequested;
use Illuminate\Support\Arr;
use RuntimeException;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(name: 'queue:retry')]
class RetryCommand extends Command
{
    /**
     * The console command signature.
     *
     * @var string
     */
    protected $signature = 'queue:retry
                            {id?* : The ID of the failed job or "all" to retry all jobs}
                            {--queue= : Retry all of the failed jobs for the specified queue}
                            {--range=* : Range of job IDs (numeric) to be retried (e.g. 1-5)}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Retry a failed queue job';

    /**
     * Execute the console command.
     *
     * @return void
     */
    public function handle()
    {
        $jobsFound = count($ids = $this->getJobIds()) > 0;

        if ($jobsFound) {
            $this->components->info('Pushing failed queue jobs back onto the queue.');
        }

        foreach ($ids as $id) {
            $job = $this->laravel['queue.failer']->find($id);

            if (is_null($job)) {
                $this->components->error("Unable to find failed job with ID [{$id}].");
            } else {
                $this->laravel['events']->dispatch(new JobRetryRequested($job));

                $this->components->task($id, fn () => $this->retryJob($job));

                $this->laravel['queue.failer']->forget($id);
            }
        }

        $jobsFound ? $this->newLine() : $this->components->info('No retryable jobs found.');
    }

    /**
     * Get the job IDs to be retried.
     *
     * @return array
     */
    protected function getJobIds()
    {
        $ids = (array) $this->argument('id');

        if (count($ids) === 1 && $ids[0] === 'all') {
            $failer = $this->laravel['queue.failer'];

            return method_exists($failer, 'ids')
                ? $failer->ids()
                : Arr::pluck($failer->all(), 'id');
        }

        if ($queue = $this->option('queue')) {
            return $this->getJobIdsByQueue($queue);
        }

        if ($ranges = (array) $this->option('range')) {
            $ids = array_merge($ids, $this->getJobIdsByRanges($ranges));
        }

        return array_values(array_filter(array_unique($ids)));
    }

    /**
     * Get the job IDs by queue, if applicable.
     *
     * @param  string  $queue
     * @return array
     */
    protected function getJobIdsByQueue($queue)
    {
        $failer = $this->laravel['queue.failer'];

        $ids = method_exists($failer, 'ids')
            ? $failer->ids($queue)
            : collect($failer->all())
                ->where('queue', $queue)
                ->pluck('id')
                ->toArray();

        if (count($ids) === 0) {
            $this->components->error("Unable to find failed jobs for queue [{$queue}].");
        }

        return $ids;
    }

    /**
     * Get the job IDs ranges, if applicable.
     *
     * @param  array  $ranges
     * @return array
     */
    protected function getJobIdsByRanges(array $ranges)
    {
        $ids = [];

        foreach ($ranges as $range) {
            if (preg_match('/^[0-9]+\-[0-9]+$/', $range)) {
                $ids = array_merge($ids, range(...explode('-', $range)));
            }
        }

        return $ids;
    }

    /**
     * Retry the queue job.
     *
     * @param  \stdClass  $job
     * @return void
     */
    protected function retryJob($job)
    {
        $this->laravel['queue']->connection($job->connection)->pushRaw(
            $this->refreshRetryUntil($this->resetAttempts($job->payload)), $job->queue
        );
    }

    /**
     * Reset the payload attempts.
     *
     * Applicable to Redis and other jobs which store attempts in their payload.
     *
     * @param  string  $payload
     * @return string
     */
    protected function resetAttempts($payload)
    {
        $payload = json_decode($payload, true);

        if (isset($payload['attempts'])) {
            $payload['attempts'] = 0;
        }

        return json_encode($payload);
    }

    /**
     * Refresh the "retry until" timestamp for the job.
     *
     * @param  string  $payload
     * @return string
     *
     * @throws \RuntimeException
     */
    protected function refreshRetryUntil($payload)
    {
        $payload = json_decode($payload, true);

        if (! isset($payload['data']['command'])) {
            return json_encode($payload);
        }

        if (str_starts_with($payload['data']['command'], 'O:')) {
            $instance = unserialize($payload['data']['command']);
        } elseif ($this->laravel->bound(Encrypter::class)) {
            $instance = unserialize($this->laravel->make(Encrypter::class)->decrypt($payload['data']['command']));
        }

        if (! isset($instance)) {
            throw new RuntimeException('Unable to extract job payload.');
        }

        if (is_object($instance) && ! $instance instanceof \__PHP_Incomplete_Class && method_exists($instance, 'retryUntil')) {
            $retryUntil = $instance->retryUntil();

            $payload['retryUntil'] = $retryUntil instanceof DateTimeInterface
                                        ? $retryUntil->getTimestamp()
                                        : $retryUntil;
        }

        return json_encode($payload);
    }
}