Commit e8d2e371 authored by Helmut Hummel's avatar Helmut Hummel Committed by Benni Mack
Browse files

[TASK] Improve dependency injection container caching

Disallow disabling and flushing the DI cache and
base the cache identifier as well on the currently
installed extensions.

A disabled DI cache creates an unbearable performance hit,
so that disabling won't make sense anyway.
Disallowing flushing that cache will not flush the DI cache
when caches are flushed using the regular backend UI,
but only when caches in install tool are flushed.

Last but not least, the install tool cache flushing is
changed to not bypass the caching API any more by removing complete
caching folders or caching database tables.
Instead the CacheManager is now used twice, once with basic
caching configuration and a second time with caching configuration
that is provided by extensions (if any).

Releases: master
Resolves: #90418
Change-Id: Idc3d053e181c909ccd662065a9c1ab7a893fa9ac
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63288


Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Georg Ringer's avatarGeorg Ringer <georg.ringer@gmail.com>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
parent 17534d8f
......@@ -26,8 +26,10 @@ use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Exception\InvalidBackendException;
use TYPO3\CMS\Core\Cache\Exception\InvalidCacheException;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Cache\Frontend\VariableFrontend;
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
use TYPO3\CMS\Core\DependencyInjection\Cache\ContainerBackend;
use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
use TYPO3\CMS\Core\Imaging\IconRegistry;
use TYPO3\CMS\Core\IO\PharStreamWrapperInterceptor;
......@@ -111,6 +113,7 @@ class Bootstrap
static::setMemoryLimit();
$assetsCache = static::createCache('assets', $disableCaching);
$dependencyInjectionContainerCache = static::createCache('di');
$bootState = new \stdClass;
$bootState->done = false;
......@@ -121,6 +124,7 @@ class Bootstrap
ApplicationContext::class => Environment::getContext(),
ConfigurationManager::class => $configurationManager,
LogManager::class => $logManager,
'cache.di' => $dependencyInjectionContainerCache,
'cache.core' => $coreCache,
'cache.assets' => $assetsCache,
PackageManager::class => $packageManager,
......@@ -129,7 +133,7 @@ class Bootstrap
'boot.state' => $bootState,
]);
$container = $builder->createDependencyInjectionContainer($packageManager, $coreCache, $failsafe);
$container = $builder->createDependencyInjectionContainer($packageManager, $dependencyInjectionContainerCache, $failsafe);
// Push the container to GeneralUtility as we want to make sure its
// makeInstance() method creates classes using the container from now on.
......@@ -310,7 +314,11 @@ class Bootstrap
*/
public static function createCache(string $identifier, bool $disableCaching = false): FrontendInterface
{
$configuration = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'][$identifier] ?? [];
$cacheConfigurations = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] ?? [];
$cacheConfigurations['di']['frontend'] = PhpFrontend::class;
$cacheConfigurations['di']['backend'] = ContainerBackend::class;
$cacheConfigurations['di']['options'] = [];
$configuration = $cacheConfigurations[$identifier] ?? [];
$frontend = $configuration['frontend'] ?? VariableFrontend::class;
$backend = $configuration['backend'] ?? Typo3DatabaseBackend::class;
......
<?php
declare(strict_types = 1);
namespace TYPO3\CMS\Core\DependencyInjection\Cache;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Cache\Backend\SimpleFileBackend;
/**
* @internal
*/
class ContainerBackend extends SimpleFileBackend
{
public function flush()
{
// disable cache flushing
}
public function set($entryIdentifier, $data, array $tags = [], $lifetime = null)
{
// Remove stale cache files, once a new DI container was built
parent::flush();
parent::set($entryIdentifier, $data, $tags, $lifetime);
}
}
......@@ -22,6 +22,7 @@ use Symfony\Component\DependencyInjection\Dumper\PhpDumper;
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Cache\Frontend\PhpFrontend;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Information\Typo3Version;
use TYPO3\CMS\Core\Package\PackageManager;
......@@ -62,6 +63,9 @@ class ContainerBuilder
*/
public function createDependencyInjectionContainer(PackageManager $packageManager, FrontendInterface $cache, bool $failsafe = false): ContainerInterface
{
if (!$cache instanceof PhpFrontend) {
throw new \RuntimeException('Cache must be instance of PhpFrontend', 1582022226);
}
$serviceProviderRegistry = new ServiceProviderRegistry($packageManager, $failsafe);
if ($failsafe) {
......@@ -70,29 +74,16 @@ class ContainerBuilder
$container = null;
$cacheIdentifier = $this->getCacheIdentifier();
$cacheIdentifier = $this->getCacheIdentifier($packageManager);
$containerClassName = $cacheIdentifier;
$hasCache = $cache->requireOnce($cacheIdentifier) !== false;
if (!$hasCache) {
$containerBuilder = $this->buildContainer($packageManager, $serviceProviderRegistry);
$code = $this->dumpContainer($containerBuilder, $cache);
// In theory we could use the $containerBuilder directly as $container,
// but as we patch the compiled source to use
// GeneralUtility::makeInstanceForDi, we need to use the compiled container.
// Once we remove support for singletons configured in ext_localconf.php
// and $GLOBALS['TYPO_CONF_VARS']['SYS']['Objects'], we can remove this,
// and use `$container = $containerBuilder` directly
$hasCache = $cache->requireOnce($cacheIdentifier) !== false;
if (!$hasCache) {
// $cacheIdentifier may be unavailable if the 'core' cache iis configured to
// use the NullBackend
eval($code);
}
$this->dumpContainer($containerBuilder, $cache, $cacheIdentifier);
$cache->requireOnce($cacheIdentifier);
}
$fullyQualifiedContainerClassName = '\\' . $containerClassName;
$container = new $fullyQualifiedContainerClassName();
$container = new $containerClassName();
foreach ($this->defaultServices as $id => $service) {
$container->set('_early.' . $id, $service);
......@@ -129,7 +120,7 @@ class ContainerBuilder
// Store defaults entries in the DIC container
// We need to use a workaround using aliases for synthetic services
// But that's common in symfony (same technique is used to provide the
// symfony container interface as well.
// Symfony container interface as well).
foreach (array_keys($this->defaultServices) as $id) {
$syntheticId = '_early.' . $id;
$containerBuilder->register($syntheticId)->setSynthetic(true)->setPublic(true);
......@@ -144,11 +135,11 @@ class ContainerBuilder
/**
* @param SymfonyContainerBuilder $containerBuilder
* @param FrontendInterface $cache
* @param string $cacheIdentifier
* @return string
*/
protected function dumpContainer(SymfonyContainerBuilder $containerBuilder, FrontendInterface $cache): string
protected function dumpContainer(SymfonyContainerBuilder $containerBuilder, FrontendInterface $cache, string $cacheIdentifier): string
{
$cacheIdentifier = $this->getCacheIdentifier();
$containerClassName = $cacheIdentifier;
$phpDumper = new PhpDumper($containerBuilder);
......@@ -165,18 +156,20 @@ class ContainerBuilder
}
/**
* @param PackageManager $packageManager
* @return string
*/
protected function getCacheIdentifier(): string
protected function getCacheIdentifier(PackageManager $packageManager): string
{
return $this->cacheIdentifier ?? $this->createCacheIdentifier();
return $this->cacheIdentifier ?? $this->createCacheIdentifier($packageManager->getCacheIdentifier());
}
/**
* @param string|null $additionalIdentifier
* @return string
*/
protected function createCacheIdentifier(): string
protected function createCacheIdentifier(string $additionalIdentifier = null): string
{
return $this->cacheIdentifier = 'DependencyInjectionContainer_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . 'DependencyInjectionContainer');
return $this->cacheIdentifier = 'DependencyInjectionContainer_' . sha1((string)(new Typo3Version()) . Environment::getProjectPath() . ($additionalIdentifier ?? '') . 'DependencyInjectionContainer');
}
}
......@@ -139,9 +139,10 @@ class PackageManager implements SingletonInterface
}
/**
* @return string
* @internal
* @return string | null
*/
protected function getCacheIdentifier()
public function getCacheIdentifier()
{
if ($this->cacheIdentifier === null) {
$mTime = @filemtime($this->packageStatesPathAndFilename);
......
......@@ -65,10 +65,12 @@ class ServiceProvider extends AbstractServiceProvider
$defaultCaches = [
$container->get('cache.core'),
$container->get('cache.assets'),
$container->get('cache.di'),
];
$cacheManager = self::new($container, Cache\CacheManager::class, [$disableCaching]);
$cacheManager->setCacheConfigurations($cacheConfigurations);
$cacheConfigurations['di']['groups'] = ['system'];
foreach ($defaultCaches as $cache) {
$cacheManager->registerCache($cache, $cacheConfigurations[$cache->getIdentifier()]['groups'] ?? ['all']);
}
......
......@@ -14,7 +14,6 @@ namespace TYPO3\CMS\Install\Service;
* The TYPO3 project - inspiring people to share!
*/
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Utility\GeneralUtility;
......@@ -24,6 +23,10 @@ use TYPO3\CMS\Core\Utility\GeneralUtility;
*/
class ClearCacheService
{
private const legacyDatabaseCacheTables = [
'cache_treelist',
];
/**
* @var LateBootService
*/
......@@ -49,41 +52,47 @@ class ClearCacheService
*/
public function clearAll()
{
// Delete typo3temp/Cache
GeneralUtility::flushDirectory(Environment::getVarPath() . '/cache', true, true);
// Get all table names from Default connection starting with 'cf_' and truncate them
// Low level flush of legacy database cache tables
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
$connection = $connectionPool->getConnectionByName('Default');
$tableNames = $connection->getSchemaManager()->listTableNames();
foreach ($tableNames as $tableName) {
if (strpos($tableName, 'cf_') === 0 || $tableName === 'cache_treelist') {
$connection->truncate($tableName);
}
foreach (self::legacyDatabaseCacheTables as $tableName) {
$connection = $connectionPool->getConnectionForTable($tableName);
$connection->truncate($tableName);
}
// check tables on other connections
$remappedTables = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
? array_keys((array)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
: [];
foreach ($remappedTables as $tableName) {
if (strpos((string)$tableName, 'cf_') === 0 || $tableName === 'cache_treelist') {
$connectionPool->getConnectionForTable($tableName)->truncate($tableName);
}
}
// Flush all caches defined in TYPO3_CONF_VARS, but not the ones defined by extensions in ext_localconf.php
$baseCaches = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] ?? [];
$this->flushCaches($baseCaches);
// Remove DI container cache (this might be removed in preference of functionality to rebuild this cache)
// We need to remove using the remove method because the DI cache backend disables the flush method
$container = $this->lateBootService->getContainer();
$container->get('cache.di')->remove(get_class($container));
// From this point on, the code may fatal, if some broken extension is loaded.
$this->lateBootService->loadExtLocalconfDatabaseAndExtTables();
// The cache manager is already instantiated in the install tool
// (both in the failsafe and the late boot container), but
// with some hacked settings to disable caching of extbase and fluid.
// We want a "fresh" object here to operate on a different cache setup.
// cacheManager implements SingletonInterface, so the only way to get a "fresh"
// instance is by circumventing makeInstance and/or the objectManager and
// using new directly!
$extensionCaches = $GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations'] ?? [];
// Loose comparison on purpose to allow changed ordering of the array
if ($baseCaches != $extensionCaches) {
// When configuration has changed during loading of extensions (due to ext_localconf.php), flush all caches again
$this->flushCaches($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
}
}
/**
* The cache manager is already instantiated in the install tool
* (both in the failsafe and the late boot container), but
* with settings to disable caching (all caches using NullBackend).
* We want a "fresh" object here to operate with the really configured cache backends.
* CacheManager implements SingletonInterface, so the only way to get a "fresh"
* instance is by circumventing makeInstance and using new directly!
*
* @param array $cacheConfiguration
*/
private function flushCaches(array $cacheConfiguration): void
{
$cacheManager = new \TYPO3\CMS\Core\Cache\CacheManager();
$cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
$cacheManager->setCacheConfigurations($cacheConfiguration);
$cacheManager->flushCaches();
}
}
......@@ -69,15 +69,12 @@ class LateBootService
private function prepareContainer(): ContainerInterface
{
$packageManager = $this->failsafeContainer->get(PackageManager::class);
// Use caching for the full boot – uncached symfony autowiring for every install-tool lateboot request would be too slow.
$disableCaching = false;
$coreCache = Bootstrap::createCache('core', $disableCaching);
$dependencyInjectionContainerCache = $this->failsafeContainer->get('cache.di');
$failsafe = false;
// Build a non-failsafe container which is required for loading ext_localconf
return $this->container = $this->containerBuilder->createDependencyInjectionContainer($packageManager, $coreCache, $failsafe);
return $this->container = $this->containerBuilder->createDependencyInjectionContainer($packageManager, $dependencyInjectionContainerCache, $failsafe);
}
/**
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment