Skip to content

v5.0 - Symfony 7 support and refactoring #57

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

Merged
merged 2 commits into from
Jun 21, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 2 additions & 4 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -5,10 +5,8 @@ cache:
- $HOME/.composer/cache/files

php:
- 7.2
- 7.3
- 7.4
- 8.0
- 8.2
- 8.3

before_script:
- COMPOSER_ROOT_VERSION=dev-master composer --prefer-dist install
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -14,7 +14,8 @@ Use composer to include it into your Symfony project:

### What version to use?
Symfony did make some breaking changes, so you should make sure to use a compatible bundle version:
* Version 4.0.* for Symfony 6 and higher
* Version 5.* for Symfony 7 and higher
* Version 4.* for Symfony 6
* Version 3.0.* for Symfony 4 and 5
* Version 2.0.* for Symfony 3
* Version 1.3.* for Symfony 2.8+
13 changes: 13 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Upgrades

Important information about breaking changes and backward incompabilities

## 4.* to 5.0

### `EndlessContainerAwareCommand` removed

Since `ContainerAwareInterface` has been deprecated in Symfony 6.4, programmers are encouraged to use dependency injection instead in the constructor.

Therefore, it makes no sense to keep `EndlessContainerAwareCommand`. If you need to call `EntityManager::clear()` on your doctrine instance inside `EndlessCommand::finishIteration()`, you have to handle this in your code now.

If you need to access any service, inject it in the constructor of your derived class.
14 changes: 8 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
@@ -15,13 +15,15 @@
"issues": "https://github.com/mac-cain13/daemonizable-command/issues"
},
"require": {
"php": ">=8.0",
"symfony/console": "^6.0 || ^7.0",
"symfony/dependency-injection": "^6.0 || ^7.0"
},
"php": ">=8.2",
"symfony/console": "^7.0",
"symfony/dependency-injection": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "^8.0 || ^9.0"
},
"phpunit/phpunit": "^9.0",
"ext-pcntl": "*",
"ext-posix": "*"
},
"suggest": {
"symfony/filesystem": "If you can't use Upstart or systemd"
},
6 changes: 3 additions & 3 deletions examples/ExampleCommand.php
Original file line number Diff line number Diff line change
@@ -2,11 +2,11 @@

namespace Acme\DemoBundle\Command;

use Wrep\Daemonizable\Command\EndlessContainerAwareCommand;
use Wrep\Daemonizable\Command\EndlessCommand;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Input\InputInterface;

