Skip to content

Commit b30e490

Browse files
committed
ACPT-1751: Enable Suspension of Cron-Triggered Indexer Operations
1 parent 9846306 commit b30e490

File tree

10 files changed

+314
-3
lines changed

10 files changed

+314
-3
lines changed

app/code/Magento/Indexer/Block/Backend/Grid/Column/Renderer/Status.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ public function render(\Magento\Framework\DataObject $row)
3333
$class = 'grid-severity-minor';
3434
$text = __('Processing');
3535
break;
36+
case \Magento\Framework\Indexer\StateInterface::STATUS_SUSPENDED:
37+
$class = 'grid-severity-minor';
38+
$text = __('Suspended');
39+
break;
3640
}
3741
return '<span class="' . $class . '"><span>' . $text . '</span></span>';
3842
}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Indexer\Console\Command;
9+
10+
use Magento\Framework\App\ObjectManagerFactory;
11+
use Magento\Framework\Console\Cli;
12+
use Magento\Framework\Exception\AlreadyExistsException;
13+
use Magento\Framework\Indexer\IndexerInterface;
14+
use Magento\Framework\Indexer\StateInterface;
15+
use Magento\Indexer\Model\ResourceModel\Indexer\State;
16+
use Symfony\Component\Console\Input\InputArgument;
17+
use Symfony\Component\Console\Input\InputInterface;
18+
use Symfony\Component\Console\Input\InputOption;
19+
use Symfony\Component\Console\Output\OutputInterface;
20+
21+
/**
22+
* Command for setting index status for indexers
23+
*/
24+
class IndexerSetStatusCommand extends AbstractIndexerManageCommand
25+
{
26+
/**#@+
27+
* Names of input arguments or options
28+
*/
29+
const INPUT_KEY_STATUS = 'status';
30+
/**#@- */
31+
32+
/**
33+
* @var State
34+
*/
35+
private State $stateResourceModel;
36+
37+
/**
38+
* @param State $stateResourceModel
39+
* @param ObjectManagerFactory $objectManagerFactory
40+
*/
41+
public function __construct(
42+
State $stateResourceModel,
43+
ObjectManagerFactory $objectManagerFactory
44+
) {
45+
$this->stateResourceModel = $stateResourceModel;
46+
parent::__construct($objectManagerFactory);
47+
}
48+
49+
/**
50+
* {@inheritdoc}
51+
*/
52+
protected function configure()
53+
{
54+
$this->setName('indexer:set-status')
55+
->setDescription('Sets the specified indexer status')
56+
->setDefinition($this->getInputList());
57+
58+
parent::configure();
59+
}
60+
61+
/**
62+
* {@inheritdoc}
63+
*/
64+
protected function execute(InputInterface $input, OutputInterface $output)
65+
{
66+
$errors = $this->validate($input);
67+
if ($errors) {
68+
throw new \InvalidArgumentException(implode("\n", $errors));
69+
}
70+
71+
$newStatus = $input->getArgument(self::INPUT_KEY_STATUS);
72+
$indexers = $this->getIndexers($input);
73+
$returnValue = Cli::RETURN_SUCCESS;
74+
75+
foreach ($indexers as $indexer) {
76+
try {
77+
$this->updateIndexerStatus($indexer, $newStatus, $output);
78+
} catch (\Exception $e) {
79+
$output->writeln($e->getMessage());
80+
$returnValue = Cli::RETURN_FAILURE;
81+
}
82+
}
83+
84+
return $returnValue;
85+
}
86+
87+
/**
88+
* Get list of arguments for the command
89+
*
90+
* @return InputOption[]
91+
*/
92+
public function getInputList(): array
93+
{
94+
$modeOptions[] = new InputArgument(
95+
self::INPUT_KEY_STATUS,
96+
InputArgument::REQUIRED,
97+
'Indexer status type [' . StateInterface::STATUS_VALID
98+
. '|' . StateInterface::STATUS_INVALID . '|' . StateInterface::STATUS_SUSPENDED . ']'
99+
);
100+
101+
return array_merge($modeOptions, parent::getInputList());
102+
}
103+
104+
/**
105+
* Check if all CLI command options are provided
106+
*
107+
* @param InputInterface $input
108+
* @return string[]
109+
*/
110+
private function validate(InputInterface $input): array
111+
{
112+
$errors = [];
113+
$acceptedValues = [
114+
StateInterface::STATUS_VALID,
115+
StateInterface::STATUS_INVALID,
116+
StateInterface::STATUS_SUSPENDED,
117+
];
118+
$acceptedValuesString = '"' . implode('", "', $acceptedValues) . '"';
119+
$inputStatus = $input->getArgument(self::INPUT_KEY_STATUS);
120+
121+
if (!$inputStatus) {
122+
$errors[] = sprintf(
123+
'Missing argument "%s". Accepted values are %s.',
124+
self::INPUT_KEY_STATUS,
125+
$acceptedValuesString
126+
);
127+
} elseif (!in_array($inputStatus, $acceptedValues, true)) {
128+
$errors[] = sprintf(
129+
'Invalid status "%s". Accepted values are %s.',
130+
$inputStatus,
131+
$acceptedValuesString
132+
);
133+
}
134+
135+
return $errors;
136+
}
137+
138+
/**
139+
* Update the status of a specified indexer
140+
*
141+
* @param IndexerInterface $indexer
142+
* @param string $newStatus
143+
* @param OutputInterface $output
144+
* @return void
145+
* @throws AlreadyExistsException
146+
*/
147+
private function updateIndexerStatus(IndexerInterface $indexer, string $newStatus, OutputInterface $output): void
148+
{
149+
$state = $indexer->getState();
150+
$previousStatus = $state->getStatus();
151+
$this->stateResourceModel->save($state->setStatus($newStatus));
152+
$currentStatus = $state->getStatus();
153+
154+
if ($previousStatus !== $currentStatus) {
155+
$output->writeln(
156+
sprintf(
157+
"Index status for Indexer '%s' was changed from '%s' to '%s'.",
158+
$indexer->getTitle(),
159+
$previousStatus,
160+
$currentStatus
161+
)
162+
);
163+
} else {
164+
$output->writeln(sprintf("Index status for Indexer '%s' has not been changed.", $indexer->getTitle()));
165+
}
166+
}
167+
}

