core/core.services.yml | 2 +- core/lib/Drupal/Core/Cache/DatabaseBackend.php | 35 ++++++- .../Drupal/Core/Cache/DatabaseBackendFactory.php | 28 +++++- core/modules/system/system.install | 11 ++ .../KernelTests/Core/Cache/DatabaseBackendTest.php | 52 +++++++++- .../Core/Cache/DatabaseBackendFactoryTest.php | 111 +++++++++++++++++++++ 6 files changed, 234 insertions(+), 5 deletions(-) diff --git a/core/core.services.yml b/core/core.services.yml index 7608878..598754e 100644 --- a/core/core.services.yml +++ b/core/core.services.yml @@ -192,7 +192,7 @@ services: - [setContainer, ['@service_container']] cache.backend.database: class: Drupal\Core\Cache\DatabaseBackendFactory - arguments: ['@database', '@cache_tags.invalidator.checksum'] + arguments: ['@database', '@cache_tags.invalidator.checksum', '@settings'] cache.backend.apcu: class: Drupal\Core\Cache\ApcuBackendFactory arguments: ['@app.root', '@site.path', '@cache_tags.invalidator.checksum'] diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackend.php b/core/lib/Drupal/Core/Cache/DatabaseBackend.php index d53c51c..9d96ddc 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackend.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackend.php @@ -17,6 +17,13 @@ class DatabaseBackend implements CacheBackendInterface { /** + * The maximum number of rows that this cache bin table is allowed to store. + * + * @var int + */ + protected $maxRows; + + /** * @var string */ protected $bin; @@ -45,14 +52,17 @@ class DatabaseBackend implements CacheBackendInterface { * The cache tags checksum provider. * @param string $bin * The cache bin for which the object is created. + * @param int $max_rows + * The maximum number of rows that are allowed in this cache bin table. */ - public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin) { + public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, $bin, $max_rows) { // All cache tables should be prefixed with 'cache_'. $bin = 'cache_' . $bin; $this->bin = $bin; $this->connection = $connection; $this->checksumProvider = $checksum_provider; + $this->maxRows = $max_rows; } /** @@ -326,6 +336,14 @@ public function invalidateAll() { */ public function garbageCollection() { try { + // Bounded size cache bin, using FIFO. + $query = $this->connection->select($this->bin); + $query->addExpression('MAX(bounded_id)', 'bounded_id'); + $max_bounded_id = $query->execute()->fetchField(); + $this->connection->delete($this->bin) + ->condition('bounded_id', $max_bounded_id - $this->maxRows, '<=') + ->execute(); + $this->connection->delete($this->bin) ->condition('expire', Cache::PERMANENT, '<>') ->condition('expire', REQUEST_TIME, '<') @@ -469,13 +487,28 @@ public function schemaDefinition() { 'length' => 255, 'not null' => TRUE, ], + 'bounded_id' => [ + 'type' => 'serial', + ], ], 'indexes' => [ 'expire' => ['expire'], ], 'primary key' => ['cid'], + 'unique keys' => [ + 'bounded_id' => ['bounded_id'], + ] ]; return $schema; } + /** + * The maximum number of rows that this cache bin table is allowed to store. + * + * @return int + */ + public function getMaxRows() { + return $this->maxRows; + } + } diff --git a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php index 8aa018e..c610a77 100644 --- a/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php +++ b/core/lib/Drupal/Core/Cache/DatabaseBackendFactory.php @@ -3,6 +3,7 @@ namespace Drupal\Core\Cache; use Drupal\Core\Database\Connection; +use Drupal\Core\Site\Settings; class DatabaseBackendFactory implements CacheFactoryInterface { @@ -21,16 +22,26 @@ class DatabaseBackendFactory implements CacheFactoryInterface { protected $checksumProvider; /** + * The settings array. + * + * @var \Drupal\Core\Site\Settings + */ + protected $settings; + + /** * Constructs the DatabaseBackendFactory object. * * @param \Drupal\Core\Database\Connection $connection * Database connection * @param \Drupal\Core\Cache\CacheTagsChecksumInterface $checksum_provider * The cache tags checksum provider. + * @param \Drupal\Core\Site\Settings $settings + * The settings array. */ - public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider) { + public function __construct(Connection $connection, CacheTagsChecksumInterface $checksum_provider, Settings $settings) { $this->connection = $connection; $this->checksumProvider = $checksum_provider; + $this->settings = $settings; } /** @@ -43,7 +54,20 @@ public function __construct(Connection $connection, CacheTagsChecksumInterface $ * The cache backend object for the specified cache bin. */ public function get($bin) { - return new DatabaseBackend($this->connection, $this->checksumProvider, $bin); + $max_rows_settings = $this->settings->get('database_cache_max_rows'); + // First, look for a cache bin specific setting. + if (isset($max_rows_settings['bins'][$bin])) { + $max_rows = $max_rows_settings['bins'][$bin]; + } + // Third, use configured default backend. + elseif (isset($max_rows_settings['default'])) { + $max_rows = $max_rows_settings['default']; + } + else { + // Fall back to the default max rows if nothing else is configured. + $max_rows = 10000; + } + return new DatabaseBackend($this->connection, $this->checksumProvider, $bin, $max_rows); } } diff --git a/core/modules/system/system.install b/core/modules/system/system.install index a4ffffa..684fb77 100644 --- a/core/modules/system/system.install +++ b/core/modules/system/system.install @@ -1984,3 +1984,14 @@ function system_update_8401() { ->clear('response') ->save(); } + +/** + * Delete all cache_* tables. They are recreated on demand with the new schema. + */ +function system_update_8402() { + foreach (\Drupal\Core\Cache\Cache::getBins() as $bin => $cache_backend) { + if ($cache_backend instanceof \Drupal\Core\Cache\DatabaseBackend) { + db_drop_table("cache_$bin"); + } + } +} diff --git a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php index de8bbda..9857b3e 100644 --- a/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php +++ b/core/tests/Drupal/KernelTests/Core/Cache/DatabaseBackendTest.php @@ -12,6 +12,13 @@ class DatabaseBackendTest extends GenericCacheBackendUnitTestBase { /** + * The max rows to use for test bins. + * + * @var int + */ + public static $maxRows = 100; + + /** * Modules to enable. * * @var array @@ -25,7 +32,7 @@ class DatabaseBackendTest extends GenericCacheBackendUnitTestBase { * A new DatabaseBackend object. */ protected function createCacheBackend($bin) { - return new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), $bin); + return new DatabaseBackend($this->container->get('database'), $this->container->get('cache_tags.invalidator.checksum'), $bin, static::$maxRows); } /** @@ -48,4 +55,47 @@ public function testSetGet() { $this->assertIdentical($cached_value_short, $backend->get($cid_short)->data, "Backend contains the correct value for short, non-ASCII cache id."); } + /** + * Tests the row count limiting of cache bin database tables. + */ + public function testGarbageCollection() { + $backend = $this->getCacheBackend(); + $max_rows = static::$maxRows; + + $this->assertSame(0, (int) $this->getNumRows()); + + // Fill to just the limit. + for ($i = 0; $i < $max_rows; $i++) { + $backend->set("test$i", $i); + } + $this->assertSame($max_rows, $this->getNumRows()); + + // Garbage collection has no effect. + $backend->garbageCollection(); + $this->assertSame($max_rows, $this->getNumRows()); + + // Go one row beyond the limit. + $backend->set('test' . ($max_rows + 1), $max_rows + 1); + $this->assertSame($max_rows + 1, $this->getNumRows()); + + // Garbage collection removes one row: the oldest. + $backend->garbageCollection(); + $this->assertSame($max_rows, $this->getNumRows()); + $this->assertSame(FALSE, $backend->get('test0')); + } + + /** + * Gets the number of rows in the test cache bin database table. + * + * @return int + * The number of rows in the test cache bin database table. + */ + protected function getNumRows() { + $table = 'cache_' . $this->testBin; + $connection = $this->container->get('database'); + $query = $connection->select($table); + $query->addExpression('COUNT(cid)', 'cid'); + return (int) $query->execute()->fetchField(); + } + } diff --git a/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendFactoryTest.php b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendFactoryTest.php new file mode 100644 index 0000000..f609ff1 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Cache/DatabaseBackendFactoryTest.php @@ -0,0 +1,111 @@ +prophesize(Connection::class)->reveal(), + $this->prophesize(CacheTagsChecksumInterface::class)->reveal(), + new Settings($settings) + ); + + $this->assertSame($expected_max_rows_foo, $database_backend_factory->get('foo')->getMaxRows()); + $this->assertSame($expected_max_rows_bar, $database_backend_factory->get('bar')->getMaxRows()); + } + + public function getProvider() { + return [ + 'default' => [ + [], + 10000, + 10000, + ], + 'default overridden' => [ + [ + 'database_cache_max_rows' => [ + 'default' => 99, + ], + ], + 99, + 99, + ], + 'default + foo bin overridden' => [ + [ + 'database_cache_max_rows' => [ + 'bins' => [ + 'foo' => 13, + ], + ], + ], + 13, + 10000, + ], + 'default + bar bin overridden' => [ + [ + 'database_cache_max_rows' => [ + 'bins' => [ + 'bar' => 13, + ], + ], + ], + 10000, + 13, + ], + 'default overridden + bar bin overridden' => [ + [ + 'database_cache_max_rows' => [ + 'default' => 99, + 'bins' => [ + 'bar' => 13, + ], + ], + ], + 99, + 13, + ], + 'default + both bins overridden' => [ + [ + 'database_cache_max_rows' => [ + 'bins' => [ + 'foo' => 13, + 'bar' => 31, + ], + ], + ], + 13, + 31, + ], + 'default overridden + both bins overridden' => [ + [ + 'database_cache_max_rows' => [ + 'default' => 99, + 'bins' => [ + 'foo' => 13, + 'bar' => 31, + ], + ], + ], + 13, + 31, + ], + ]; + } + +}