Skip to content

Signal able commands

macropay-solutions edited this page Jul 22, 2025 · 2 revisions

The commands as opposed to jobs, are not by default signal aware so, in a deploy or scale down scenario the long running commands may be killed before they finish.

Note that this tutorial will check for signals in the middle of the command not after the command has finished like in the job's case.

To make the commands signal aware they must implement Symfony\Component\Console\Command\SignalableCommandInterface.

  • Start by adding this class to your project and extend it into all your commands (or only in the long running ones):
<?php

namespace App\Console\Commands;

use App\Exceptions\SignalableCommandException;
use Illuminate\Console\Command;
use Symfony\Component\Console\Command\SignalableCommandInterface;

abstract class SignalableCommand extends Command implements SignalableCommandInterface
{
    protected bool $shouldStop = false;
    protected string $signalReceived = '';
    protected array $subscribedSignals = [
        'SIGTERM' => SIGTERM,
        'SIGINT' => SIGINT,
        'SIGHUP' => SIGHUP
    ];

    public function getSubscribedSignals(): array
    {
        return $this->subscribedSignals;
    }

    /**
     * @todo If updating from symfony 5.4 update the function definition
     */
     // for symfony 6.4 the definition is public function handleSignal(int $signal, /* int|false $previousExitCode = 0 */): int|false
    public function handleSignal(int $signal): void
    {
        $this->signalReceived = (string)\array_search($signal, $this->subscribedSignals, true);

        if ('' !== $this->signalReceived) {
            $this->shouldStop = true;
        }
    }

    /**
     * @throws SignalableCommandException
     */
    protected function throwIfShouldStop(): void
    {
        if ($this->shouldStop) {
            throw new SignalableCommandException(
                'Signal received ' . $this->signalReceived . '. Stopping artisan command: ' . $this->signature
            );
        }
    }
}
  • Add this exception class
<?php

namespace App\Exceptions;

class SignalableCommandException extends \Exception
{
}
  • Add this test command and registed it in \App\Console\Kernel::$commands
<?php

namespace App\Console\Commands;

use App\Exceptions\SignalableCommandException;
use Illuminate\Support\Facades\Log;

class TestBgLongRunningCommand extends SignalableCommand
{
    /**
     * The name and signature of the console command.
     * @var string
     */
    protected $signature = 'background:test';

    /**
     * The console command description.
     * @var string
     */
    protected $description = 'This command runs for a long period';

    public function handle(): void
    {
        try {
            while (true) {
                Log::error($this->signature);
                \sleep(1);
                $this->throwIfShouldStop();
            }
        } catch (SignalableCommandException $e) {
            Log::error($e->getMessage());
        }
    }
}
  • Add this in your composer.json
        "ext-pcntl": "*"
  • Use this code in your command's handle function inside a loop for example
            try {
                $this->throwIfShouldStop();
            } catch (SignalableCommandException $e) {
                Log::error($e->getMessage());

                return;
            }

or if the loop is outside your command class:

            try {
                $this->service->doSomething([$this, 'throwIfShouldStop']);
            } catch (SignalableCommandException $e) {
                Log::error($e->getMessage());

                return;
            }

and in the service:

public function doSomething(callable $throwIfShouldStop): void
{ 
    //loop
    throwIfShouldStop();
    ...
}