Skip to content
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

Add Additional Measure to Ensure Indexers Remain in Locked Mode #9

Merged
merged 8 commits into from
Feb 25, 2023
Merged
Show file tree
Hide file tree
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
77 changes: 77 additions & 0 deletions Cron/SetIndexerMode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php
/**
* Copyright © element119. All rights reserved.
* See LICENCE.txt for licence details.
*/
declare(strict_types=1);

namespace Element119\IndexerDeployConfig\Cron;

use Element119\IndexerDeployConfig\Model\IndexerConfig;
use Element119\IndexerDeployConfig\Scope\ModuleConfig;
use Magento\Indexer\Console\Command\IndexerSetModeCommand as IndexerMode;
use Magento\Indexer\Model\Indexer\CollectionFactory as IndexerCollectionFactory;
use Magento\Indexer\Model\Indexer;

class SetIndexerMode
{
/** @var IndexerConfig */
private IndexerConfig $indexerConfig;

/** @var ModuleConfig */
private ModuleConfig $moduleConfig;

/** @var IndexerCollectionFactory */
private IndexerCollectionFactory $indexerCollectionFactory;

/**
* @param IndexerConfig $indexerConfig
* @param ModuleConfig $moduleConfig
* @param IndexerCollectionFactory $indexerCollectionFactory
*/
public function __construct(
IndexerConfig $indexerConfig,
ModuleConfig $moduleConfig,
IndexerCollectionFactory $indexerCollectionFactory
) {
$this->indexerConfig = $indexerConfig;
$this->moduleConfig = $moduleConfig;
$this->indexerCollectionFactory = $indexerCollectionFactory;
}

/**
* Ensure indexers are in the mode they are configured to be locked to.
*
* Can be disabled in the admin:
* Stores -> Settings -> Configuration -> Advanced -> System -> Indexer Mode Locking -> Enable Cron Fallback -> No
*
* @return void
*/
public function execute(): void
{
if (!$this->moduleConfig->isCronFallbackEnabled()
|| !($indexerConfig = $this->indexerConfig->getFlatIndexerConfig())
) {
return; // fallback disabled or indexers are not locked, nothing to do
}

/** @var Indexer[] $allIndexers */
$allIndexers = $this->indexerCollectionFactory->create()->getItems();

foreach ($allIndexers as $indexer) {
foreach ($indexerConfig as $indexerId => $lockedMode) {
if ($indexer->getId() !== $indexerId) {
continue; // ensure we're looking at the same indexer in both loops
}

$indexerMode = $indexer->isScheduled()
? IndexerMode::INPUT_KEY_SCHEDULE
: IndexerMode::INPUT_KEY_REALTIME;

if ($indexerMode !== $lockedMode) {
$indexer->setScheduled($lockedMode === IndexerMode::INPUT_KEY_SCHEDULE);
}
}
}
}
}
94 changes: 89 additions & 5 deletions Model/IndexerConfig.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,43 @@
use Magento\Framework\App\DeploymentConfig;
use Magento\Framework\Exception\FileSystemException;
use Magento\Framework\Exception\RuntimeException;
use Magento\Indexer\Console\Command\IndexerSetModeCommand as IndexerMode;
use Magento\Indexer\Model\Indexer\CollectionFactory as IndexerCollectionFactory;
use Magento\Indexer\Model\Indexer;
use Psr\Log\LoggerInterface;