class ExampleCommand extends EndlessContainerAwareCommand
class ExampleCommand extends EndlessCommand
{
// This is just a normal Command::configure() method
protected function configure()
@@ -38,7 +38,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int

if ( false === file_put_contents('/tmp/acme-avg-score.txt', $score) )
{
// Set the returncode tot non-zero if there are any errors
// Set the return code to non-zero if there are any errors
$this->setReturnCode(1);

// After this execute method returns we want the command exit
95 changes: 42 additions & 53 deletions src/Wrep/Daemonizable/Command/EndlessCommand.php
Original file line number Diff line number Diff line change
@@ -4,11 +4,13 @@

namespace Wrep\Daemonizable\Command;

use Exception;
use InvalidArgumentException;
use Symfony\Component\Console\Exception\LogicException;
use Throwable;
use Wrep\Daemonizable\Exception\ShutdownEndlessCommandException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use function pcntl_async_signals;
@@ -18,12 +20,12 @@ abstract class EndlessCommand extends Command
{
public const DEFAULT_TIMEOUT = 5;

private $code;
private $timeout;
private $returnCode;
private $shutdownRequested;
private $lastUsage;
private $lastPeakUsage;
/** @var int<0,max> $timeout */
private int $timeout;
private int $returnCode;
private bool $shutdownRequested;
private int $lastUsage;
private int $lastPeakUsage;

/**
* @see Command::__construct()
@@ -59,7 +61,7 @@ public function run(InputInterface $input, OutputInterface $output): int
// Enable async signals for fast signal processing
try {
pcntl_async_signals(true);
} catch (\Throwable $e) {
} catch (Throwable $e) {
declare(ticks=1);
}

@@ -93,16 +95,16 @@ public function handleSignal(int $signal): void
* @param InputInterface $input An InputInterface instance
* @param OutputInterface $output An OutputInterface instance
*
* @return integer The command exit code
* @return int The command exit code
*
* @throws \Exception
* @throws Exception
*/
protected function runloop(InputInterface $input, OutputInterface $output)
protected function runloop(InputInterface $input, OutputInterface $output): int
{
try {
$this->starting($input, $output);

do {
while (! $this->shutdownRequested) {
// Start iteration
$this->startIteration($input, $output);

@@ -113,12 +115,12 @@ protected function runloop(InputInterface $input, OutputInterface $output)
$this->finishIteration($input, $output);

// Request shutdown if we only should run once
if ((bool)$input->getOption('run-once')) {
if ($input->getOption('run-once')) {
$this->shutdown();
}

// Print memory report if requested
if ((bool)$input->getOption('detect-leaks')) {
if ($input->getOption('detect-leaks')) {
// Gather memory info
$peak = $this->getMemoryInfo(true);
$curr = $this->getMemoryInfo(false);
@@ -139,8 +141,9 @@ protected function runloop(InputInterface $input, OutputInterface $output)
if (! $this->shutdownRequested) {
usleep($this->timeout);
}
} while (! $this->shutdownRequested);
}
} catch (ShutdownEndlessCommandException $ignore) {
// this exception is just caught to break out of the loop and signal we are done and finalize
}

// Prepare for shutdown
@@ -179,13 +182,14 @@ protected function finishIteration(InputInterface $input, OutputInterface $outpu
/**
* Get information about the current memory usage
*
* @param bool True for peak usage, false for current usage
* @param bool $peak True for peak usage, false for current usage
*
* @return array
* @return array{amount: int, diff: int, diffPercentage: int|float, statusDescription: 'decreasing'|'increasing'|'stable', statusType: 'comment'|'error'|'info'}
*/
private function getMemoryInfo(bool $peak = false): array
{
$lastUsage = ($peak) ? $this->lastPeakUsage : $this->lastUsage;
$info = [];
$info['amount'] = ($peak) ? memory_get_peak_usage() : memory_get_usage();
$info['diff'] = $info['amount'] - $lastUsage;
$info['diffPercentage'] = ($lastUsage == 0) ? 0 : $info['diff'] / ($lastUsage / 100);
@@ -212,22 +216,6 @@ private function getMemoryInfo(bool $peak = false): array
return $info;
}

/**
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@mac-cain13 I could not find any reason whatsoever to override the setCode function in our class. We call parent::setCode inside constructor. the code we execute is programmed in the derived classes inside its functions, which are called from the runloop. So why setCode is important? Also the function was copy of the parent function from Symfony 2, since Symfony 3 different code was used using Closure class. Yet another example why I think his was never used, thus the inconsistency never manifested as a bug.

* @see Command::setCode()
*/
public function setCode(callable $code): static
{
// Exact copy of our parent
// Makes sure we can access to call it every iteration
if (! is_callable($code)) {
throw new \InvalidArgumentException('Invalid callable provided to Command::setCode.');
}

$this->code = $code;

return $this;
}

/**
* Execution logic.
*
@@ -252,19 +240,20 @@ protected function execute(InputInterface $input, OutputInterface $output): int
/**
* Set the timeout of this command.
*
* @param int|float $timeout Timeout between two iterations in seconds
*
* @return Command The current instance
* @param float $timeout Timeout between two iterations in seconds
*
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
public function setTimeout(float $timeout)
public function setTimeout(float $timeout): self
{
if ($timeout < 0) {
throw new \InvalidArgumentException('Invalid timeout provided to Command::setTimeout.');
throw new InvalidArgumentException('Invalid timeout provided to Command::setTimeout.');
}

$this->timeout = (int) (1000000 * $timeout);
/** @var int<0,max> $timeout */
$timeout = (int)(1000000 * $timeout);

$this->timeout = $timeout;

return $this;
}
@@ -282,16 +271,14 @@ public function getTimeout(): float
/**
* Set the return code of this command.
*
* @param int 0 if everything went fine, or an error code
*
* @return Command The current instance
* @param int $returnCode 0 if everything went fine, or an error code
*
* @throws \InvalidArgumentException
* @throws InvalidArgumentException
*/
public function setReturnCode(int $returnCode)
public function setReturnCode(int $returnCode): self
{
if ($returnCode < 0) {
throw new \InvalidArgumentException('Invalid returnCode provided to Command::setReturnCode.');
throw new InvalidArgumentException('Invalid returnCode provided to Command::setReturnCode.');
}

$this->returnCode = $returnCode;
@@ -313,11 +300,10 @@ public function getReturnCode(): int
* Instruct the command to end the endless loop gracefully.
*
* This will finish the current iteration and give the command a chance
* to cleanup.
* to clean up.
*
* @return Command The current instance
*/
public function shutdown()
public function shutdown(): self
{
$this->shutdownRequested = true;

@@ -331,19 +317,17 @@ public function shutdown()
* execution code takes quite long to finish on a point where you still can exit
* without corrupting any data.
*
* @return Command The current instance
*
* @throws ShutdownEndlessCommandException
*/
protected function throwExceptionOnShutdown()
protected function throwExceptionOnShutdown(): self
{
// Make sure all signals are handled
if (function_exists('pcntl_signal_dispatch')) {
pcntl_signal_dispatch();
}

if ($this->shutdownRequested) {
throw new ShutdownEndlessCommandException('Volunteered to break out of the EndlessCommand runloop because a shutdown is requested.');
throw new ShutdownEndlessCommandException('Volunteered to break out of the EndlessCommand::runloop because a shutdown is requested.');
}

return $this;
@@ -362,4 +346,9 @@ protected function throwExceptionOnShutdown()
protected function finalize(InputInterface $input, OutputInterface $output): void
{
}

protected function isShutdownRequested(): bool
{
return $this->shutdownRequested;
}
}
57 changes: 0 additions & 57 deletions src/Wrep/Daemonizable/Command/EndlessContainerAwareCommand.php

This file was deleted.

Original file line number Diff line number Diff line change
@@ -4,6 +4,8 @@

namespace Wrep\Daemonizable\Exception;

class ShutdownEndlessCommandException extends \RuntimeException
use RuntimeException;

class ShutdownEndlessCommandException extends RuntimeException
{
}