Skip to content

Commit 9649f62

Browse files
authored
Merge pull request #8106 from magento-performance/ACPT-992
ACPT-992: Warm config cache during config clean
2 parents c31925f + 3aa8188 commit 9649f62

File tree

9 files changed

+175
-18
lines changed

9 files changed

+175
-18
lines changed

app/code/Magento/Config/App/Config/Type/System.php

Lines changed: 99 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,22 @@
66

77
namespace Magento\Config\App\Config\Type;
88

9+
use Magento\Config\App\Config\Type\System\Reader;
10+
use Magento\Framework\App\Cache\StateInterface;
11+
use Magento\Framework\App\Cache\Type\Config;
912
use Magento\Framework\App\Config\ConfigSourceInterface;
1013
use Magento\Framework\App\Config\ConfigTypeInterface;
1114
use Magento\Framework\App\Config\Spi\PostProcessorInterface;
1215
use Magento\Framework\App\Config\Spi\PreProcessorInterface;
1316
use Magento\Framework\App\ObjectManager;
14-
use Magento\Config\App\Config\Type\System\Reader;
1517
use Magento\Framework\App\ScopeInterface;
1618
use Magento\Framework\Cache\FrontendInterface;
1719
use Magento\Framework\Cache\LockGuardedCacheLoader;
20+
use Magento\Framework\Encryption\Encryptor;
1821
use Magento\Framework\Lock\LockManagerInterface;
1922
use Magento\Framework\Serialize\SerializerInterface;
2023
use Magento\Store\Model\Config\Processor\Fallback;
21-
use Magento\Framework\Encryption\Encryptor;
2224
use Magento\Store\Model\ScopeInterface as StoreScope;
23-
use Magento\Framework\App\Cache\StateInterface;
24-
use Magento\Framework\App\Cache\Type\Config;
2525

2626
/**
2727
* System configuration type
@@ -292,11 +292,13 @@ private function loadScopeData($scopeType, $scopeId)
292292
}
293293

294294
$loadAction = function () use ($scopeType, $scopeId) {
295+
/* Note: configType . '_scopes' needs to be loaded first to avoid race condition where cache finishes
296+
saving after configType . '_' . $scopeType . '_' . $scopeId but before configType . '_scopes'. */
297+
$cachedScopeData = $this->cache->load($this->configType . '_scopes');
295298
$cachedData = $this->cache->load($this->configType . '_' . $scopeType . '_' . $scopeId);
296299
$scopeData = false;
297300
if ($cachedData === false) {
298301
if ($this->availableDataScopes === null) {
299-
$cachedScopeData = $this->cache->load($this->configType . '_scopes');
300302
if ($cachedScopeData !== false) {
301303
$serializedCachedData = $this->encryptor->decrypt($cachedScopeData);
302304
$this->availableDataScopes = $this->serializer->unserialize($serializedCachedData);
@@ -437,18 +439,102 @@ private function readData(): array
437439
*/
438440
public function clean()
439441
{
440-
$this->data = [];
441442
$cleanAction = function () {
442-
$this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]);
443+
$this->cacheData($this->readData()); // Note: If cache is enabled, pre-load the new config data.
443444
};
444-
445+
$this->data = [];
445446
if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) {
446-
return $cleanAction();
447+
// Note: If cache is disabled, we still clean cache in case it will be enabled later
448+
$this->cache->clean(\Zend_Cache::CLEANING_MODE_MATCHING_TAG, [self::CACHE_TAG]);
449+
return;
447450
}
451+
$this->lockQuery->lockedCleanData(self::$lockName, $cleanAction);
452+
}
448453

