Skip to content
14 changes: 14 additions & 0 deletions app/code/Magento/Cron/Console/Command/CronCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -102,13 +114,15 @@ protected function execute(InputInterface $input, OutputInterface $output)
$output->writeln('<info>' . 'Cron is disabled. Jobs were not run.' . '</info>');
return Cli::RETURN_SUCCESS;
}

// phpcs:ignore Magento2.Security.Superglobal
$omParams = $_SERVER;
$omParams[StoreManager::PARAM_RUN_CODE] = 'admin';
$omParams[Store::CUSTOM_ENTRY_POINT_PARAM] = true;
$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) {
Expand Down
15 changes: 15 additions & 0 deletions app/code/Magento/Cron/Observer/ProcessCronQueueObserver.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
) {
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
*/
namespace Magento\Cron\Observer;

use Magento\Cron\Observer\ProcessCronQueueObserver;
use \Magento\TestFramework\Helper\Bootstrap;

class ProcessCronQueueObserverTest extends \PHPUnit\Framework\TestCase
Expand Down Expand Up @@ -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;
}
}