Skip to content
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

[Console] Add AlarmableCommandInterface and console.alarm event #53533

Open
wants to merge 1 commit into
base: 7.2
Choose a base branch
from

Conversation

HypeMC
Copy link
Contributor

@HypeMC HypeMC commented Jan 13, 2024

Q A
Branch? 7.1
Bug fix? no
New feature? yes
Deprecations? no
Issues Fix #47920
License MIT

Part of #53508

This PR introduces a new AlarmableCommandInterface and a console.alarm event. A command implementing this interface will automatically subscribe to the SIGALRM signal and dispatch an alarm at the specified interval:

#[AsCommand('app:alarm')]
class AlarmCommandCopy extends Command implements AlarmableCommandInterface, SignalableCommandInterface
{
    private bool $run = true;
    private int $count = 0;
    private OutputInterface $output;

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $this->output = $output;

        while ($this->run) {
            $this->count++;
            $output->writeln('execute '.$this->count);
            sleep(1);
        }

        $output->writeln('Done');

        return Command::SUCCESS;
    }

    public function getAlarmInterval(InputInterface $input): int
    {
        return 10;
    }

    public function handleAlarm(int|false $previousExitCode = 0): false|int
    {
        $this->count = 0;
        $this->output->writeln('handleAlarm');

        return false;
    }

    public function getSubscribedSignals(): array
    {
        return [\SIGINT];
    }

    public function handleSignal(int $signal, false|int $previousExitCode = 0): int|false
    {
        if (\SIGINT === $signal) {
            $this->run = false;
        }

        return false;
    }
}

When a command implements both the AlarmableCommandInterface and the SignalableCommandInterface, the handleSignal() method will not be called for a SIGALRM signal.

The console.alarm event is dispatched on every SIGALRM signal:

#[AsEventListener(event: 'console.alarm')]
final class ConsoleAlarmListener
{
    public function __invoke(ConsoleAlarmEvent $event): void
    {
        $event->getOutput()->writeln('ConsoleAlarmListener');
    }
}

*/
public function getSubscribedSignals(): array;
public function getSubscribedSignals(/* InputInterface $input, OutputInterface $output */): array;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you cannot add mandatory arguments in a method signature through such BC layer, because the child class is not allowed to add them as mandatory arguments.

Btw, changing the subscribed signals based on the output looks weird to me. What is the use case for it ? Is this worth a BC break ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The more I thought about it, the less sense it made to me as well. I initially had a use case for #53508, but I've since found a better approach, and now I can't really think of a good use case. Therefore, I've removed the entire commit. If an actual use case comes up in the future, it can be added then. I've also removed the $output argument from getAlarmTime() as it was also useless.

src/Symfony/Component/Console/Command/TraceableCommand.php Outdated Show resolved Hide resolved
}

$commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals($input, $output) : [];
if ($command instanceof AlarmableCommandInterface && \defined('SIGALRM') && SignalRegistry::isSupported() && !\in_array(\SIGALRM, $commandSignals, true)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Making the TraceableCommand implement AlarmableCommandInterface means that in dev, all commands will register this SIGALRM (even when they are not alarmable). Is this expected ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, good catch, I've totally missed that one. I've added a TraceableAlarmableCommand, so this should no longer be an issue. I'm not sure if this is the best approach, but I couldn't think of any other.

// If the command is signalable, we call the handleSignal() method
if (\in_array($signal, $commandSignals, true)) {
elseif (\in_array($signal, $commandSignals, true)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this will never be called in dev when a SignalableCommand asks for SIGALRM because of the TraceableCommand wrapping it and implementing AlarmableCommandInterface. This means this PR is currently a BC break.

$this->signalRegistry->register($signal, function (int $signal) use ($command): void {
if (false !== $exitCode = $command->handleSignal($signal)) {
$this->signalRegistry->register($signal, function (int $signal) use ($command, $input, $output): void {
if (\SIGALRM === $signal && $command instanceof AlarmableCommandInterface) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue here regarding TraceableCommand.

use Symfony\Component\Console\Output\OutputInterface;

/**
* Interface for command that dispatches SIGALRM signal.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the command actually dispatching SIGALRM signals ? The rest of the code gives me the impression that it listens to those signals.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@HypeMC HypeMC force-pushed the add-alarmable-command branch 2 times, most recently from 3496c6f to 26d9c59 Compare February 25, 2024 13:45
@valtzu
Copy link
Contributor

valtzu commented May 7, 2024

Btw, SIGALRM has the same effect as any other signal – it interrupts sleep & usleep. According to docs, one should check based on return code if the sleep was interrupted but I doubt many do that – and usleep returns void so for that it's impossible.

So if a command logic f.e. includes sleep(30); or $clock->sleep(30);, it will only sleep until next alarm, potentially causing unexpected behavior.


Another (old) thing I found that using SIGALRM to provide timer functionality may result in deadlock – not sure if it's still relevant (or if it ever was, for php).

@ro0NL
Copy link
Contributor

ro0NL commented May 8, 2024

from the original issue, it was taken into account:

    $left = sleep(10);
    while ($left > 0) {
        $left = sleep($left);
    }

i think this is a reasonable workaround (but needs documentation), even if it's not supported on windows (that's a PHP API issue/bug), and should not block this PR

for Linux, we could even fix/abstract it in $clock->sleep() (as per above example), either optionally or out-of-the-box

@xabbuh xabbuh modified the milestones: 7.1, 7.2 May 15, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

[Console] add console.alarm event
7 participants