From 2d11fb48e4bafb639553578e012638e545d555a2 Mon Sep 17 00:00:00 2001 From: Nicolas MELONI Date: Tue, 26 Oct 2021 09:15:03 +0200 Subject: [PATCH 1/5] added clean "scheduled command history" command --- src/Command/PurgeScheduledCommandCommand.php | 119 ++++++++++++++++++ .../ScheduledCommandPostRemoveEvent.php} | 19 +-- src/Repository/ScheduledCommandRepository.php | 22 ++++ .../ScheduledCommandRepositoryInterface.php | 4 + src/Resources/config/services.yaml | 4 + 5 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 src/Command/PurgeScheduledCommandCommand.php rename src/{EventSubscriber/DeletedScheduledCommandEventSubscriber.php => DoctrineEvent/ScheduledCommandPostRemoveEvent.php} (57%) diff --git a/src/Command/PurgeScheduledCommandCommand.php b/src/Command/PurgeScheduledCommandCommand.php new file mode 100644 index 00000000..e56127cc --- /dev/null +++ b/src/Command/PurgeScheduledCommandCommand.php @@ -0,0 +1,119 @@ +entityManager = $entityManager; + $this->scheduledCommandRepository = $scheduledCommandRepository; + $this->logger = $logger; + } + + protected function configure(): void + { + $this + ->setDescription('Purge scheduled command history lesser than {X} days old.') + ->addOption('all', 'p', InputOption::VALUE_NONE, 'Remove all schedules with specified state (default is finished).') + ->addOption('days', 'd', InputOption::VALUE_OPTIONAL, '{X} days old', self::DEFAULT_PURGE_PERIODE_IN_DAYS) + ->addOption('state', 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'State of scheduled history to be cleaned', [self::DEFAULT_STATE]) + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $purgeAll = $input->getOption('all'); + $daysOld = $input->getOption('days'); + $state = $input->getOption('state'); + /** @var bool $dryRun */ + $dryRun = $input->getOption('dry-run') ?? false; + + if (!\is_numeric($daysOld)) { + throw new \Exception('Invalid days provided.'); + } + + $maxDate = new \DateTime(); + $maxDate->modify(\sprintf('-%d days', $daysOld)); + + $io->note(\sprintf( + 'Schedules lesser than %s days(s) (%s) will be purged.', + $daysOld, + $maxDate->format('Y-m-d') + )); + + /** @var ScheduledCommandInterface[] $schedules */ + $schedules = $this->getScheduledHistory($purgeAll, $maxDate, $state); + + $counter = 0; + foreach ($schedules as $schedule) { + $this->logger->info(\sprintf( + 'Removed scheduled command "%s" (%d)', + $schedule->getName(), + $schedule->getId(), + )); + + if ($dryRun) { + continue; + } + + $this->entityManager->remove($schedule); + + if ($counter % self::DEFAULT_BATCH === 0) { + $this->entityManager->flush(); + } + $counter++; + } + + $this->entityManager->flush(); + + return 0; + } + + private function getScheduledHistory(bool $purgeAll, \DateTimeInterface $maxDate, array $states): iterable + { + if ($purgeAll) { + return $this->scheduledCommandRepository->findAllHavingState($states); + } + + return $this->scheduledCommandRepository->findAllSinceXDaysWithState($maxDate, $states); + } +} diff --git a/src/EventSubscriber/DeletedScheduledCommandEventSubscriber.php b/src/DoctrineEvent/ScheduledCommandPostRemoveEvent.php similarity index 57% rename from src/EventSubscriber/DeletedScheduledCommandEventSubscriber.php rename to src/DoctrineEvent/ScheduledCommandPostRemoveEvent.php index 1861b84f..06f2054f 100644 --- a/src/EventSubscriber/DeletedScheduledCommandEventSubscriber.php +++ b/src/DoctrineEvent/ScheduledCommandPostRemoveEvent.php @@ -2,13 +2,14 @@ declare(strict_types=1); -namespace Synolia\SyliusSchedulerCommandPlugin\EventSubscriber; +namespace Synolia\SyliusSchedulerCommandPlugin\DoctrineEvent; -use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Events; use Synolia\SyliusSchedulerCommandPlugin\Entity\ScheduledCommandInterface; -class DeletedScheduledCommandEventSubscriber implements EventSubscriberInterface +class ScheduledCommandPostRemoveEvent implements EventSubscriber { /** @var string */ private $logsDir; @@ -18,14 +19,16 @@ public function __construct(string $logsDir) $this->logsDir = $logsDir; } - public static function getSubscribedEvents(): array + public function getSubscribedEvents(): array { - return ['synolia.scheduled_command.pre_delete' => ['deleteLogFile']]; + return [ + Events::postRemove, + ]; } - public function deleteLogFile(ResourceControllerEvent $event): void + public function postRemove(LifecycleEventArgs $eventArgs): void { - $scheduledCommand = $event->getSubject(); + $scheduledCommand = $eventArgs->getEntity(); if (!$scheduledCommand instanceof ScheduledCommandInterface) { return; diff --git a/src/Repository/ScheduledCommandRepository.php b/src/Repository/ScheduledCommandRepository.php index 9ea7da4e..e3275d81 100644 --- a/src/Repository/ScheduledCommandRepository.php +++ b/src/Repository/ScheduledCommandRepository.php @@ -32,4 +32,26 @@ public function findLastCreatedCommand(CommandInterface $command): ?ScheduledCom return null; } } + + public function findAllSinceXDaysWithState(\DateTimeInterface $dateTime, array $states): iterable + { + return $this->createQueryBuilder('scheduled') + ->where('scheduled.state IN (:states)') + ->andWhere('scheduled.createdAt < :createdAt') + ->setParameter('states', $states) + ->setParameter('createdAt', $dateTime->format('Y-m-d 00:00:00')) + ->getQuery() + ->getResult() + ; + } + + public function findAllHavingState(array $states): iterable + { + return $this->createQueryBuilder('scheduled') + ->where('scheduled.state IN (:states)') + ->setParameter('states', $states) + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/ScheduledCommandRepositoryInterface.php b/src/Repository/ScheduledCommandRepositoryInterface.php index 9e408306..65b2e4a4 100644 --- a/src/Repository/ScheduledCommandRepositoryInterface.php +++ b/src/Repository/ScheduledCommandRepositoryInterface.php @@ -22,4 +22,8 @@ interface ScheduledCommandRepositoryInterface extends RepositoryInterface public function findAllRunnable(): iterable; public function findLastCreatedCommand(CommandInterface $command): ?ScheduledCommandInterface; + + public function findAllSinceXDaysWithState(\DateTimeInterface $dateTime, array $states): iterable; + + public function findAllHavingState(array $states): iterable; } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 5c132825..2ab194ec 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -72,3 +72,7 @@ services: bind: $pingInterval: '%env(int:SYNOLIA_SCHEDULER_PLUGIN_PING_INTERVAL)%' $keepConnectionAlive: '%env(bool:SYNOLIA_SCHEDULER_PLUGIN_KEEP_ALIVE)%' + + Synolia\SyliusSchedulerCommandPlugin\DoctrineEvent\ScheduledCommandPostRemoveEvent: + tags: + - { name: doctrine.event_subscriber, connection: default } From dee5b761b5de0d472034945230d3a31c269eee01 Mon Sep 17 00:00:00 2001 From: Nicolas MELONI Date: Fri, 12 Nov 2021 17:08:40 +0100 Subject: [PATCH 2/5] added clean "scheduled command history" command tests --- README.md | 26 +++ src/Command/PurgeScheduledCommandCommand.php | 41 ++-- .../PurgeScheduledCommandCommandTest.php | 188 ++++++++++++++++++ 3 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php diff --git a/README.md b/README.md index aed339cb..10cad6d8 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,32 @@ sylius_fixtures: enabled: false ``` +## Commands +### synolia:scheduler-run + +Execute scheduled commands. + +* options: + * --id (run only a specific scheduled command) + +**Run all scheduled commands :** php bin/console synolia:scheduler-run + +**Run one scheduled command :** php bin/console synolia:scheduler-run --id=5 + +### synolia:scheduler:purge-history + +Purge scheduled command history greater than {X} days old. + +* options: + * --all (purge everything) + * --days (number of days to keep) + * --state (array of schedule states) + * --dry-run + +**Example to remove all finished and in error scheduled commands after 7 days :** + +php bin/console synolia:scheduler:purge-history --state=finished --state=error --days=7 + ## Optional services ```yaml services: diff --git a/src/Command/PurgeScheduledCommandCommand.php b/src/Command/PurgeScheduledCommandCommand.php index e56127cc..47967e31 100644 --- a/src/Command/PurgeScheduledCommandCommand.php +++ b/src/Command/PurgeScheduledCommandCommand.php @@ -34,6 +34,8 @@ class PurgeScheduledCommandCommand extends Command /** @var LoggerInterface */ private $logger; + private SymfonyStyle $io; + public function __construct( EntityManagerInterface $entityManager, ScheduledCommandRepositoryInterface $scheduledCommandRepository, @@ -50,7 +52,7 @@ public function __construct( protected function configure(): void { $this - ->setDescription('Purge scheduled command history lesser than {X} days old.') + ->setDescription('Purge scheduled command history greater than {X} days old.') ->addOption('all', 'p', InputOption::VALUE_NONE, 'Remove all schedules with specified state (default is finished).') ->addOption('days', 'd', InputOption::VALUE_OPTIONAL, '{X} days old', self::DEFAULT_PURGE_PERIODE_IN_DAYS) ->addOption('state', 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'State of scheduled history to be cleaned', [self::DEFAULT_STATE]) @@ -60,28 +62,16 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output) { - $io = new SymfonyStyle($input, $output); + $this->io = new SymfonyStyle($input, $output); + $purgeAll = $input->getOption('all'); - $daysOld = $input->getOption('days'); + $daysOld = (int) $input->getOption('days'); $state = $input->getOption('state'); /** @var bool $dryRun */ $dryRun = $input->getOption('dry-run') ?? false; - if (!\is_numeric($daysOld)) { - throw new \Exception('Invalid days provided.'); - } - - $maxDate = new \DateTime(); - $maxDate->modify(\sprintf('-%d days', $daysOld)); - - $io->note(\sprintf( - 'Schedules lesser than %s days(s) (%s) will be purged.', - $daysOld, - $maxDate->format('Y-m-d') - )); - /** @var ScheduledCommandInterface[] $schedules */ - $schedules = $this->getScheduledHistory($purgeAll, $maxDate, $state); + $schedules = $this->getScheduledHistory($purgeAll, $daysOld, $state); $counter = 0; foreach ($schedules as $schedule) { @@ -108,12 +98,27 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } - private function getScheduledHistory(bool $purgeAll, \DateTimeInterface $maxDate, array $states): iterable + private function getScheduledHistory(bool $purgeAll, int $daysOld, array $states): iterable { if ($purgeAll) { + $this->io->note(\sprintf( + 'All schedules with states ["%s"] will be purged.', + \implode(',', $states), + )); + return $this->scheduledCommandRepository->findAllHavingState($states); } + $maxDate = new \DateTime(); + $maxDate->modify(\sprintf('-%d days', $daysOld)); + + $this->io->note(\sprintf( + 'Schedules with states ["%s"] lesser than %s days(s) (%s) will be purged.', + \implode(',', $states), + $daysOld, + $maxDate->format('Y-m-d') + )); + return $this->scheduledCommandRepository->findAllSinceXDaysWithState($maxDate, $states); } } diff --git a/tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php b/tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php new file mode 100644 index 00000000..2cd73884 --- /dev/null +++ b/tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php @@ -0,0 +1,188 @@ +entityManager = static::$container->get(EntityManagerInterface::class); + $this->entityManager->beginTransaction(); + + $this->reflectionClass = new ReflectionClass(PurgeScheduledCommandCommand::class); + } + + protected function tearDown(): void + { + $this->entityManager->rollback(); + parent::tearDown(); + } + + public function testExecuteWithoutArgument(): void + { + $commandTester = $this->createCommandTester(); + $commandTester->execute([]); + + $days = $this->reflectionClass->getConstant('DEFAULT_PURGE_PERIODE_IN_DAYS'); + $now = new \DateTime(); + $now->modify(\sprintf('-%d days', $days)); + + self::assertStringContainsString( + \sprintf( + 'Schedules with states ["finished"] lesser than %d days(s) (%s)', + $days, + $now->format('Y-m-d'), + ), + $commandTester->getDisplay(true) + ); + } + + public function testExecuteWithDaysArgument(): void + { + $commandTester = $this->createCommandTester(); + $commandTester->execute(['--days' => 0], ['interactive' => false]); + + $days = 0; + $now = new \DateTime(); + $now->modify(\sprintf('-%d days', $days)); + + self::assertStringContainsString( + \sprintf( + 'Schedules with states ["finished"] lesser than %d days(s) (%s)', + $days, + $now->format('Y-m-d'), + ), + $commandTester->getDisplay(true) + ); + } + + /** @dataProvider scheduleStateDataProvider */ + public function testExecuteWithStateArgument(array $states, string $expected): void + { + $commandTester = $this->createCommandTester(); + $commandTester->execute([ + '--state' => $states, + ], [ + 'interactive' => false, + ]); + + self::assertStringContainsString( + \sprintf( + 'Schedules with states ["%s"]', + $expected + ), + $commandTester->getDisplay(true) + ); + } + + public function scheduleStateDataProvider(): \Generator + { + yield [[ScheduledCommandStateEnum::FINISHED], ScheduledCommandStateEnum::FINISHED]; + yield [[ScheduledCommandStateEnum::WAITING], ScheduledCommandStateEnum::WAITING]; + yield [ + [ScheduledCommandStateEnum::FINISHED, ScheduledCommandStateEnum::WAITING], + ScheduledCommandStateEnum::FINISHED . ',' . ScheduledCommandStateEnum::WAITING + ]; + } + + public function testPurgeAllWithDryRun(): void + { + $scheduledCommand = $this->generateScheduleCommand( + 'about', + 'about', + ScheduledCommandStateEnum::FINISHED + ); + + $this->save($scheduledCommand); + + $commandTester = $this->createCommandTester(); + $commandTester->execute([ + '--all' => true, + '--dry-run' => true, + ], [ + 'interactive' => false, + 'verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE, + ]); + + $count = $this->entityManager->getRepository(ScheduledCommandInterface::class)->count([]); + $this->assertEquals(1, $count); + } + + public function testPurgeAllWithoutDryRun(): void + { + $scheduledCommand = $this->generateScheduleCommand( + 'about', + 'about', + ScheduledCommandStateEnum::FINISHED + ); + $this->save($scheduledCommand); + + $scheduledCommand = $this->generateScheduleCommand( + 'about1', + 'about1', + ScheduledCommandStateEnum::FINISHED + ); + $this->save($scheduledCommand); + + $this->createCommandTester()->execute([ + '--all' => true, + ], [ + 'interactive' => false, + 'verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE, + ]); + + $count = $this->entityManager->getRepository(ScheduledCommandInterface::class)->count([]); + $this->assertEquals(0, $count); + } + + private function generateScheduleCommand(string $name, string $command, string $state): ScheduledCommandInterface + { + /** @var ScheduledCommand $scheduledCommand */ + $scheduledCommand = (new Factory(ScheduledCommand::class))->createNew(); + $scheduledCommand + ->setName($name) + ->setCommand($command) + ->setState($state) + ; + + return $scheduledCommand; + } + + private function save(ScheduledCommandInterface $scheduledCommand): void + { + $this->entityManager->persist($scheduledCommand); + $this->entityManager->flush(); + } + + private function createCommandTester(): CommandTester + { + $application = new Application(static::$kernel); + $command = $application->find(PurgeScheduledCommandCommand::getDefaultName()); + + return new CommandTester($command); + } +} From 77cb27699ed8b40ed987022e15ecb33d372d7a7a Mon Sep 17 00:00:00 2001 From: Nicolas MELONI Date: Tue, 26 Oct 2021 09:15:03 +0200 Subject: [PATCH 3/5] added clean "scheduled command history" command --- src/Command/PurgeScheduledCommandCommand.php | 119 ++++++++++++++++++ .../ScheduledCommandPostRemoveEvent.php} | 19 +-- src/Repository/ScheduledCommandRepository.php | 22 ++++ .../ScheduledCommandRepositoryInterface.php | 4 + src/Resources/config/services.yaml | 4 + 5 files changed, 160 insertions(+), 8 deletions(-) create mode 100644 src/Command/PurgeScheduledCommandCommand.php rename src/{EventSubscriber/DeletedScheduledCommandEventSubscriber.php => DoctrineEvent/ScheduledCommandPostRemoveEvent.php} (57%) diff --git a/src/Command/PurgeScheduledCommandCommand.php b/src/Command/PurgeScheduledCommandCommand.php new file mode 100644 index 00000000..e56127cc --- /dev/null +++ b/src/Command/PurgeScheduledCommandCommand.php @@ -0,0 +1,119 @@ +entityManager = $entityManager; + $this->scheduledCommandRepository = $scheduledCommandRepository; + $this->logger = $logger; + } + + protected function configure(): void + { + $this + ->setDescription('Purge scheduled command history lesser than {X} days old.') + ->addOption('all', 'p', InputOption::VALUE_NONE, 'Remove all schedules with specified state (default is finished).') + ->addOption('days', 'd', InputOption::VALUE_OPTIONAL, '{X} days old', self::DEFAULT_PURGE_PERIODE_IN_DAYS) + ->addOption('state', 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'State of scheduled history to be cleaned', [self::DEFAULT_STATE]) + ->addOption('dry-run', null, InputOption::VALUE_NONE, 'Dry run') + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = new SymfonyStyle($input, $output); + $purgeAll = $input->getOption('all'); + $daysOld = $input->getOption('days'); + $state = $input->getOption('state'); + /** @var bool $dryRun */ + $dryRun = $input->getOption('dry-run') ?? false; + + if (!\is_numeric($daysOld)) { + throw new \Exception('Invalid days provided.'); + } + + $maxDate = new \DateTime(); + $maxDate->modify(\sprintf('-%d days', $daysOld)); + + $io->note(\sprintf( + 'Schedules lesser than %s days(s) (%s) will be purged.', + $daysOld, + $maxDate->format('Y-m-d') + )); + + /** @var ScheduledCommandInterface[] $schedules */ + $schedules = $this->getScheduledHistory($purgeAll, $maxDate, $state); + + $counter = 0; + foreach ($schedules as $schedule) { + $this->logger->info(\sprintf( + 'Removed scheduled command "%s" (%d)', + $schedule->getName(), + $schedule->getId(), + )); + + if ($dryRun) { + continue; + } + + $this->entityManager->remove($schedule); + + if ($counter % self::DEFAULT_BATCH === 0) { + $this->entityManager->flush(); + } + $counter++; + } + + $this->entityManager->flush(); + + return 0; + } + + private function getScheduledHistory(bool $purgeAll, \DateTimeInterface $maxDate, array $states): iterable + { + if ($purgeAll) { + return $this->scheduledCommandRepository->findAllHavingState($states); + } + + return $this->scheduledCommandRepository->findAllSinceXDaysWithState($maxDate, $states); + } +} diff --git a/src/EventSubscriber/DeletedScheduledCommandEventSubscriber.php b/src/DoctrineEvent/ScheduledCommandPostRemoveEvent.php similarity index 57% rename from src/EventSubscriber/DeletedScheduledCommandEventSubscriber.php rename to src/DoctrineEvent/ScheduledCommandPostRemoveEvent.php index 1861b84f..06f2054f 100644 --- a/src/EventSubscriber/DeletedScheduledCommandEventSubscriber.php +++ b/src/DoctrineEvent/ScheduledCommandPostRemoveEvent.php @@ -2,13 +2,14 @@ declare(strict_types=1); -namespace Synolia\SyliusSchedulerCommandPlugin\EventSubscriber; +namespace Synolia\SyliusSchedulerCommandPlugin\DoctrineEvent; -use Sylius\Bundle\ResourceBundle\Event\ResourceControllerEvent; -use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Event\LifecycleEventArgs; +use Doctrine\ORM\Events; use Synolia\SyliusSchedulerCommandPlugin\Entity\ScheduledCommandInterface; -class DeletedScheduledCommandEventSubscriber implements EventSubscriberInterface +class ScheduledCommandPostRemoveEvent implements EventSubscriber { /** @var string */ private $logsDir; @@ -18,14 +19,16 @@ public function __construct(string $logsDir) $this->logsDir = $logsDir; } - public static function getSubscribedEvents(): array + public function getSubscribedEvents(): array { - return ['synolia.scheduled_command.pre_delete' => ['deleteLogFile']]; + return [ + Events::postRemove, + ]; } - public function deleteLogFile(ResourceControllerEvent $event): void + public function postRemove(LifecycleEventArgs $eventArgs): void { - $scheduledCommand = $event->getSubject(); + $scheduledCommand = $eventArgs->getEntity(); if (!$scheduledCommand instanceof ScheduledCommandInterface) { return; diff --git a/src/Repository/ScheduledCommandRepository.php b/src/Repository/ScheduledCommandRepository.php index 9ea7da4e..e3275d81 100644 --- a/src/Repository/ScheduledCommandRepository.php +++ b/src/Repository/ScheduledCommandRepository.php @@ -32,4 +32,26 @@ public function findLastCreatedCommand(CommandInterface $command): ?ScheduledCom return null; } } + + public function findAllSinceXDaysWithState(\DateTimeInterface $dateTime, array $states): iterable + { + return $this->createQueryBuilder('scheduled') + ->where('scheduled.state IN (:states)') + ->andWhere('scheduled.createdAt < :createdAt') + ->setParameter('states', $states) + ->setParameter('createdAt', $dateTime->format('Y-m-d 00:00:00')) + ->getQuery() + ->getResult() + ; + } + + public function findAllHavingState(array $states): iterable + { + return $this->createQueryBuilder('scheduled') + ->where('scheduled.state IN (:states)') + ->setParameter('states', $states) + ->getQuery() + ->getResult() + ; + } } diff --git a/src/Repository/ScheduledCommandRepositoryInterface.php b/src/Repository/ScheduledCommandRepositoryInterface.php index 9e408306..65b2e4a4 100644 --- a/src/Repository/ScheduledCommandRepositoryInterface.php +++ b/src/Repository/ScheduledCommandRepositoryInterface.php @@ -22,4 +22,8 @@ interface ScheduledCommandRepositoryInterface extends RepositoryInterface public function findAllRunnable(): iterable; public function findLastCreatedCommand(CommandInterface $command): ?ScheduledCommandInterface; + + public function findAllSinceXDaysWithState(\DateTimeInterface $dateTime, array $states): iterable; + + public function findAllHavingState(array $states): iterable; } diff --git a/src/Resources/config/services.yaml b/src/Resources/config/services.yaml index 5c132825..2ab194ec 100644 --- a/src/Resources/config/services.yaml +++ b/src/Resources/config/services.yaml @@ -72,3 +72,7 @@ services: bind: $pingInterval: '%env(int:SYNOLIA_SCHEDULER_PLUGIN_PING_INTERVAL)%' $keepConnectionAlive: '%env(bool:SYNOLIA_SCHEDULER_PLUGIN_KEEP_ALIVE)%' + + Synolia\SyliusSchedulerCommandPlugin\DoctrineEvent\ScheduledCommandPostRemoveEvent: + tags: + - { name: doctrine.event_subscriber, connection: default } From 0816bf3161441daefa3a1bf3b05da2e8254ad6d2 Mon Sep 17 00:00:00 2001 From: Nicolas MELONI Date: Fri, 12 Nov 2021 17:08:40 +0100 Subject: [PATCH 4/5] added clean "scheduled command history" command tests --- README.md | 26 +++ src/Command/PurgeScheduledCommandCommand.php | 41 ++-- .../PurgeScheduledCommandCommandTest.php | 188 ++++++++++++++++++ 3 files changed, 237 insertions(+), 18 deletions(-) create mode 100644 tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php diff --git a/README.md b/README.md index 0586dd50..749a7bf9 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,32 @@ sylius_fixtures: enabled: false ``` +## Commands +### synolia:scheduler-run + +Execute scheduled commands. + +* options: + * --id (run only a specific scheduled command) + +**Run all scheduled commands :** php bin/console synolia:scheduler-run + +**Run one scheduled command :** php bin/console synolia:scheduler-run --id=5 + +### synolia:scheduler:purge-history + +Purge scheduled command history greater than {X} days old. + +* options: + * --all (purge everything) + * --days (number of days to keep) + * --state (array of schedule states) + * --dry-run + +**Example to remove all finished and in error scheduled commands after 7 days :** + +php bin/console synolia:scheduler:purge-history --state=finished --state=error --days=7 + ## Optional services ```yaml services: diff --git a/src/Command/PurgeScheduledCommandCommand.php b/src/Command/PurgeScheduledCommandCommand.php index e56127cc..47967e31 100644 --- a/src/Command/PurgeScheduledCommandCommand.php +++ b/src/Command/PurgeScheduledCommandCommand.php @@ -34,6 +34,8 @@ class PurgeScheduledCommandCommand extends Command /** @var LoggerInterface */ private $logger; + private SymfonyStyle $io; + public function __construct( EntityManagerInterface $entityManager, ScheduledCommandRepositoryInterface $scheduledCommandRepository, @@ -50,7 +52,7 @@ public function __construct( protected function configure(): void { $this - ->setDescription('Purge scheduled command history lesser than {X} days old.') + ->setDescription('Purge scheduled command history greater than {X} days old.') ->addOption('all', 'p', InputOption::VALUE_NONE, 'Remove all schedules with specified state (default is finished).') ->addOption('days', 'd', InputOption::VALUE_OPTIONAL, '{X} days old', self::DEFAULT_PURGE_PERIODE_IN_DAYS) ->addOption('state', 's', InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'State of scheduled history to be cleaned', [self::DEFAULT_STATE]) @@ -60,28 +62,16 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output) { - $io = new SymfonyStyle($input, $output); + $this->io = new SymfonyStyle($input, $output); + $purgeAll = $input->getOption('all'); - $daysOld = $input->getOption('days'); + $daysOld = (int) $input->getOption('days'); $state = $input->getOption('state'); /** @var bool $dryRun */ $dryRun = $input->getOption('dry-run') ?? false; - if (!\is_numeric($daysOld)) { - throw new \Exception('Invalid days provided.'); - } - - $maxDate = new \DateTime(); - $maxDate->modify(\sprintf('-%d days', $daysOld)); - - $io->note(\sprintf( - 'Schedules lesser than %s days(s) (%s) will be purged.', - $daysOld, - $maxDate->format('Y-m-d') - )); - /** @var ScheduledCommandInterface[] $schedules */ - $schedules = $this->getScheduledHistory($purgeAll, $maxDate, $state); + $schedules = $this->getScheduledHistory($purgeAll, $daysOld, $state); $counter = 0; foreach ($schedules as $schedule) { @@ -108,12 +98,27 @@ protected function execute(InputInterface $input, OutputInterface $output) return 0; } - private function getScheduledHistory(bool $purgeAll, \DateTimeInterface $maxDate, array $states): iterable + private function getScheduledHistory(bool $purgeAll, int $daysOld, array $states): iterable { if ($purgeAll) { + $this->io->note(\sprintf( + 'All schedules with states ["%s"] will be purged.', + \implode(',', $states), + )); + return $this->scheduledCommandRepository->findAllHavingState($states); } + $maxDate = new \DateTime(); + $maxDate->modify(\sprintf('-%d days', $daysOld)); + + $this->io->note(\sprintf( + 'Schedules with states ["%s"] lesser than %s days(s) (%s) will be purged.', + \implode(',', $states), + $daysOld, + $maxDate->format('Y-m-d') + )); + return $this->scheduledCommandRepository->findAllSinceXDaysWithState($maxDate, $states); } } diff --git a/tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php b/tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php new file mode 100644 index 00000000..2cd73884 --- /dev/null +++ b/tests/PHPUnit/Command/PurgeScheduledCommandCommandTest.php @@ -0,0 +1,188 @@ +entityManager = static::$container->get(EntityManagerInterface::class); + $this->entityManager->beginTransaction(); + + $this->reflectionClass = new ReflectionClass(PurgeScheduledCommandCommand::class); + } + + protected function tearDown(): void + { + $this->entityManager->rollback(); + parent::tearDown(); + } + + public function testExecuteWithoutArgument(): void + { + $commandTester = $this->createCommandTester(); + $commandTester->execute([]); + + $days = $this->reflectionClass->getConstant('DEFAULT_PURGE_PERIODE_IN_DAYS'); + $now = new \DateTime(); + $now->modify(\sprintf('-%d days', $days)); + + self::assertStringContainsString( + \sprintf( + 'Schedules with states ["finished"] lesser than %d days(s) (%s)', + $days, + $now->format('Y-m-d'), + ), + $commandTester->getDisplay(true) + ); + } + + public function testExecuteWithDaysArgument(): void + { + $commandTester = $this->createCommandTester(); + $commandTester->execute(['--days' => 0], ['interactive' => false]); + + $days = 0; + $now = new \DateTime(); + $now->modify(\sprintf('-%d days', $days)); + + self::assertStringContainsString( + \sprintf( + 'Schedules with states ["finished"] lesser than %d days(s) (%s)', + $days, + $now->format('Y-m-d'), + ), + $commandTester->getDisplay(true) + ); + } + + /** @dataProvider scheduleStateDataProvider */ + public function testExecuteWithStateArgument(array $states, string $expected): void + { + $commandTester = $this->createCommandTester(); + $commandTester->execute([ + '--state' => $states, + ], [ + 'interactive' => false, + ]); + + self::assertStringContainsString( + \sprintf( + 'Schedules with states ["%s"]', + $expected + ), + $commandTester->getDisplay(true) + ); + } + + public function scheduleStateDataProvider(): \Generator + { + yield [[ScheduledCommandStateEnum::FINISHED], ScheduledCommandStateEnum::FINISHED]; + yield [[ScheduledCommandStateEnum::WAITING], ScheduledCommandStateEnum::WAITING]; + yield [ + [ScheduledCommandStateEnum::FINISHED, ScheduledCommandStateEnum::WAITING], + ScheduledCommandStateEnum::FINISHED . ',' . ScheduledCommandStateEnum::WAITING + ]; + } + + public function testPurgeAllWithDryRun(): void + { + $scheduledCommand = $this->generateScheduleCommand( + 'about', + 'about', + ScheduledCommandStateEnum::FINISHED + ); + + $this->save($scheduledCommand); + + $commandTester = $this->createCommandTester(); + $commandTester->execute([ + '--all' => true, + '--dry-run' => true, + ], [ + 'interactive' => false, + 'verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE, + ]); + + $count = $this->entityManager->getRepository(ScheduledCommandInterface::class)->count([]); + $this->assertEquals(1, $count); + } + + public function testPurgeAllWithoutDryRun(): void + { + $scheduledCommand = $this->generateScheduleCommand( + 'about', + 'about', + ScheduledCommandStateEnum::FINISHED + ); + $this->save($scheduledCommand); + + $scheduledCommand = $this->generateScheduleCommand( + 'about1', + 'about1', + ScheduledCommandStateEnum::FINISHED + ); + $this->save($scheduledCommand); + + $this->createCommandTester()->execute([ + '--all' => true, + ], [ + 'interactive' => false, + 'verbosity' => OutputInterface::VERBOSITY_VERY_VERBOSE, + ]); + + $count = $this->entityManager->getRepository(ScheduledCommandInterface::class)->count([]); + $this->assertEquals(0, $count); + } + + private function generateScheduleCommand(string $name, string $command, string $state): ScheduledCommandInterface + { + /** @var ScheduledCommand $scheduledCommand */ + $scheduledCommand = (new Factory(ScheduledCommand::class))->createNew(); + $scheduledCommand + ->setName($name) + ->setCommand($command) + ->setState($state) + ; + + return $scheduledCommand; + } + + private function save(ScheduledCommandInterface $scheduledCommand): void + { + $this->entityManager->persist($scheduledCommand); + $this->entityManager->flush(); + } + + private function createCommandTester(): CommandTester + { + $application = new Application(static::$kernel); + $command = $application->find(PurgeScheduledCommandCommand::getDefaultName()); + + return new CommandTester($command); + } +} From e9a667010d93b563f0812d4e4f15c4e8e3e89c27 Mon Sep 17 00:00:00 2001 From: Nicolas MELONI Date: Fri, 7 Jan 2022 09:34:27 +0100 Subject: [PATCH 5/5] fix CI --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 09d8cffc..66f531af 100644 --- a/Makefile +++ b/Makefile @@ -80,7 +80,7 @@ behat-configure: ## Configure Behat (cd ${TEST_DIRECTORY} && sed -i "s#vendor/sylius/sylius/src/Sylius/Behat/Resources/config/suites.yml#vendor/${PLUGIN_NAME}/tests/Behat/Resources/suites.yml#g" behat.yml) (cd ${TEST_DIRECTORY} && sed -i "s#vendor/sylius/sylius/features#vendor/${PLUGIN_NAME}/features#g" behat.yml) (cd ${TEST_DIRECTORY} && sed -i "s#@cli#@javascript#g" behat.yml) - (cd ${TEST_DIRECTORY} && echo ' - { resource: "../vendor/${PLUGIN_NAME}/tests/Behat/Resources/services.yml" }' >> config/services_test.yaml) + (cd ${TEST_DIRECTORY} && sed -i '2i \ \ \ \ - { resource: "../vendor/${PLUGIN_NAME}/tests/Behat/Resources/services.yml\" }' config/services_test.yaml) grumphp: ## Run GrumPHP vendor/bin/grumphp run