diff --git a/app/code/Magento/Cron/Console/Command/CronCommand.php b/app/code/Magento/Cron/Console/Command/CronCommand.php index 0a9fd4c195f0a..4032a74802652 100644 --- a/app/code/Magento/Cron/Console/Command/CronCommand.php +++ b/app/code/Magento/Cron/Console/Command/CronCommand.php @@ -35,6 +35,12 @@ class CronCommand extends Command public const INPUT_KEY_GROUP = 'group'; /** + * Name of input option + */ + public const INPUT_KEY_EXCLUDE_GROUP = 'exclude-group'; + + /** + * * @var ObjectManagerFactory */ private $objectManagerFactory; @@ -73,6 +79,12 @@ protected function configure() InputOption::VALUE_REQUIRED, 'Run jobs only from specified group' ), + new InputOption( + self::INPUT_KEY_EXCLUDE_GROUP, + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Exclude jobs from the specified group' + ), new InputOption( Cli::INPUT_KEY_BOOTSTRAP, null, @@ -102,6 +114,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $output->writeln('' . 'Cron is disabled. Jobs were not run.' . ''); return Cli::RETURN_SUCCESS; } + // phpcs:ignore Magento2.Security.Superglobal $omParams = $_SERVER; $omParams[StoreManager::PARAM_RUN_CODE] = 'admin'; @@ -109,6 +122,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $objectManager = $this->objectManagerFactory->create($omParams); $params[self::INPUT_KEY_GROUP] = $input->getOption(self::INPUT_KEY_GROUP); + $params[self::INPUT_KEY_EXCLUDE_GROUP] = $input->getOption(self::INPUT_KEY_EXCLUDE_GROUP); $params[ProcessCronQueueObserver::STANDALONE_PROCESS_STARTED] = '0'; $bootstrap = $input->getOption(Cli::INPUT_KEY_BOOTSTRAP); if ($bootstrap) { diff --git a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php index a4a11156956d9..857e419fe93f3 100644 --- a/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php +++ b/app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php @@ -257,6 +257,9 @@ function ($a, $b) { if (!$this->isGroupInFilter($groupId)) { continue; } + if ($this->isGroupInExcludeFilter($groupId)) { + continue; + } if ($this->_request->getParam(self::STANDALONE_PROCESS_STARTED) !== '1' && $this->getCronGroupConfigurationValue($groupId, 'use_separate_process') == 1 ) { @@ -809,6 +812,18 @@ private function isGroupInFilter($groupId): bool && trim($this->_request->getParam('group'), "'") !== $groupId); } + /** + * Is Group In Exclude Filter. + * + * @param string $groupId + * @return bool + */ + private function isGroupInExcludeFilter($groupId): bool + { + $excludeGroup = $this->_request->getParam('exclude-group', []); + return is_array($excludeGroup) && in_array($groupId, $excludeGroup); + } + /** * Process pending jobs. * diff --git a/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php b/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php index 4ca8ab53ffbad..237f2c95606a8 100644 --- a/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php +++ b/dev/tests/integration/testsuite/Magento/Cron/Observer/ProcessCronQueueObserverTest.php @@ -5,6 +5,7 @@ */ namespace Magento\Cron\Observer; +use Magento\Cron\Observer\ProcessCronQueueObserver; use \Magento\TestFramework\Helper\Bootstrap; class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase @@ -49,4 +50,119 @@ public function testDispatchNoFailed() $this->fail($item->getMessages()); } } + + /** + * @param array $expectedGroupsToRun + * @param null $group + * @param null $excludeGroup + * @dataProvider groupFiltersDataProvider + */ + public function testGroupFilters(array $expectedGroupsToRun, $group = null, $excludeGroup = null) + { + $config = $this->createMock(\Magento\Cron\Model\ConfigInterface::class); + $config->expects($this->any()) + ->method('getJobs') + ->willReturn($this->getFilterTestCronGroups()); + + $request = Bootstrap::getObjectManager()->get(\Magento\Framework\App\Console\Request::class); + $lockManager = $this->createMock(\Magento\Framework\Lock\LockManagerInterface::class); + + // The jobs are locked when they are run, assert on them to see which groups would run + $expectedLockData = []; + foreach ($expectedGroupsToRun as $expectedGroupToRun) { + $expectedLockData[] = [ + ProcessCronQueueObserver::LOCK_PREFIX . $expectedGroupToRun, + ProcessCronQueueObserver::LOCK_TIMEOUT + ]; + } + + // No expected lock data, means we should never call it + if (empty($expectedLockData)) { + $lockManager->expects($this->never()) + ->method('lock'); + } + + $lockManager->expects($this->exactly(count($expectedLockData))) + ->method('lock') + ->withConsecutive(...$expectedLockData); + + $request->setParams( + [ + 'group' => $group, + 'exclude-group' => $excludeGroup, + 'standaloneProcessStarted' => '1' + ] + ); + $this->_model = Bootstrap::getObjectManager() + ->create(\Magento\Cron\Observer\ProcessCronQueueObserver::class, [ + 'request' => $request, + 'lockManager' => $lockManager, + 'config' => $config + ]); + $this->_model->execute(new \Magento\Framework\Event\Observer()); + } + + /** + * @return array|array[] + */ + public function groupFiltersDataProvider(): array + { + + return [ + 'no flags runs all groups' => [ + ['index', 'consumers', 'default'] // groups to run + ], + '--group=default should run' => [ + ['default'], // groups to run + 'default', // --group default + ], + '--group=default with --exclude-group=default, nothing should run' => [ + [], // groups to run + 'default', // --group default + ['default'], // --exclude-group default + ], + '--group=default with --exclude-group=index, default should run' => [ + ['default'], // groups to run + 'default', // --group default + ['index'], // --exclude-group index + ], + '--group=index with --exclude-group=default, index should run' => [ + ['index'], // groups to run + 'index', // --group index + ['default'], // --exclude-group default + ], + '--exclude-group=index, all other groups should run' => [ + ['consumers', 'default'], // groups to run, all but index + null, // + ['index'] // --exclude-group index + ], + '--exclude-group for every group runs nothing' => [ + [], // groups to run, none + null, // + ['default', 'consumers', 'index'] // groups to exclude, all of them + ], + 'exclude all groups but consumers, consumers runs' => [ + ['consumers'], + null, + ['index', 'default'] + ], + ]; + } + + /** + * Only run the filter group tests with a limited set of cron groups, keeps tests consistent between EE and CE + * + * @return array + */ + private function getFilterTestCronGroups() + { + $listOfGroups = []; + $config = Bootstrap::getObjectManager()->get(\Magento\Cron\Model\ConfigInterface::class); + foreach ($config->getJobs() as $groupId => $data) { + if (in_array($groupId, ['default', 'consumers', 'index'])) { + $listOfGroups[$groupId] = $data; + } + } + return $listOfGroups; + } }