Skip to content

Commit f6b1286

Browse files
committed
ACP2E-190: Empty categories due to catalog_category_product index issues
1 parent 207c959 commit f6b1286

File tree

9 files changed

+325
-11
lines changed

9 files changed

+325
-11
lines changed

app/code/Magento/Catalog/Model/Indexer/Category/Product.php

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@
55
*/
66
namespace Magento\Catalog\Model\Indexer\Category;
77

8+
use Magento\Framework\App\ObjectManager;
89
use Magento\Framework\Indexer\CacheContext;
10+
use Magento\Framework\Indexer\IndexMutexException;
11+
use Magento\Framework\Indexer\IndexMutexInterface;
912

1013
/**
1114
* Category product indexer
@@ -41,26 +44,35 @@ class Product implements \Magento\Framework\Indexer\ActionInterface, \Magento\Fr
4144
*/
4245
protected $cacheContext;
4346

47+
/**
48+
* @var IndexMutexInterface
49+
*/
50+
private $indexMutex;
51+
4452
/**
4553
* @param Product\Action\FullFactory $fullActionFactory
4654
* @param Product\Action\RowsFactory $rowsActionFactory
4755
* @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry
56+
* @param IndexMutexInterface|null $indexMutex
4857
*/
4958
public function __construct(
5059
Product\Action\FullFactory $fullActionFactory,
5160
Product\Action\RowsFactory $rowsActionFactory,
52-
\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry
61+
\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry,
62+
?IndexMutexInterface $indexMutex = null
5363
) {
5464
$this->fullActionFactory = $fullActionFactory;
5565
$this->rowsActionFactory = $rowsActionFactory;
5666
$this->indexerRegistry = $indexerRegistry;
67+
$this->indexMutex = $indexMutex ?? ObjectManager::getInstance()->get(IndexMutexInterface::class);
5768
}
5869