app/code/Magento/Indexer/Console/Command/IndexerStatusCommand.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ private function getStatus(Indexer\IndexerInterface $indexer)
9595
case \Magento\Framework\Indexer\StateInterface::STATUS_WORKING:
9696
$status = 'Processing';
9797
break;
98+
case \Magento\Framework\Indexer\StateInterface::STATUS_SUSPENDED:
99+
$status = 'Suspended';
100+
break;
98101
}
99102
return $status;
100103
}

app/code/Magento/Indexer/Model/Indexer.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
namespace Magento\Indexer\Model;
88

9+
use Magento\Framework\DataObject;
910
use Magento\Framework\Indexer\ActionFactory;
1011
use Magento\Framework\Indexer\ActionInterface;
1112
use Magento\Framework\Indexer\ConfigInterface;
@@ -14,13 +15,14 @@
1415
use Magento\Framework\Indexer\StateInterface;
1516
use Magento\Framework\Indexer\StructureFactory;
1617
use Magento\Framework\Indexer\IndexerInterfaceFactory;
18+
use Magento\Framework\Indexer\SuspendableIndexerInterface;
1719

1820
/**
1921
* Indexer model.
2022
*
2123
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
2224
*/
23-
class Indexer extends \Magento\Framework\DataObject implements IndexerInterface
25+
class Indexer extends DataObject implements IndexerInterface, SuspendableIndexerInterface
2426
{
2527
/**
2628
* @var string
@@ -332,6 +334,16 @@ public function isInvalid()
332334
return $this->getState()->getStatus() == StateInterface::STATUS_INVALID;
333335
}
334336

337+
/**
338+
* Check whether indexer is valid
339+
*
340+
* @return bool
341+
*/
342+
public function isSuspended(): bool
343+
{
344+
return $this->getState()->getStatus() === StateInterface::STATUS_SUSPENDED;
345+
}
346+
335347
/**
336348
* Check whether indexer is working
337349
*

app/code/Magento/Indexer/Model/Processor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ public function reindexAllInvalid()
8181
$indexer->load($indexerId);
8282
$indexerConfig = $this->config->getIndexer($indexerId);
8383

84-
if ($indexer->isInvalid()) {
84+
if ($indexer->isInvalid() && !$indexer->isSuspended()) {
8585
// Skip indexers having shared index that was already complete
8686
$sharedIndex = $indexerConfig['shared_index'] ?? null;
8787
if (!in_array($sharedIndex, $this->sharedIndexesComplete)) {

app/code/Magento/Indexer/Model/ResourceModel/Indexer/State.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,14 @@ protected function prepareDataForUpdate($object)
3131
$data = parent::prepareDataForUpdate($object);
3232

3333
if (isset($data['status']) && StateInterface::STATUS_VALID === $data['status']) {
34+
$condition = $this->getConnection()->quoteInto('status IN (?)',
35+
[
36+
StateInterface::STATUS_WORKING,
37+
StateInterface::STATUS_SUSPENDED
38+
]
39+
);
3440
$data['status'] = $this->getConnection()->getCheckSql(
35-
$this->getConnection()->quoteInto('status = ?', StateInterface::STATUS_WORKING),
41+
$condition,
3642
$this->getConnection()->quote($data['status']),
3743
'status'
3844
);
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Indexer\Plugin\Mview;
9+
10+
use Magento\Framework\Indexer\ConfigInterface;
11+
use Magento\Framework\Indexer\IndexerRegistry;
12+
use Magento\Framework\Indexer\StateInterface;
13+
use Magento\Framework\Mview\ViewInterface;
14+
use Psr\Log\LoggerInterface;
15+
16+
/**
17+
* Plugin to prevent updating a view if the associated indexer is suspended
18+
*/
19+
class ViewUpdatePlugin
20+
{
21+
/**
22+
* @var IndexerRegistry
23+
*/
24+
private IndexerRegistry $indexerRegistry;
25+
26+
/**
27+
* @var ConfigInterface
28+
*/
29+
private ConfigInterface $indexerConfig;
30+
31+
/**
32+
* @var LoggerInterface
33+
*/
34+
private LoggerInterface $logger;
35+
36+
/**
37+
* @param IndexerRegistry $indexerRegistry
38+
* @param ConfigInterface $indexerConfig
39+
* @param LoggerInterface $logger
40+
*/
41+
public function __construct(
42+
IndexerRegistry $indexerRegistry,
43+
ConfigInterface $indexerConfig,
44+
LoggerInterface $logger
45+
) {
46+
$this->indexerRegistry = $indexerRegistry;
47+
$this->indexerConfig = $indexerConfig;
48+
$this->logger = $logger;
49+
}
50+
51+
/**
52+
* Prevent updating a view if the associated indexer is suspended
53+
*
54+
* @param ViewInterface $subject
55+
* @param callable $proceed
56+
* @return void
57+
*/
58+
public function aroundUpdate(ViewInterface $subject, callable $proceed): void
59+
{
60+
$indexerId = $this->mapViewIdToIndexerId($subject->getId());
61+
62+
if ($indexerId === null) {
63+
$proceed();
64+
return;
65+
}
66+
67+
$indexer = $this->indexerRegistry->get($indexerId);
68+
69+
if ($indexer->getStatus() != StateInterface::STATUS_SUSPENDED) {
70+
$proceed();
71+
} else {
72+
$this->logger->info(
73+
"Indexer {$indexer->getId()} is suspended. The view {$subject->getId()} will not be updated."
74+
);
75+
}
76+
}
77+
78+
/**
79+
* Map view ID to indexer ID
80+
*
81+
* @param string $viewId
82+
* @return string|null
83+
*/
84+
private function mapViewIdToIndexerId(string $viewId): ?string
85+
{
86+
foreach ($this->indexerConfig->getIndexers() as $indexerId => $config) {
87+
if (isset($config['view_id']) && $config['view_id'] === $viewId) {
88+
return $indexerId;
89+
}
90+
}
91+
return null;
92+
}
93+
}

app/code/Magento/Indexer/etc/di.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
<item name="info" xsi:type="object">Magento\Indexer\Console\Command\IndexerInfoCommand</item>
5959
<item name="reindex" xsi:type="object">Magento\Indexer\Console\Command\IndexerReindexCommand</item>
6060
<item name="set-mode" xsi:type="object">Magento\Indexer\Console\Command\IndexerSetModeCommand</item>
61+
<item name="set-status" xsi:type="object">Magento\Indexer\Console\Command\IndexerSetStatusCommand</item>
6162
<item name="show-mode" xsi:type="object">Magento\Indexer\Console\Command\IndexerShowModeCommand</item>
6263
<item name="status" xsi:type="object">Magento\Indexer\Console\Command\IndexerStatusCommand</item>
6364
<item name="reset" xsi:type="object">Magento\Indexer\Console\Command\IndexerResetStateCommand</item>
@@ -78,4 +79,7 @@
7879
<type name="Magento\Framework\Indexer\CacheContext">
7980
<plugin name="defer_cache_cleaning" type="Magento\Indexer\Model\Indexer\DeferCacheCleaning" />
8081
</type>
82+
<type name="Magento\Framework\Mview\ViewInterface">
83+
<plugin name="skip_suspended_indexer_mview_update" type="Magento\Indexer\Plugin\Mview\ViewUpdatePlugin" sortOrder="10"/>
84+
</type>
8185
</config>

lib/internal/Magento/Framework/Indexer/StateInterface.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface StateInterface
1717
const STATUS_WORKING = 'working';
1818
const STATUS_VALID = 'valid';
1919
const STATUS_INVALID = 'invalid';
20+
const STATUS_SUSPENDED = 'suspended';
2021

2122
/**
2223
* Return indexer id
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
/**
3+
* Copyright © Magento, Inc. All rights reserved.
4+
* See COPYING.txt for license details.
5+
*/
6+
declare(strict_types=1);
7+
8+
namespace Magento\Framework\Indexer;
9+
10+
/**
11+
* Interface for indexers that can be suspended
12+
*/
13+
interface SuspendableIndexerInterface extends IndexerInterface
14+
{
15+
/**
16+
* Check whether indexer is suspended
17+
*
18+
* @return bool
19+
*/
20+
public function isSuspended(): bool;
21+
}

0 commit comments

Comments
 (0)