Loading core/lib/Drupal/Core/Cache/ChainedFastBackend.php +34 −10 Original line number Diff line number Diff line Loading @@ -120,7 +120,19 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { // anything in the fast backend is valid, so don't even bother fetching // from there. $last_write_timestamp = $this->getLastWriteTimestamp(); if ($last_write_timestamp) { // Don't bother to either read from or write to the fast backend if the last // write timestamp is in the future - it is always set with an additional // grace period for this reason. This reduces the likelihood of a cache // stampede on the fast backend when the consistent backend is being written // to frequently. It can also reduce the storage (usually memory) // requirement of the fast backend due to layered caching - e.g. when a // higher level cache is warm, the lower level cache items that are used to // build it won't be requested. Once the grace period has passed, the fast // backend will begin to take over from the consistent backend again. $compare = round(microtime(TRUE), 3); if ($last_write_timestamp && $compare > $last_write_timestamp) { // Items in the fast backend might be invalid based on their timestamp, // but we can't check the timestamp prior to getting the item, which // includes unserializing it. However, unserializing an invalid item can Loading Loading @@ -166,7 +178,10 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { if ($cids) { foreach ($this->consistentBackend->getMultiple($cids, $allow_invalid) as $item) { $cache[$item->cid] = $item; if (!$allow_invalid || $item->valid) { // Only write back to the fast backend if the created time will be later // than $last_write_timestamp the next time it is retrieved, to // avoid wasted writes. if ((!$allow_invalid || $item->valid) && $compare > $last_write_timestamp) { $this->fastBackend->set($item->cid, $item->data, $item->expire, $item->tags); } } Loading @@ -179,9 +194,17 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { * {@inheritdoc} */ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { // Setting a cache item on the consistent backend requires invalidating the // fast backend. In a cold cache situation, there can be thousands of cache // sets. However, because each cache set invalidates every previous set, // only the item(s) from the last one will be valid. Therefore, don't write // to the fast backend, this avoids lock/write contention on the fast // backend, for cache items which may not be requested immediately anyway, // e.g. when higher level caches are warmed at the same time. The fast // backend will be populated via the logic in ::get() instead when cache // items are actually requested. $this->consistentBackend->set($cid, $data, $expire, $tags); $this->markAsOutdated(); $this->fastBackend->set($cid, $data, $expire, $tags); } /** Loading @@ -190,7 +213,6 @@ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { public function setMultiple(array $items) { $this->consistentBackend->setMultiple($items); $this->markAsOutdated(); $this->fastBackend->setMultiple($items); } /** Loading Loading @@ -292,15 +314,17 @@ protected function getLastWriteTimestamp() { protected function markAsOutdated() { // Clocks on a single server can drift. Multiple servers may have slightly // differing opinions about the current time. Given that, do not assume // 'now' on this server is always later than our stored timestamp. Add 50ms // to the current time each time we write it to the persistent cache, and // make sure it is always at least 1ms ahead of the current time. This // 'now' on this server is always later than our stored timestamp. Add one // second to the current time each time we write it to the persistent cache // and make sure it is always at least 1ms ahead of the current time. This // somewhat protects against clock drift, while also reducing the number of // persistent cache writes to one every 50ms if this method is called // multiple times during a request. // persistent cache writes to one every second if this method is called // multiple times during a request. Reads and writes from the fast cache // are skipped when this timestamp is in the future, which also helps to // avoid write contention on the fast cache. $compare = round(microtime(TRUE) + .001, 3); if ($compare > $this->getLastWriteTimestamp()) { $now = round(microtime(TRUE) + .050, 3); $now = round(microtime(TRUE) + 1, 3); $this->lastWriteTimestamp = $now; $this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp); } Loading core/tests/Drupal/Tests/Core/Cache/ChainedFastBackendTest.php +88 −5 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Component\Datetime\Time; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\ChainedFastBackend; use Drupal\Core\Cache\MemoryBackend; use Drupal\Tests\UnitTestCase; Loading Loading @@ -106,14 +107,45 @@ public function testSetInvalidDataFastBackend(): void { $this->assertEquals(NULL, $fast_cache->get($cid), 'Invalid data was not saved on the fast cache.'); } /** * Tests that sets only get written to the consistent backend. */ public function testSet(): void { $consistent_cache = $this->createMock(CacheBackendInterface::class); $fast_cache = $this->createMock(CacheBackendInterface::class); // The initial write to the fast backend should result in two writes to the // consistent backend, once to invalidate the last write timestamp and once // for the item itself. However subsequent writes during the same second // should only write to the cache item without further invalidations. $consistent_cache->expects($this->exactly(5)) ->method('set'); $fast_cache->expects($this->never()) ->method('set'); $fast_cache->expects($this->never()) ->method('setMultiple'); $chained_fast_backend = new ChainedFastBackend( $consistent_cache, $fast_cache, 'foo' ); $chained_fast_backend->set('foo', TRUE); $chained_fast_backend->set('bar', TRUE); $chained_fast_backend->set('baz', TRUE); $chained_fast_backend->set('zoo', TRUE); } /** * Tests a fast cache miss gets data from the consistent cache backend. */ public function testFallThroughToConsistentCache(): void { public function testFastBackendGracePeriod(): void { $timestamp_item = (object) [ 'cid' => ChainedFastBackend::LAST_WRITE_TIMESTAMP_PREFIX . 'cache_foo', // Time travel is easy. 'data' => time() + 60, // This is set two seconds in the future so that the grace period before // writing through to the fast backend is observed. 'data' => time() + 2, ]; $cache_item = (object) [ 'cid' => 'foo', Loading @@ -123,8 +155,59 @@ public function testFallThroughToConsistentCache(): void { 'tags' => ['tag'], ]; $consistent_cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface'); $fast_cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface'); $consistent_cache = $this->createMock(CacheBackendInterface::class); $fast_cache = $this->createMock(CacheBackendInterface::class); // We should get a call for the timestamp on the consistent backend. $consistent_cache->expects($this->once()) ->method('get') ->with($timestamp_item->cid) ->willReturn($timestamp_item); // We should get a call for the cache item on the consistent backend. $consistent_cache->expects($this->once()) ->method('getMultiple') ->with([$cache_item->cid]) ->willReturn([$cache_item->cid => $cache_item]); // We should not get a call for the cache item on the fast backend. $fast_cache->expects($this->never()) ->method('getMultiple'); // We should not get a call to set the cache item on the fast backend. $fast_cache->expects($this->never()) ->method('set'); $chained_fast_backend = new ChainedFastBackend( $consistent_cache, $fast_cache, 'foo' ); $this->assertEquals('baz', $chained_fast_backend->get('foo')->data); } /** * Tests a fast cache miss gets data from the consistent cache backend. */ public function testFallThroughToConsistentCache(): void { // Make the last_write_timestamp two seconds in the past so that everything // is written back to the fast backend. $timestamp_item = (object) [ 'cid' => ChainedFastBackend::LAST_WRITE_TIMESTAMP_PREFIX . 'cache_foo', 'data' => time() - 2, ]; $cache_item = (object) [ 'cid' => 'foo', 'data' => 'baz', // The created time is set one minute in the past, e.g. before the // consistent timestamp. 'created' => time() - 60, 'expire' => time() + 3600, 'tags' => ['tag'], ]; $consistent_cache = $this->createMock(CacheBackendInterface::class); $fast_cache = $this->createMock(CacheBackendInterface::class); // We should get a call for the timestamp on the consistent backend. $consistent_cache->expects($this->once()) Loading Loading
core/lib/Drupal/Core/Cache/ChainedFastBackend.php +34 −10 Original line number Diff line number Diff line Loading @@ -120,7 +120,19 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { // anything in the fast backend is valid, so don't even bother fetching // from there. $last_write_timestamp = $this->getLastWriteTimestamp(); if ($last_write_timestamp) { // Don't bother to either read from or write to the fast backend if the last // write timestamp is in the future - it is always set with an additional // grace period for this reason. This reduces the likelihood of a cache // stampede on the fast backend when the consistent backend is being written // to frequently. It can also reduce the storage (usually memory) // requirement of the fast backend due to layered caching - e.g. when a // higher level cache is warm, the lower level cache items that are used to // build it won't be requested. Once the grace period has passed, the fast // backend will begin to take over from the consistent backend again. $compare = round(microtime(TRUE), 3); if ($last_write_timestamp && $compare > $last_write_timestamp) { // Items in the fast backend might be invalid based on their timestamp, // but we can't check the timestamp prior to getting the item, which // includes unserializing it. However, unserializing an invalid item can Loading Loading @@ -166,7 +178,10 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { if ($cids) { foreach ($this->consistentBackend->getMultiple($cids, $allow_invalid) as $item) { $cache[$item->cid] = $item; if (!$allow_invalid || $item->valid) { // Only write back to the fast backend if the created time will be later // than $last_write_timestamp the next time it is retrieved, to // avoid wasted writes. if ((!$allow_invalid || $item->valid) && $compare > $last_write_timestamp) { $this->fastBackend->set($item->cid, $item->data, $item->expire, $item->tags); } } Loading @@ -179,9 +194,17 @@ public function getMultiple(&$cids, $allow_invalid = FALSE) { * {@inheritdoc} */ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { // Setting a cache item on the consistent backend requires invalidating the // fast backend. In a cold cache situation, there can be thousands of cache // sets. However, because each cache set invalidates every previous set, // only the item(s) from the last one will be valid. Therefore, don't write // to the fast backend, this avoids lock/write contention on the fast // backend, for cache items which may not be requested immediately anyway, // e.g. when higher level caches are warmed at the same time. The fast // backend will be populated via the logic in ::get() instead when cache // items are actually requested. $this->consistentBackend->set($cid, $data, $expire, $tags); $this->markAsOutdated(); $this->fastBackend->set($cid, $data, $expire, $tags); } /** Loading @@ -190,7 +213,6 @@ public function set($cid, $data, $expire = Cache::PERMANENT, array $tags = []) { public function setMultiple(array $items) { $this->consistentBackend->setMultiple($items); $this->markAsOutdated(); $this->fastBackend->setMultiple($items); } /** Loading Loading @@ -292,15 +314,17 @@ protected function getLastWriteTimestamp() { protected function markAsOutdated() { // Clocks on a single server can drift. Multiple servers may have slightly // differing opinions about the current time. Given that, do not assume // 'now' on this server is always later than our stored timestamp. Add 50ms // to the current time each time we write it to the persistent cache, and // make sure it is always at least 1ms ahead of the current time. This // 'now' on this server is always later than our stored timestamp. Add one // second to the current time each time we write it to the persistent cache // and make sure it is always at least 1ms ahead of the current time. This // somewhat protects against clock drift, while also reducing the number of // persistent cache writes to one every 50ms if this method is called // multiple times during a request. // persistent cache writes to one every second if this method is called // multiple times during a request. Reads and writes from the fast cache // are skipped when this timestamp is in the future, which also helps to // avoid write contention on the fast cache. $compare = round(microtime(TRUE) + .001, 3); if ($compare > $this->getLastWriteTimestamp()) { $now = round(microtime(TRUE) + .050, 3); $now = round(microtime(TRUE) + 1, 3); $this->lastWriteTimestamp = $now; $this->consistentBackend->set(self::LAST_WRITE_TIMESTAMP_PREFIX . $this->bin, $this->lastWriteTimestamp); } Loading
core/tests/Drupal/Tests/Core/Cache/ChainedFastBackendTest.php +88 −5 Original line number Diff line number Diff line Loading @@ -6,6 +6,7 @@ use Drupal\Component\Datetime\Time; use Drupal\Core\Cache\Cache; use Drupal\Core\Cache\CacheBackendInterface; use Drupal\Core\Cache\ChainedFastBackend; use Drupal\Core\Cache\MemoryBackend; use Drupal\Tests\UnitTestCase; Loading Loading @@ -106,14 +107,45 @@ public function testSetInvalidDataFastBackend(): void { $this->assertEquals(NULL, $fast_cache->get($cid), 'Invalid data was not saved on the fast cache.'); } /** * Tests that sets only get written to the consistent backend. */ public function testSet(): void { $consistent_cache = $this->createMock(CacheBackendInterface::class); $fast_cache = $this->createMock(CacheBackendInterface::class); // The initial write to the fast backend should result in two writes to the // consistent backend, once to invalidate the last write timestamp and once // for the item itself. However subsequent writes during the same second // should only write to the cache item without further invalidations. $consistent_cache->expects($this->exactly(5)) ->method('set'); $fast_cache->expects($this->never()) ->method('set'); $fast_cache->expects($this->never()) ->method('setMultiple'); $chained_fast_backend = new ChainedFastBackend( $consistent_cache, $fast_cache, 'foo' ); $chained_fast_backend->set('foo', TRUE); $chained_fast_backend->set('bar', TRUE); $chained_fast_backend->set('baz', TRUE); $chained_fast_backend->set('zoo', TRUE); } /** * Tests a fast cache miss gets data from the consistent cache backend. */ public function testFallThroughToConsistentCache(): void { public function testFastBackendGracePeriod(): void { $timestamp_item = (object) [ 'cid' => ChainedFastBackend::LAST_WRITE_TIMESTAMP_PREFIX . 'cache_foo', // Time travel is easy. 'data' => time() + 60, // This is set two seconds in the future so that the grace period before // writing through to the fast backend is observed. 'data' => time() + 2, ]; $cache_item = (object) [ 'cid' => 'foo', Loading @@ -123,8 +155,59 @@ public function testFallThroughToConsistentCache(): void { 'tags' => ['tag'], ]; $consistent_cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface'); $fast_cache = $this->createMock('Drupal\Core\Cache\CacheBackendInterface'); $consistent_cache = $this->createMock(CacheBackendInterface::class); $fast_cache = $this->createMock(CacheBackendInterface::class); // We should get a call for the timestamp on the consistent backend. $consistent_cache->expects($this->once()) ->method('get') ->with($timestamp_item->cid) ->willReturn($timestamp_item); // We should get a call for the cache item on the consistent backend. $consistent_cache->expects($this->once()) ->method('getMultiple') ->with([$cache_item->cid]) ->willReturn([$cache_item->cid => $cache_item]); // We should not get a call for the cache item on the fast backend. $fast_cache->expects($this->never()) ->method('getMultiple'); // We should not get a call to set the cache item on the fast backend. $fast_cache->expects($this->never()) ->method('set'); $chained_fast_backend = new ChainedFastBackend( $consistent_cache, $fast_cache, 'foo' ); $this->assertEquals('baz', $chained_fast_backend->get('foo')->data); } /** * Tests a fast cache miss gets data from the consistent cache backend. */ public function testFallThroughToConsistentCache(): void { // Make the last_write_timestamp two seconds in the past so that everything // is written back to the fast backend. $timestamp_item = (object) [ 'cid' => ChainedFastBackend::LAST_WRITE_TIMESTAMP_PREFIX . 'cache_foo', 'data' => time() - 2, ]; $cache_item = (object) [ 'cid' => 'foo', 'data' => 'baz', // The created time is set one minute in the past, e.g. before the // consistent timestamp. 'created' => time() - 60, 'expire' => time() + 3600, 'tags' => ['tag'], ]; $consistent_cache = $this->createMock(CacheBackendInterface::class); $fast_cache = $this->createMock(CacheBackendInterface::class); // We should get a call for the timestamp on the consistent backend. $consistent_cache->expects($this->once()) Loading