diff --git a/vendor/magento/module-sales/Model/GridAsyncInsert.php b/vendor/magento/module-sales/Model/GridAsyncInsert.php index 43b4b3a5b346d..29a7097b2a0be 100644 --- a/vendor/magento/module-sales/Model/GridAsyncInsert.php +++ b/vendor/magento/module-sales/Model/GridAsyncInsert.php @@ -5,6 +5,9 @@ */ namespace Magento\Sales\Model; +use Magento\Framework\Lock\LockManagerInterface; +use Psr\Log\LoggerInterface; + /** * Sales entity grids indexing observer. * @@ -27,23 +30,46 @@ class GridAsyncInsert */ protected $globalConfig; + /** + * @var LockManagerInterface|null + */ + private $lockManager; + + /** + * @var LoggerInterface|null + */ + private $logger; + + /** + * @var string + */ + private $lockName; + /** * @param \Magento\Sales\Model\ResourceModel\GridInterface $entityGrid * @param \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig + * @param LockManagerInterface|null $lockManager + * @param LoggerInterface|null $logger + * @param string $lockName */ public function __construct( \Magento\Sales\Model\ResourceModel\GridInterface $entityGrid, - \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig + \Magento\Framework\App\Config\ScopeConfigInterface $globalConfig, + ?LockManagerInterface $lockManager = null, + ?LoggerInterface $logger = null, + string $lockName = '' ) { $this->entityGrid = $entityGrid; $this->globalConfig = $globalConfig; + $this->lockManager = $lockManager; + $this->logger = $logger; + $this->lockName = $lockName; } /** - * Handles asynchronous insertion of the new entity into - * corresponding grid during cron job. + * Handles asynchronous insertion of the new entity into corresponding grid during cron job. * - * Also method is used in the next events: + * Also, method is used in the next events: * * - config_data_dev_grid_async_indexing_disabled * @@ -55,7 +81,23 @@ public function __construct( public function asyncInsert() { if ($this->globalConfig->getValue('dev/grid/async_indexing')) { - $this->entityGrid->refreshBySchedule(); + if ($this->lockManager && $this->lockName !== '') { + if (!$this->lockManager->lock($this->lockName, 0)) { + if ($this->logger) { + $this->logger->warning( + sprintf('Grid async insert is locked: %s, skipping run', $this->lockName) + ); + } + return; + } + try { + $this->entityGrid->refreshBySchedule(); + } finally { + $this->lockManager->unlock($this->lockName); + } + } else { + $this->entityGrid->refreshBySchedule(); + } } } } diff --git a/vendor/magento/module-sales/Model/ResourceModel/Grid.php b/vendor/magento/module-sales/Model/ResourceModel/Grid.php index 70588c3c5140b..af13c039651bc 100644 --- a/vendor/magento/module-sales/Model/ResourceModel/Grid.php +++ b/vendor/magento/module-sales/Model/ResourceModel/Grid.php @@ -54,7 +54,12 @@ class Grid extends AbstractGrid /** * Order grid rows batch size */ - const BATCH_SIZE = 100; + public const BATCH_SIZE = 100; + + /** + * Maximum reconciliation iterations per cron run. + */ + private const MAX_REFRESH_ITERATIONS = 1000; /** * @param Context $context @@ -130,24 +135,28 @@ public function refresh($value, $field = null) public function refreshBySchedule() { $lastUpdatedAt = null; - $notSyncedIds = $this->notSyncedDataProvider->getIds($this->mainTableName, $this->gridTableName); - foreach (array_chunk($notSyncedIds, self::BATCH_SIZE) as $bunch) { - $select = $this->getGridOriginSelect()->where($this->mainTableName . '.entity_id IN (?)', $bunch); - $fetchResult = $this->getConnection()->fetchAll($select); - $this->getConnection()->insertOnDuplicate( - $this->getTable($this->gridTableName), - $fetchResult, - array_keys($this->columns) - ); - - $timestamps = array_column($fetchResult, 'updated_at'); - if ($timestamps) { - $lastUpdatedAt = max(max($timestamps), $lastUpdatedAt); + $iteration = 0; + while ($iteration < self::MAX_REFRESH_ITERATIONS) { + $iteration++; + $notSyncedIds = $this->notSyncedDataProvider->getIds($this->mainTableName, $this->gridTableName); + if (empty($notSyncedIds)) { + break; + } + foreach (array_chunk($notSyncedIds, self::BATCH_SIZE) as $bunch) { + $select = $this->getGridOriginSelect()->where($this->mainTableName . '.entity_id IN (?)', $bunch); + $fetchResult = $this->getConnection()->fetchAll($select); + $this->getConnection()->insertOnDuplicate( + $this->getTable($this->gridTableName), + $fetchResult, + array_keys($this->columns) + ); + + $timestamps = array_column($fetchResult, 'updated_at'); + if ($timestamps) { + $lastUpdatedAt = max(max($timestamps), $lastUpdatedAt); + $this->lastUpdateTimeCache->save($this->gridTableName, $lastUpdatedAt); + } } - } - - if ($lastUpdatedAt) { - $this->lastUpdateTimeCache->save($this->gridTableName, $lastUpdatedAt); } } diff --git a/vendor/magento/module-sales/Model/ResourceModel/Provider/Query/IdListBuilder.php b/vendor/magento/module-sales/Model/ResourceModel/Provider/Query/IdListBuilder.php index 193c161da11fe..7788f1ccee66b 100644 --- a/vendor/magento/module-sales/Model/ResourceModel/Provider/Query/IdListBuilder.php +++ b/vendor/magento/module-sales/Model/ResourceModel/Provider/Query/IdListBuilder.php @@ -82,14 +82,20 @@ private function getConnection(): AdapterInterface } /** - * Builds select object. + * Builds a select object. * * @param string $mainTableName * @param string $gridTableName + * @param int|null $startEntityId + * @param int|null $endEntityId * @return Select */ - public function build(string $mainTableName, string $gridTableName): Select - { + public function build( + string $mainTableName, + string $gridTableName, + ?int $startEntityId = null, + ?int $endEntityId = null + ): Select { $select = $this->getConnection()->select() ->from(['main_table' => $mainTableName], ['main_table.entity_id']) ->joinLeft( @@ -98,6 +104,12 @@ public function build(string $mainTableName, string $gridTableName): Select [] ); + if ($startEntityId !== null) { + $select->where('main_table.entity_id > ?', $startEntityId); + } + if ($endEntityId !== null) { + $select->where('main_table.entity_id <= ?', $endEntityId); + } $select->where('grid_table.entity_id IS NULL'); $select->limit(Grid::BATCH_SIZE); foreach ($this->additionalGridTables as $table) { diff --git a/vendor/magento/module-sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php b/vendor/magento/module-sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php index ea62519573787..95a84ea00eb7f 100644 --- a/vendor/magento/module-sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php +++ b/vendor/magento/module-sales/Model/ResourceModel/Provider/UpdatedIdListProvider.php @@ -5,6 +5,7 @@ */ namespace Magento\Sales\Model\ResourceModel\Provider; +use Magento\Framework\FlagManager; use Magento\Framework\App\ObjectManager; use Magento\Framework\App\ResourceConnection; use Magento\Framework\DB\Adapter\AdapterInterface; @@ -15,6 +16,16 @@ */ class UpdatedIdListProvider implements NotSyncedDataProviderInterface { + /** + * Number of entity IDs scanned per reconciliation range. + */ + private const ENTITY_ID_SCAN_RANGE = 10000; + + /** + * Prefix for persisted per-grid cursor flag. + */ + private const GRID_CURSOR_FLAG_PREFIX = 'sales_grid_async_last_entity_id_'; + /** * @var ResourceConnection */ @@ -30,17 +41,25 @@ class UpdatedIdListProvider implements NotSyncedDataProviderInterface */ private $idListQueryBuilder; + /** + * @var FlagManager + */ + private $flagManager; + /** * NotSyncedDataProvider constructor. * @param ResourceConnection $resourceConnection * @param IdListBuilder|null $idListQueryBuilder + * @param FlagManager|null $flagManager */ public function __construct( ResourceConnection $resourceConnection, - ?IdListBuilder $idListQueryBuilder = null + ?IdListBuilder $idListQueryBuilder = null, + ?FlagManager $flagManager = null ) { $this->resourceConnection = $resourceConnection; $this->idListQueryBuilder = $idListQueryBuilder ?? ObjectManager::getInstance()->get(IdListBuilder::class); + $this->flagManager = $flagManager ?? ObjectManager::getInstance()->get(FlagManager::class); } /** @@ -50,8 +69,29 @@ public function getIds($mainTableName, $gridTableName) { $mainTableName = $this->resourceConnection->getTableName($mainTableName); $gridTableName = $this->resourceConnection->getTableName($gridTableName); - $select = $this->idListQueryBuilder->build($mainTableName, $gridTableName); - return $this->getConnection()->fetchAll($select, [], \Zend_Db::FETCH_COLUMN); + $maxEntityId = $this->getMaxEntityId($mainTableName); + if ($maxEntityId === 0) { + return []; + } + + $cursorFlagCode = self::GRID_CURSOR_FLAG_PREFIX . $gridTableName; + $lastProcessedEntityId = $this->getLastProcessedEntityId($cursorFlagCode, $maxEntityId); + if ($lastProcessedEntityId >= $maxEntityId) { + return []; + } + $scanUntilEntityId = min($lastProcessedEntityId + self::ENTITY_ID_SCAN_RANGE, $maxEntityId); + $select = $this->idListQueryBuilder->build( + $mainTableName, + $gridTableName, + $lastProcessedEntityId, + $scanUntilEntityId + ); + $ids = $this->getConnection()->fetchAll($select, [], \Zend_Db::FETCH_COLUMN); + if (empty($ids)) { + $this->flagManager->saveFlag($cursorFlagCode, $scanUntilEntityId); + } + + return $ids; } /** @@ -67,4 +107,38 @@ private function getConnection() return $this->connection; } + + /** + * Returns maximum entity ID for source table. + * + * @param string $mainTableName + * @return int + */ + private function getMaxEntityId(string $mainTableName): int + { + $select = $this->getConnection()->select()->from( + ['main_table' => $mainTableName], + ['max_entity_id' => new \Zend_Db_Expr('MAX(main_table.entity_id)')] + ); + $maxEntityId = (int)$this->getConnection()->fetchOne($select); + + return max(0, $maxEntityId); + } + + /** + * Returns last processed entity ID for a grid cursor. + * + * @param string $cursorFlagCode + * @param int $maxEntityId + * @return int + */ + private function getLastProcessedEntityId(string $cursorFlagCode, int $maxEntityId): int + { + $storedCursor = $this->flagManager->getFlagData($cursorFlagCode); + if (!is_numeric($storedCursor)) { + return max(0, $maxEntityId - self::ENTITY_ID_SCAN_RANGE); + } + + return max(0, min((int)$storedCursor, $maxEntityId)); + } } diff --git a/vendor/magento/module-sales/etc/di.xml b/vendor/magento/module-sales/etc/di.xml index 3234ad12b3f3a..17488ec3cc86d 100644 --- a/vendor/magento/module-sales/etc/di.xml +++ b/vendor/magento/module-sales/etc/di.xml @@ -267,21 +267,29 @@ Magento\Sales\Model\ResourceModel\Order\Grid + Magento\Framework\Lock\LockManagerInterface + grid_async_insert_sales_order Magento\Sales\Model\ResourceModel\Order\Invoice\Grid + Magento\Framework\Lock\LockManagerInterface + grid_async_insert_sales_invoice ShipmentGridAggregator + Magento\Framework\Lock\LockManagerInterface + grid_async_insert_sales_shipment CreditmemoGridAggregator + Magento\Framework\Lock\LockManagerInterface + grid_async_insert_sales_creditmemo