449-
$this->lockQuery->lockedCleanData(
450-
self::$lockName,
451-
$cleanAction
452-
);
454+
/**
455+
* Prepares data for cache by serializing and encrypting them
456+
*
457+
* Prepares data per scope to avoid reading data for all scopes on every request
458+
*
459+
* @param array $data
460+
* @return array
461+
*/
462+
private function prepareDataForCache(array $data) :array
463+
{
464+
$dataToSave = [];
465+
$dataToSave[] = [
466+
$this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data)),
467+
$this->configType,
468+
[System::CACHE_TAG]
469+
];
470+
$dataToSave[] = [
471+
$this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($data['default'])),
472+
$this->configType . '_default',
473+
[System::CACHE_TAG]
474+
];
475+
$scopes = [];
476+
foreach ([StoreScope::SCOPE_WEBSITES, StoreScope::SCOPE_STORES] as $curScopeType) {
477+
foreach ($data[$curScopeType] ?? [] as $curScopeId => $curScopeData) {
478+
$scopes[$curScopeType][$curScopeId] = 1;
479+
$dataToSave[] = [
480+
$this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($curScopeData)),
481+
$this->configType . '_' . $curScopeType . '_' . $curScopeId,
482+
[System::CACHE_TAG]
483+
];
484+
}
485+
}
486+
$dataToSave[] = [
487+
$this->encryptor->encryptWithFastestAvailableAlgorithm($this->serializer->serialize($scopes)),
488+
$this->configType . '_scopes',
489+
[System::CACHE_TAG]
490+
];
491+
return $dataToSave;
492+
}
493+
494+
/**
495+
* Cache prepared configuration data.
496+
*
497+
* Takes data prepared by prepareDataForCache
498+
*
499+
* @param array $dataToSave
500+
* @return void
501+
*/
502+
private function cachePreparedData(array $dataToSave) : void
503+
{
504+
foreach ($dataToSave as $datumToSave) {
505+
$this->cache->save($datumToSave[0], $datumToSave[1], $datumToSave[2]);
506+
}
507+
}
508+
509+
/**
510+
* Gets configuration then cleans and warms it while locked
511+
*
512+
* This is to reduce the lock time after flushing config cache.
513+
*
514+
* @param callable $cleaner
515+
* @return void
516+
*/
517+
public function cleanAndWarmDefaultScopeData(callable $cleaner)
518+
{
519+
if (!$this->cacheState->isEnabled(Config::TYPE_IDENTIFIER)) {
520+
$cleaner();
521+
return;
522+
}
523+
$loadAction = function () {
524+
return false;
525+
};
526+
$dataCollector = function () use ($cleaner) {
527+
/* Note: call to readData() needs to be inside lock to avoid race conditions such as multiple
528+
saves at the same time. */
529+
$newData = $this->readData();
530+
$preparedData = $this->prepareDataForCache($newData);
531+
unset($newData);
532+
$cleaner(); // Note: This is where other readers start waiting for us to finish saving cache.
533+
return $preparedData;
534+
};
535+
$dataSaver = function (array $preparedData) {
536+
$this->cachePreparedData($preparedData);
537+
};
538+
$this->lockQuery->lockedLoadData(self::$lockName, $loadAction, $dataCollector, $dataSaver);
453539
}
454540
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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\Config\Plugin\Framework\App\Cache\TypeList;
9+
10+
use Magento\Config\App\Config\Type\System;
11+
use Magento\Framework\App\Cache\Type\Config as TypeConfig;
12+
use Magento\Framework\App\Cache\TypeList;
13+
14+
/**
15+
* Plugin that for warms config cache when config cache is cleaned.
16+
* This is to reduce the lock time after flushing config cache.
17+
*/
18+
class WarmConfigCache
19+
{
20+
/**
21+
* @var System
22+
*/
23+
private $system;
24+
25+
/**
26+
* @param System $system
27+
*/
28+
public function __construct(System $system)
29+
{
30+
$this->system = $system;
31+
}
32+
33+
/**
34+
* Around plugin for cache's clean type method
35+
*
36+
* @param TypeList $subject
37+
* @param callable $proceed
38+
* @param string $typeCode
39+
* @return void
40+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
41+
*/
42+
public function aroundCleanType(TypeList $subject, callable $proceed, $typeCode)
43+
{
44+
if (TypeConfig::TYPE_IDENTIFIER !== $typeCode) {
45+
return $proceed($typeCode);
46+
}
47+
$cleaner = function () use ($proceed, $typeCode) {
48+
return $proceed($typeCode);
49+
};
50+
$this->system->cleanAndWarmDefaultScopeData($cleaner);
51+
}
52+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,4 +380,7 @@
380380
<argument name="configStructure" xsi:type="object">\Magento\Config\Model\Config\Structure\Proxy</argument>
381381
</arguments>
382382
</type>
383+
<type name="Magento\Framework\App\Cache\TypeList">
384+
<plugin name="warm_config_cache" type="Magento\Config\Plugin\Framework\App\Cache\TypeList\WarmConfigCache"/>
385+
</type>
383386
</config>

dev/tests/integration/testsuite/Magento/Analytics/Model/Config/Backend/EnabledTest.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ public function testDisable()
7373
$this->checkInitialStatus();
7474
$this->saveConfigValue(Enabled::XML_ENABLED_CONFIG_STRUCTURE_PATH, (string)Enabledisable::DISABLE_VALUE);
7575
$this->reinitableConfig->reinit();
76-
7776
$this->checkDisabledStatus();
7877
}
7978

@@ -83,8 +82,8 @@ public function testDisable()
8382
*/
8483
public function testReEnable()
8584
{
86-
$this->checkDisabledStatus();
8785
$this->saveConfigValue(Enabled::XML_ENABLED_CONFIG_STRUCTURE_PATH, (string)Enabledisable::ENABLE_VALUE);
86+
$this->reinitableConfig->reinit();
8887
$this->checkReEnabledStatus();
8988
}
9089

