diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index 49e830b..cc492a3 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -42,6 +42,13 @@ class DatabaseBackend implements CacheBackendInterface { protected $checksumProvider; /** + * Per-bin database backend configuration. + * + * @var array + */ + protected $configuration; + + /** * Constructs a DatabaseBackend object. * * @param \Drupal\Core\Database\Connection $connection @@ -50,14 +57,17 @@ class DatabaseBackend implements CacheBackendInterface { * The cache tags checksum provider. * @param string $bin * The cache bin for which the object is created. + * @param array $configuration + * Per-bin database backend configuration. */ - public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin) { + public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin, array $configuration = []) { // All cache tables should be prefixed with 'cache_'. $bin = 'cache_' . $bin; $this->bin = $bin; $this->connection = $connection; $this->checksumProvider = $checksum_provider; + $this->configuration = $configuration; } /** @@ -150,6 +160,12 @@ protected function prepareItem($cache, $allow_invalid) { * Implements Drupal\Core\Cache\CacheBackendInterface::set(). */ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = array()) { + // Check if maximum expiration settings for cache bin is defined. If set, + // enforce an upper limit for the expiration to allow for garbage + // collection. + if (isset($this->configuration['max_expire']) && ($expire == Cache::PERMANENT || $expire > $this->configuration['max_expire'] + REQUEST_TIME)) { + $expire = $this->configuration['max_expire'] + REQUEST_TIME; + } Cache::validateTags($tags); $tags = array_unique($tags); // Sort the cache tags so that they are stored consistently in the database. @@ -205,11 +221,21 @@ public function setMultiple(array $items) { $values = array(); foreach ($items as $cid => $item) { + // Check if maximum expiration settings for cache bin is defined. If set, + // enforce an upper limit for the expiration to allow for garbage + // collection. + $max_expire = isset($this->configuration['max_expire']) ? $this->configuration['max_expire'] + REQUEST_TIME : Cache::PERMANENT; + $item += array( - 'expire' => CacheBackendInterface::CACHE_PERMANENT, + 'expire' => $max_expire, 'tags' => array(), ); + // If maximum expiration is set, enforce an upper limit on the expiry. + if ($max_expire != Cache::PERMANENT && ($item['expire'] == Cache::PERMANENT || $item['expire'] > $max_expire)) { + $item['expire'] = $max_expire; + } + Cache::validateTags($item['tags']); $item['tags'] = array_unique($item['tags']); // Sort the cache tags so that they are stored consistently in the DB. diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php index 59b0b22..7b55e51 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php @@ -8,6 +8,7 @@ namespace Drupal\Core\Cache; use Drupal\Core\Database\Connection; +use Drupal\Core\Site\Settings; class DatabaseBackendFactory implements CacheFactoryInterface { @@ -26,6 +27,13 @@ class DatabaseBackendFactory implements CacheFactoryInterface { protected $checksumProvider; /** + * Per-bin database backend configuration. + * + * @var array + */ + protected $configuration; + + /** * Constructs the DatabaseBackendFactory object. * * @param \Drupal\Core\Database\Connection $connection @@ -36,6 +44,9 @@ class DatabaseBackendFactory implements CacheFactoryInterface { function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider) { $this->connection = $connection; $this->checksumProvider = $checksum_provider; + // Load settings and if not set, default to maximum expiration of the render + // cache bin in one week. + $this->configuration = Settings::get('cache_database', ['render' => ['max_expire' => 86400 * 7]]); } /** @@ -48,7 +59,8 @@ function __construct(Connection $connection, CacheTagsChecksumInterface $checksu * The cache backend object for the specified cache bin. */ function get($bin) { - return new DatabaseBackend($this->connection, $this->checksumProvider, $bin); + $configuration = isset($this->configuration[$bin]) ? $this->configuration[$bin] : []; + return new DatabaseBackend($this->connection, $this->checksumProvider, $bin, $configuration); } } diff --git a/core/modules/system/src/Tests/Cache/DatabaseBackendUnitTest.php b/core/modules/system/src/Tests/Cache/DatabaseBackendUnitTest.php index 934fe81..307a853 100644 --- a/core/modules/system/src/Tests/Cache/DatabaseBackendUnitTest.php +++ b/core/modules/system/src/Tests/Cache/DatabaseBackendUnitTest.php @@ -53,4 +53,78 @@ public function testSetGet() { $this->assertIdentical($cached_value_short, $backend->get($cid_short)->data, "Backend contains the correct value for short, non-ASCII cache id."); } + public function testCacheBinExpiration() { + $configuration = ['max_expire' => 2800]; + $backend = new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), 'render', $configuration); + + $backend->set('test_cache1', 'foo'); + $cached = $backend->get('test_cache1'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2800), 'Maximum cache expire time is correct.'); + + $backend->set('test_cache2', 'foo', REQUEST_TIME + 2799); + $cached = $backend->get('test_cache2'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2799), 'Maximum cache expire time is correct.'); + + $backend->set('test_cache3', 'foo', REQUEST_TIME + 2801); + $cached = $backend->get('test_cache3'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2800), 'Maximum cache expire time is not exceeded.'); + } + + public function testCacheBinExpirationSetMultiple() { + $configuration = ['max_expire' => 2800]; + $backend = new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), 'render', $configuration); + + $backend->setMultiple([ + 'test_cache1' => [ + 'data' => 'foo', + ], + 'test_cache2' => [ + 'data' => 'foo', + 'expire' => REQUEST_TIME + 2799, + ], + 'test_cache3' => [ + 'data' => 'foo', + 'expire' => REQUEST_TIME + 2801, + ], + ]); + + $cached = $backend->get('test_cache1'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2800), 'Maximum cache expire time is correct.'); + + $cached = $backend->get('test_cache2'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2799), 'Maximum cache expire time is correct.'); + + $cached = $backend->get('test_cache3'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2800), 'Maximum cache expire time is not exceeded.'); + } + + public function testCacheBinNoExpiration() { + // Empty configuration would force DatabaseBackend to select permanent + // lifetime for cache objects. + $configuration = []; + $backend = new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), 'render', $configuration); + + $backend->setMultiple([ + 'test_cache1' => [ + 'data' => 'foo', + ], + 'test_cache2' => [ + 'data' => 'foo', + 'expire' => REQUEST_TIME + 2799, + ], + ]); + + // Test set as well. + $backend->set('test_cache3', 'foo', REQUEST_TIME + 2801); + + $cached = $backend->get('test_cache1'); + $this->assertIdentical($cached->expire, (string) (-1), 'Maximum cache expire time is correct.'); + + $cached = $backend->get('test_cache2'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2799), 'Maximum cache expire time is correct.'); + + $cached = $backend->get('test_cache3'); + $this->assertIdentical($cached->expire, (string) (REQUEST_TIME + 2801), 'Maximum cache expire time is correct.'); + } + } diff --git a/sites/default/default.settings.php b/sites/default/default.settings.php index 9ad5cae..3777cf0 100644 --- a/sites/default/default.settings.php +++ b/sites/default/default.settings.php @@ -691,3 +691,17 @@ # if (file_exists(__DIR__ . '/settings.local.php')) { # include __DIR__ . '/settings.local.php'; # } + +/** + * Per-bin database backend configuration. + * + * Configure settings for database backend. Available settings are: + * - max_expire: Specify the maximum expiry for an individual cache item in the + * selected bin. Some cache bins can get very big and persistent cache entries + * are never removed. Setting this value will allow them to be garbage + * collected after the configured time, and will need to be rebuilt if the + * item is requested again. + */ +$settings['cache_database']['render'] = [ + 'max_expire' => 86400 * 7, +];