5970
/**
6071
* Execute materialization on ids entities
6172
*
6273
* @param int[] $ids
6374
* @return void
75+
* @throws IndexMutexException
6476
*/
6577
public function execute($ids)
6678
{
@@ -84,11 +96,17 @@ protected function registerEntities($ids)
8496
* Execute full indexation
8597
*
8698
* @return void
99+
* @throws IndexMutexException
87100
*/
88101
public function executeFull()
89102
{
90-
$this->fullActionFactory->create()->execute();
91-
$this->registerTags();
103+
$this->indexMutex->execute(
104+
static::INDEXER_ID,
105+
function () {
106+
$this->fullActionFactory->create()->execute();
107+
$this->registerTags();
108+
}
109+
);
92110
}
93111

94112
/**
@@ -107,6 +125,7 @@ protected function registerTags()
107125
*
108126
* @param int[] $ids
109127
* @return void
128+
* @throws IndexMutexException
110129
*/
111130
public function executeList(array $ids)
112131
{
@@ -118,6 +137,7 @@ public function executeList(array $ids)
118137
*
119138
* @param int $id
120139
* @return void
140+
* @throws IndexMutexException
121141
*/
122142
public function executeRow($id)
123143
{
@@ -129,18 +149,22 @@ public function executeRow($id)
129149
*
130150
* @param int[] $ids
131151
* @return $this
152+
* @throws IndexMutexException
132153
*/
133154
protected function executeAction($ids)
134155
{
135156
$ids = array_unique($ids);
136157
$indexer = $this->indexerRegistry->get(static::INDEXER_ID);
137158

138-
/** @var Product\Action\Rows $action */
139-
$action = $this->rowsActionFactory->create();
140159
if ($indexer->isScheduled()) {
141-
$action->execute($ids, true);
160+
$this->indexMutex->execute(
161+
static::INDEXER_ID,
162+
function () use ($ids) {
163+
$this->rowsActionFactory->create()->execute($ids, true);
164+
}
165+
);
142166
} else {
143-
$action->execute($ids);
167+
$this->rowsActionFactory->create()->execute($ids);
144168
}
145169

146170
return $this;

app/code/Magento/Catalog/Model/Indexer/Product/Category.php

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
*/
66
namespace Magento\Catalog\Model\Indexer\Product;
77

8+
use Magento\Framework\Indexer\IndexMutexInterface;
9+
810
/**
11+
* Product Categories indexer
12+
*
913
* @api
1014
* @since 100.0.2
1115
*/
@@ -20,13 +24,17 @@ class Category extends \Magento\Catalog\Model\Indexer\Category\Product
2024
* @param \Magento\Catalog\Model\Indexer\Category\Product\Action\FullFactory $fullActionFactory
2125
* @param Category\Action\RowsFactory $rowsActionFactory
2226
* @param \Magento\Framework\Indexer\IndexerRegistry $indexerRegistry
27+
* @param IndexMutexInterface|null $indexMutex
28+
* phpcs:disable Generic.CodeAnalysis.UselessOverridingMethod.Found
2329
*/
2430
public function __construct(
2531
\Magento\Catalog\Model\Indexer\Category\Product\Action\FullFactory $fullActionFactory,
2632
Category\Action\RowsFactory $rowsActionFactory,
27-
\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry
33+
\Magento\Framework\Indexer\IndexerRegistry $indexerRegistry,
34+
?IndexMutexInterface $indexMutex = null
2835
) {
29-
parent::__construct($fullActionFactory, $rowsActionFactory, $indexerRegistry);
36+
//phpcs:enable
37+
parent::__construct($fullActionFactory, $rowsActionFactory, $indexerRegistry, $indexMutex);
3038
}
3139

3240
/**

app/code/Magento/Catalog/Test/Unit/Model/Indexer/Category/ProductTest.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Magento\Framework\Indexer\CacheContext;
1717
use Magento\Framework\Indexer\IndexerInterface;
1818
use Magento\Framework\Indexer\IndexerRegistry;
19+
use Magento\Framework\Indexer\IndexMutexInterface;
1920
use PHPUnit\Framework\MockObject\MockObject;
2021
use PHPUnit\Framework\TestCase;
2122

@@ -81,10 +82,21 @@ protected function setUp(): void
8182
['get']
8283
);
8384

85+
$indexMutexMock = $this->createMock(IndexMutexInterface::class);
86+
$indexMutexMock->method('execute')
87+
->willReturnCallback(
88+
function (string $indexerName, callable $callback) {
89+
if ($indexerName) {
90+
$callback();
91+
}
92+
}
93+
);
94+
8495
$this->model = new Product(
8596
$this->fullMock,
8697
$this->rowsMock,
87-
$this->indexerRegistryMock
98+
$this->indexerRegistryMock,
99+
$indexMutexMock
88100
);
89101

90102
$this->cacheContextMock = $this->createMock(CacheContext::class);

app/code/Magento/Catalog/Test/Unit/Model/Indexer/Product/CategoryTest.php

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use Magento\Framework\Indexer\CacheContext;
1717
use Magento\Framework\Indexer\IndexerInterface;
1818
use Magento\Framework\Indexer\IndexerRegistry;
19+
use Magento\Framework\Indexer\IndexMutexInterface;
1920
use PHPUnit\Framework\MockObject\MockObject;
2021
use PHPUnit\Framework\TestCase;
2122

@@ -84,10 +85,21 @@ protected function setUp(): void
8485
['get']
8586
);
8687

88+
$indexMutexMock = $this->createMock(IndexMutexInterface::class);
89+
$indexMutexMock->method('execute')
90+
->willReturnCallback(
91+
function (string $indexerName, callable $callback) {
92+
if ($indexerName) {
93+
$callback();
94+
}
95+
}
96+
);
97+
8798
$this->model = new Category(
8899
$this->fullMock,
89100
$this->rowsMock,
90-
$this->indexerRegistryMock
101+
$this->indexerRegistryMock,
102+
$indexMutexMock
91103
);
92104

93105
$this->cacheContextMock = $this->createMock(CacheContext::class);
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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\Model;
9+
10+
use Magento\Framework\Indexer\ConfigInterface;
11+
use Magento\Framework\Indexer\IndexMutexException;
12+
use Magento\Framework\Indexer\IndexMutexInterface;
13+
use Magento\Framework\Lock\LockManagerInterface;
14+
15+
/**
16+
* Intended to prevent race conditions between indexers using the same index table.
17+
*/
18+
class IndexMutex implements IndexMutexInterface
19+
{
20+
private const LOCK_PREFIX = 'indexer_lock_';
21+
22+
private const LOCK_TIMEOUT = 60;
23+
24+
/**
25+
* @var LockManagerInterface
26+
*/
27+
private $lockManager;
28+
29+
/**
30+
* @var ConfigInterface
31+
*/
32+
private $config;
33+
34+
/**
35+
* @var int
36+
*/
37+
private $lockWaitTimeout;
38+
39+
/**
40+
* @param LockManagerInterface $lockManager
41+
* @param ConfigInterface $config
42+
* @param int $lockWaitTimeout
43+
*/
44+
public function __construct(
45+
LockManagerInterface $lockManager,
46+
ConfigInterface $config,
47+
int $lockWaitTimeout = self::LOCK_TIMEOUT
48+
) {
49+
$this->lockManager = $lockManager;
50+
$this->lockWaitTimeout = $lockWaitTimeout;
51+
$this->config = $config;
52+
}
53+
54+
/**
55+
* @inheritdoc
56+
*/
57+
public function execute(string $indexerName, callable $callback): void
58+
{
59+
$lockName = $indexerName;
60+
$indexerConfig = $this->config->getIndexer($indexerName);
61+
if (isset($indexerConfig['shared_index'])) {
62+
$lockName = $indexerConfig['shared_index'];
63+
}
64+
65+
if ($this->lockManager->lock(self::LOCK_PREFIX . $lockName, $this->lockWaitTimeout)) {
66+
try {
67+
$callback();
68+
} finally {
69+
$this->lockManager->unlock(self::LOCK_PREFIX . $lockName);
70+
}
71+
} else {
72+
throw new IndexMutexException($indexerName);
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)