dev/tests/integration/testsuite/Magento/CatalogImportExport/Model/Import/ProductTest/ProductStockTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,15 +154,19 @@ public function testImportWithBackordersDisabled(): void
154154
*
155155
* @magentoDataFixture mediaImportImageFixture
156156
* @magentoDataFixture Magento/Catalog/_files/product_simple.php
157+
* @magentoDbIsolation disabled
157158
*/
158159
public function testProductStockStatusShouldBeUpdated()
159160
{
161+
$this->stockRegistryStorage->clean();
160162
$status = $this->stockRegistry->getStockStatusBySku('simple');
161163
$this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus());
162164
$this->importFile('disable_product.csv');
165+
$this->stockRegistryStorage->clean();
163166
$status = $this->stockRegistry->getStockStatusBySku('simple');
164167
$this->assertEquals(Stock::STOCK_OUT_OF_STOCK, $status->getStockStatus());
165168
$this->importDataForMediaTest('enable_product.csv');
169+
$this->stockRegistryStorage->clean();
166170
$status = $this->stockRegistry->getStockStatusBySku('simple');
167171
$this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus());
168172
}
@@ -177,17 +181,19 @@ public function testProductStockStatusShouldBeUpdated()
177181
*/
178182
public function testProductStockStatusShouldBeUpdatedOnSchedule()
179183
{
180-
/** * @var $indexProcessor \Magento\Indexer\Model\Processor */
181184
$indexProcessor = $this->objectManager->create(\Magento\Indexer\Model\Processor::class);
182185
$indexProcessor->updateMview();
186+
$this->stockRegistryStorage->clean();
183187
$status = $this->stockRegistry->getStockStatusBySku('simple');
184188
$this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus());
185189
$this->importDataForMediaTest('disable_product.csv');
186190
$indexProcessor->updateMview();
191+
$this->stockRegistryStorage->clean();
187192
$status = $this->stockRegistry->getStockStatusBySku('simple');
188193
$this->assertEquals(Stock::STOCK_OUT_OF_STOCK, $status->getStockStatus());
189194
$this->importDataForMediaTest('enable_product.csv');
190195
$indexProcessor->updateMview();
196+
$this->stockRegistryStorage->clean();
191197
$status = $this->stockRegistry->getStockStatusBySku('simple');
192198
$this->assertEquals(Stock::STOCK_IN_STOCK, $status->getStockStatus());
193199
}

dev/tests/integration/testsuite/Magento/Config/App/Config/Type/SystemTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,8 @@ public function testGetValueDefaultScope()
6161
*/
6262
public function testEnvGetValueStoreScope()
6363
{
64-
$this->system->clean();
6564
$_ENV['CONFIG__STORES__DEFAULT__ABC__QRS__XYZ'] = 'test_env_value';
65+
$this->system->clean();
6666

6767
$this->assertEquals(
6868
'value1.db.default.test',

dev/tests/integration/testsuite/Magento/Review/_files/config.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,17 @@
44
* See COPYING.txt for license details.
55
*/
66

7-
/** @var Value $config */
87
use Magento\Framework\App\Config\Value;
8+
use Magento\TestFramework\App\Config as AppConfig;
99

10+
/** @var Value $config */
1011
$config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class);
1112
$config->setPath('catalog/review/allow_guest');
1213
$config->setScope('default');
1314
$config->setScopeId(0);
1415
$config->setValue(1);
1516
$config->save();
17+
18+
/** @var AppConfig $appConfig */
19+
$appConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(AppConfig::class);
20+
$appConfig->clean();

dev/tests/integration/testsuite/Magento/Review/_files/disable_config.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,15 @@
66

77
/** @var Value $config */
88
use Magento\Framework\App\Config\Value;
9+
use Magento\TestFramework\App\Config as AppConfig;
910

1011
$config = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(Value::class);
1112
$config->setPath('catalog/review/allow_guest');
1213
$config->setScope('default');
1314
$config->setScopeId(0);
1415
$config->setValue(0);
1516
$config->save();
17+
18+
/** @var AppConfig $appConfig */
19+
$appConfig = \Magento\TestFramework\Helper\Bootstrap::getObjectManager()->create(AppConfig::class);
20+
$appConfig->clean();

dev/tests/integration/testsuite/Magento/Store/App/Config/Source/InitialConfigSourceTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
/**
2222
* Test that initial scopes config are loaded if database is available
2323
* @magentoAppIsolation enabled
24+
* @magentoCache config disabled
2425
*/
2526
class InitialConfigSourceTest extends TestCase
2627
{

0 commit comments

Comments
 (0)