Aborting and resuming a Symfony Console command
Asked Answered
D

3

14

I have a Symfony Console command that iterates over a potentially big collection of items and does a task with each of them. Since the collection can be big, the command can take a long time to run (hours). Once the command finishes, it displays some statistics.

I'd like to make it possible to abort the command in a nice way. Right now if I abort it (ie with ctrl+c in the CLI), there is no statistics summary and no way to output the parameters needed to resume the command. Another issue is that the command might be terminated in the middle of handling an item - it'd be better if it could only terminate in between handling items.

So is there a way to tell a command to "abort nicely as soon as possible", or have the ctrl+c command be interpreted as such?

I tried using the ConsoleEvents::TERMINATE event, though the handlers for this only get fired on command completion, not when I ctrl+c the thing. And I've not been able to find further info on making such resumable commands.

Dubbing answered 5/5, 2014 at 18:19 Comment(2)
I think by making your input as interactive input you will be able to fix issue, but I don't know how exactly you should implement it so that on specific key press terminate the command and give you the statistics This link might help [davidbu.ch/slides/20130613_techtalk_symfony-console.html] step by step creating interactive commandCordillera
Or you may check the event listener in command and base on that terminate your command [symfony.com/doc/current/components/console/events.html]Cordillera
D
23

This is what worked for me. You need to call pcntl_signal_dispatch before the signal handlers are actually executed. Without it, all tasks will finish first.

<?php
use Symfony\Component\Console\Command\Command;

class YourCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        pcntl_signal(SIGTERM, [$this, 'stopCommand']);
        pcntl_signal(SIGINT, [$this, 'stopCommand']);

        $this->shouldStop = false;

        foreach ( $this->tasks as $task )
        {
            pcntl_signal_dispatch();
            if ( $this->shouldStop ) break; 
            $task->execute();
        }

        $this->showSomeStats($output);
    }

    public function stopCommand()
    {
        $this->shouldStop = true;
    }
}
Dubbing answered 17/5, 2014 at 11:19 Comment(4)
Thats what I'm looking for, thanks, but one hint: You have to set declare(ticks = 1); php.net/manual/en/…Hallette
Thanks, since PHP 7.1 you can also turn on asynchronous signal handling by execution pcntl_async_signals(true); php.net/manual/en/function.pcntl-async-signals.phpBoehike
@Hallette You don't need declare(ticks=1) if you're using pcntl_signal_dispatch() in the loop, or pcntl_async_signals(true).Eli
This is now supported in Symfony by implementing the SignalableCommandInterface symfony.com/doc/current/components/console/…Lorrainelorrayne
O
3

You should take a look at RabbitMqBundle's signal handling. Its execute method just links some callbacks via the pcntl_signal() function call. A common case should look pretty much like this:

<?php
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand as Command;

class YourCommand extends Command
{
    protected function execute(InputInterface $input, OutputInterface $output)
    {
        pcntl_signal(SIGTERM, array(&$this, 'stopCommand', $output));
        pcntl_signal(SIGINT, array(&$this, 'stopCommand', $output));
        pcntl_signal(SIGHUP, array(&$this, 'restartCommand', $output));

        // The real execute method body
    }

    public function stopCommand(OutputInterface $output)
    {
        $output->writeln('Stopping');

        // Do what you need to stop your process
    }

    public function restartCommand(OutputInterface $output)
    {
        $output->writeln('Restarting');

        // Do what you need to restart your process
    }
}
Orb answered 6/5, 2014 at 11:32 Comment(2)
Thanks a lot, this and the above comments made me find the solution. You example is missing a call to pcntl_signal_dispatch and also has a syntax error: a callback array only has two elements. You cannot stuff in arguments after those.Dubbing
@JeroenDeDauw, oops, this was some pseudo-code, my bad :)Orb
C
2

The answers are more complex than they need to be. Sure, you can register POSIX signal handlers, but if the only signals that need to be handled are basic interrupts and the like, you should just define a destructor on the Command.


class YourCommand extends Command
{
    // Other code goes here.

    __destruct()
    {
        $this->shouldStop = true;
    }
}

A case where you would want to register a POSIX signal is for the SIGCONT signal, which can handle the resumption of a process that was stopped (SIGSTOP).

Another case would be where you want every signal to behave differently; for the most part, though, SIGINT and SIGTERM and a handful of others would be registered with the same "OMG THE PROCESS HAS BEEN KILLED" operation.

Aside from these examples, registering signal events is unnecessary. This is why destructors exist.

You can even extend Symfony's base Command class with a __destruct method, which would automatically provide cleanup for every command; should a particular command require additional operations, just overwrite it.

Casuistry answered 13/11, 2015 at 16:50 Comment(2)
I like this approach, but it's worth noting that because of the way dependency injection works, __destruct() will be called at the end of every command. Perhaps that wasn't true back when you wrote this (I don't know) but I think it's an important thing for people to know.Bhagavadgita
I'm not sure if __destruct is called if the process is killed with CTRL+CWrapper

© 2022 - 2024 — McMap. All rights reserved.