class IndexerConfig
{
/** @var DeploymentConfig */
private DeploymentConfig $deploymentConfig;

/** @var IndexerCollectionFactory */
private IndexerCollectionFactory $indexerCollectionFactory;

/** @var LoggerInterface */
private LoggerInterface $logger;

/** @var array */
private array $indexerConfig = [];

/** @var array */
private array $flatIndexerConfig = [];

/** @var array */
private array $viewIdToIndexerIdMap = [];

/**
* @param DeploymentConfig $deploymentConfig
* @param IndexerCollectionFactory $indexerCollectionFactory
* @param LoggerInterface $logger
*/
public function __construct(
DeploymentConfig $deploymentConfig,
IndexerCollectionFactory $indexerCollectionFactory,
LoggerInterface $logger
) {
$this->deploymentConfig = $deploymentConfig;
$this->indexerCollectionFactory = $indexerCollectionFactory;
$this->logger = $logger;
}

Expand All @@ -39,13 +57,79 @@ public function __construct(
*/
public function getIndexerConfig(): array
{
try {
return $this->deploymentConfig->get('indexers') ?? [];
} catch (FileSystemException | RunTimeException $e) {
$this->logger->error(__('Could not load indexer configuration from app/etc/config.php'));
if (!$this->indexerConfig) {
try {
$this->indexerConfig = $this->deploymentConfig->get('indexers');
} catch (FileSystemException|RunTimeException $e) {
$this->logger->error(__('Could not load indexer configuration from app/etc/config.php'));
}
}

return $this->indexerConfig;
}

/**
* Get indexer config as indexerId => lockedMode pairs.
*
* @return array
*/
public function getFlatIndexerConfig(): array
{
if (!$this->flatIndexerConfig) {
$flatIndexerConfig = [];
$indexerConfig = $this->getIndexerConfig();

return [];
foreach ($indexerConfig as $mode => $indexers) {
foreach ($indexers as $indexer) {
$flatIndexerConfig[$indexer] = $mode;
}
}

$this->flatIndexerConfig = $flatIndexerConfig;
}

return $this->flatIndexerConfig;
}

/**
* Determine whether a given indexer ID should be locked to a particular mode.
*
* @param string $indexerId
* @return bool
*/
public function isIndexerLocked(string $indexerId): bool
{
return array_key_exists($indexerId, $this->getFlatIndexerConfig());
}

/**
* Map view ID to indexer ID.
*
* @return array
*/
public function getViewIdToIndexerIdMap(): array
{
if (!$this->viewIdToIndexerIdMap) {
/** @var Indexer $indexer */
foreach ($this->indexerCollectionFactory->create()->getItems() as $indexer) {
$this->viewIdToIndexerIdMap[$indexer->getViewId()] = $indexer->getId();
}
}

return $this->viewIdToIndexerIdMap;
}

/**
* Get the indexer ID for the given view ID.
*
* @param string|null $viewId
* @return string
*/
public function getIndexerIdForViewId(?string $viewId): string
{
$map = $this->getViewIdToIndexerIdMap();

return array_key_exists($viewId, $map) ? $map[$viewId] : '';
}

/**
Expand Down
84 changes: 69 additions & 15 deletions Plugin/SetIndexerMode.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@

use Element119\IndexerDeployConfig\Model\IndexerConfig;
use Magento\Framework\Message\ManagerInterface as MessageManagerInterface;
use Magento\Framework\Mview\View\StateInterface;
use Magento\Indexer\Console\Command\IndexerSetModeCommand as IndexerMode;
use Magento\Indexer\Controller\Adminhtml\Indexer\MassChangelog;
use Magento\Indexer\Controller\Adminhtml\Indexer\MassOnTheFly;
use Magento\Indexer\Model\Indexer;

class SetIndexerMode
{
Expand Down Expand Up @@ -41,28 +41,82 @@ public function __construct(
}

/**
* Determine whether the indexer should be in scheduled mode or not based on deploy configuration.
* Ensure indexer mode remains in the mode defined by deploy config.
*
* @param Indexer $subject
* @param bool $scheduled
* @return array
* @param StateInterface $subject
* @param string $mode
* @return string[]
*/
public function beforeSetScheduled(
Indexer $subject,
bool $scheduled
public function beforeSetMode(
StateInterface $subject,
string $mode
): array {
$indexerConfig = $this->indexerConfig->getIndexerConfig();
$indexerId = $subject->getId();
if (!$this->indexerConfig->getIndexerConfig()
|| !($indexerId = $this->indexerConfig->getIndexerIdForViewId($subject->getViewId()))
|| !$this->indexerConfig->isIndexerLocked($indexerId)
) {
return [$mode]; // no indexers should be locked, could not find indexer ID, or indexer is not mode-locked
}

$isBeingScheduled = $mode === 'enabled';
$shouldBeScheduled = $this->indexerConfig->indexerHasMode($indexerId, IndexerMode::INPUT_KEY_SCHEDULE);
$shouldBeRealTime = $this->indexerConfig->indexerHasMode($indexerId, IndexerMode::INPUT_KEY_REALTIME);

if ($this->indexerConfig->indexerHasMode($indexerId, IndexerMode::INPUT_KEY_SCHEDULE, $indexerConfig)) {
return [true];
if ($shouldBeScheduled && !$isBeingScheduled) {
return ['enabled']; // maintain on schedule status
} else if ($shouldBeRealTime && $isBeingScheduled) {
return ['disabled']; // maintain on save status
}

if ($this->indexerConfig->indexerHasMode($indexerId, IndexerMode::INPUT_KEY_REALTIME, $indexerConfig)) {
return [false];
return [$mode];
}

/**
* Ensure indexer mode remains in the mode defined by deploy config.
*
* @param $subject
* @param $key
* @param $value
* @return array
*/
public function beforeSetData(
$subject,
$key,
$value = null
) {
if (!($subject instanceof StateInterface)
|| $key !== 'mode'
|| !$this->indexerConfig->getIndexerConfig()
|| !($indexerId = $this->indexerConfig->getIndexerIdForViewId($subject->getViewId()))
|| !$this->indexerConfig->isIndexerLocked($indexerId)
) {
// not an indexer state, not setting mode data, no indexers should be locked, cannot find indexer ID, or indexer is not mode-locked
return [$key, $value];
}

$isBeingScheduled = null;
$shouldBeScheduled = $this->indexerConfig->indexerHasMode($indexerId, IndexerMode::INPUT_KEY_SCHEDULE);
$shouldBeRealTime = $this->indexerConfig->indexerHasMode($indexerId, IndexerMode::INPUT_KEY_REALTIME);

if ($key === (array)$key) {
$mode = array_key_exists('mode', $key) ? $key['mode'] : null;

if ($mode !== null) {
$isBeingScheduled = $mode === 'enabled';
}
} elseif ($value !== null) {
$isBeingScheduled = $value === 'enabled';
}

if ($isBeingScheduled !== null) {
if ($shouldBeScheduled && !$isBeingScheduled) {
$value = 'enabled'; // maintain on schedule status
} else if ($shouldBeRealTime && $isBeingScheduled) {
$value = 'disabled'; // maintain on save status
}
}

return [$scheduled];
return [$key, $value];
}

/**
Expand Down
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,15 @@ Coming soon...

<br>

### Indexer Mode Locking Cron Fallback
A new system configuration option allows you to enable a cron job that will ensure indexers are in the mode they are
supposed to be in, according to deployment config. This option can be found in `Stores -> Configuration -> Advanced ->
System -> Indexer Mode Locking`.

![indexer-mode-locking-cron-config](https://user-images.githubusercontent.com/40261741/221367876-d04e812d-9628-4bb2-a335-8532dd27299e.png)

<br>

### `indexer:lock-all` Command
The module adds a new `indexer:lock-all` command that you can use to lock the indexer modes via the command line.

Expand Down
35 changes: 35 additions & 0 deletions Scope/ModuleConfig.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php
/**
* Copyright © element119. All rights reserved.
* See LICENCE.txt for licence details.
*/
declare(strict_types=1);

namespace Element119\IndexerDeployConfig\Scope;

use Magento\Framework\App\Config\ScopeConfigInterface;

class ModuleConfig
{
private const XML_PATH_CRON_FALLBACK_ENABLED = 'system/e119_indexer_deploy_config/cron_enable';

/** @var ScopeConfigInterface */
private ScopeConfigInterface $scopeConfig;

/**
* @param ScopeConfigInterface $scopeConfig
*/
public function __construct(
ScopeConfigInterface $scopeConfig
) {
$this->scopeConfig = $scopeConfig;
}

/**
* @return bool
*/
public function isCronFallbackEnabled(): bool
{
return $this->scopeConfig->isSetFlag(self::XML_PATH_CRON_FALLBACK_ENABLED);
}
}
7 changes: 7 additions & 0 deletions etc/adminhtml/di.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@
-->
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
<!-- Ensure Indexer Mode Remains in the Mode Defined by Deploy Config -->
<type name="Magento\Framework\Mview\View\StateInterface">
<plugin name="config_based_indexer_mode"
type="Element119\IndexerDeployConfig\Plugin\SetIndexerMode"
sortOrder="999999"/>
</type>

<!-- Prevent Indexer Mode Changes from Save to Schedule -->
<type name="Magento\Indexer\Controller\Adminhtml\Indexer\MassChangelog">
<plugin name="config_based_indexer_mode_mass_action_schedule"
Expand Down
Loading