Skip to content

Messenger Supervisor graceful shutdown #41906

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
AymDev opened this issue Jun 29, 2021 · 2 comments
Closed

Messenger Supervisor graceful shutdown #41906

AymDev opened this issue Jun 29, 2021 · 2 comments

Comments

@AymDev
Copy link

AymDev commented Jun 29, 2021

Description:

I have a proposition to perform a graceful shutdown of the messenger:consume command with Supervisor, especially useful when the Symfony application is in a Docker container.

Given a Docker image which entrypoint uses supervisord, stopping it sends a SIGTERM signal to Supervisor which sends a SIGTERM signal to the programs (the messenger:consume commands), stopping the processes immediately. The issue I see here is that it could stop a message from being processed completely by its handler, thus loosing a message per worker process.

Goal: make the messenger:consume command finish processing its current message before exiting.

Failing workarounds:

Using a bind mount for the source code and calling messenger:stop-workers implies we can't update the Docker image itself.
Calling messenger:stop-workers before shutting down the container does not work because the command returns as soon as the "signal" to stop the workers is sent, therefore Docker/Supervisor will kill the processes even if a message is currently being processed.
Editing the supervisor.conf file on the fly (e.g. using sed) to set autorestart=false and then calling supervisorctl to reread and update does not work as supervisorctl update kills and restarts the processes immediately.
And I tried a lot more of failing workarounds I just forgot about.

Example of a working solution:

Symfony 5.2 introduced the SignalableCommandInterface to allow commands to react to signals.
I created a clone of the ConsumeMessagesCommand implementing this feature:

<?php

declare(strict_types=1);

namespace App\Messenger;

use Symfony\Component\Console\Command\SignalableCommandInterface;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Messenger\Command\ConsumeMessagesCommand;

final class ConsumerCommand extends ConsumeMessagesCommand implements SignalableCommandInterface
{
    protected static $defaultName = 'app:messenger:consume';

    /**
     * The command will react to a SIGUSR1 signal
     */
    public function getSubscribedSignals(): array
    {
        return [
            SIGUSR1
        ];
    }

    /**
     *  When the SIGUSR1 signal is received, the `messenger:stop-workers` command is run
     */
    public function handleSignal(int $signal): void
    {
        $command = $this->getApplication()->find('messenger:stop-workers');
        $command->run(new ArrayInput([]), new NullOutput());
    }
}

This service has been based on the original command as such:

services:
    App\Messenger\ConsumerCommand:
        parent: 'console.command.messenger_consume_messages'

Only this is needed in Symfony's codebase to make it work. I tested with a message handler which loops from 0 to 30 and prints a number per second (I'm not pasting it here to avoid making this issue longer): when calling docker-compose down in the middle of a handler's execution it waits for it to finish before exiting.

Then comes the (shortened) Supervisor configuration to make use of it using the stopsignal (which signal to send to the program) and stopwaitsecs (how long is the timeout before a SIGKILL is sent):

[program:messenger-my_kafka_consumer]
command=php /var/www/html/bin/console app:messenger:consume my_transport -vvv
stopsignal=USR1
stopwaitsecs=20

About Docker, if the shutdown is specified in a CLI command, there is a -t option for both docker stop and docker-compose down commands to define a timeout between the SIGTERM and SIGKILL sent to Supervisor.
If using a docker-compose.yml file, the stop_grace_period key can be used instead.

The users would just have to set the grace period themselves depending on their message handlers, Symfony wouldn't be responsible for it. If the users don't want a graceful shutdown they just don't set a grace period nor specific signal.

Questions:

I think the messenger:consume command could implement SignalableCommandInterface, and that the Docker/Supervisor configuration suggestion could be added to the documentation.

Is this feature acceptable ?
I used SIGUSR1 but would SIGTERM be acceptable ? I can't think about any cases where someone would like to get the message processing being stop in the middle, but I may be wrong.

I'd love to make the PR myself for this, should I target a specific branch as the interface was released in 5.2 and Messenger currently requires the Console component in "^4.4|^5.0" versions ? Do you have any hints for me to write a decent test for signal handling of the command ?

Thanks for reading this long issue :-)

@quazardous
Copy link

Supervisor which sends a SIGTERM signal to the programs (the messenger:consume commands), stopping the processes immediately.

I think this is wrongly assumed (at least in 5.3).

vendor/symfony/messenger/EventListener/StopWorkerOnSigtermSignalListener.php defines

pcntl_signal(\SIGTERM, static function () use ($event) {
    $event->getWorker()->stop();
});

and stop() is basically just a flag that the main loop takes into account after it has handled a message.

It's the same flag that is used with --limit* options. So in my comprehension (?) no message is lost.

In my case I needed to interrupt long running jobs so I've tweak it like:

pcntl_signal(\SIGTERM, static function () use ($event, $jobManager) {
    $event->getWorker()->stop();
    $jobManager->stop(); // tell the sub system that it would be cool to let go when possible
});

But effectively there is something to do with supervisor regarding exit codes #42029

@AymDev
Copy link
Author

AymDev commented Jul 18, 2021

I made tests and it appears to be working correctly as long as the pcntl extension is installed, good news !

But still, I think we could add a short section in the documentation. I'm closing this issue and I'll try to make a PR on the docs repo. Thank you @quazardous !

@AymDev AymDev closed this as completed Jul 18, 2021
javiereguiluz added a commit to symfony/symfony-docs that referenced this issue Jul 20, 2021
This PR was squashed before being merged into the 4.4 branch.

Discussion
----------

[Messenger] Supervisor graceful shutdown

>[Messenger] Add details on graceful shutdown
Added a section to Messenger's documentation on the workers SIGTERM
signal handling when the pcntl extension is installed.

I'm making this PR because I struggled to perform a graceful shutdown as described in symfony/symfony#41906 and I think this could help other users in the future.
This is my first contribution, I'd be happy to rework anything which doesn't meet the documentation standards.

Commits
-------

79d5a07 [Messenger] Supervisor graceful shutdown
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants