diff --git a/src/Illuminate/Cache/RedisTaggedCache.php b/src/Illuminate/Cache/RedisTaggedCache.php index b8120be95c03..d691484a795b 100644 --- a/src/Illuminate/Cache/RedisTaggedCache.php +++ b/src/Illuminate/Cache/RedisTaggedCache.php @@ -126,4 +126,14 @@ public function flushStale() return true; } + + protected function onKeyWritten(string $key): void + { + // No need to do anything as the Redis store manages the entry list internally. + } + + protected function onKeyForgotten(string $key): void + { + // No need to do anything as the Redis store manages the entry list internally. + } } diff --git a/src/Illuminate/Cache/TaggedCache.php b/src/Illuminate/Cache/TaggedCache.php index 7cd12303882c..03936db3d8b8 100644 --- a/src/Illuminate/Cache/TaggedCache.php +++ b/src/Illuminate/Cache/TaggedCache.php @@ -2,6 +2,8 @@ namespace Illuminate\Cache; +use Illuminate\Cache\Events\KeyForgotten; +use Illuminate\Cache\Events\KeyWritten; use Illuminate\Contracts\Cache\Store; class TaggedCache extends Repository @@ -78,6 +80,10 @@ public function decrement($key, $value = 1) */ public function flush() { + foreach ($this->getItemKeys() as $key) { + $this->store->forget($key); + } + $this->store->forget($this->getMetadataKey()); $this->tags->reset(); return true; @@ -110,9 +116,61 @@ public function taggedItemKey($key) */ protected function event($event) { + $itemKey = $this->itemKey($event->key); + if ($itemKey !== $this->getMetadataKey()) { + if ($event instanceof KeyWritten) { + $this->onKeyWritten($itemKey); + } elseif ($event instanceof KeyForgotten) { + $this->onKeyForgotten($itemKey); + } + } parent::event($event->setTags($this->tags->getNames())); } + protected function onKeyWritten(string $key): void + { + $itemKeys = $this->getItemKeys(); + if (! in_array($key, $itemKeys)) { + $itemKeys[] = $key; + $this->putItemKeys($itemKeys); + } + } + + protected function onKeyForgotten(string $key): void + { + $itemKeys = $this->getItemKeys(); + if (in_array($key, $itemKeys)) { + $itemKeys = array_values( + array_filter($itemKeys, function ($k) use ($key) { + return $k !== $key; + }) + ); + $this->putItemKeys($itemKeys); + } + } + + private function getMetadataKey(): string + { + return $this->itemKey('meta:entries'); + } + + private function getItemKeys(): array + { + $metadataKey = $this->getMetadataKey(); + $keys = $this->store->get($metadataKey); + if (! is_array($keys)) { + $keys = []; + } + + return $keys; + } + + private function putItemKeys(array $keys): void + { + $metadataKey = $this->getMetadataKey(); + $this->store->forever($metadataKey, $keys); + } + /** * Get the tag set instance. * diff --git a/tests/Cache/CacheTaggedCacheTest.php b/tests/Cache/CacheTaggedCacheTest.php index 1834bce56d4f..3744fc8d8ce1 100644 --- a/tests/Cache/CacheTaggedCacheTest.php +++ b/tests/Cache/CacheTaggedCacheTest.php @@ -203,6 +203,22 @@ public function testTagsCacheForever() $this->assertSame('bar', $store->tags($tags)->get('foo')); } + public function testFlushFunctionDoesntLeakMemory() + { + $store = new ArrayStore; + $tags = ['bop']; + $before = memory_get_usage(true); + // Store a 5MB cache value then flush it 100 times, then verify the overall memory usage did not increase + for ($i = 0; $i < 100; $i++) { + $key = str_replace('.', '', uniqid()); + $value = bin2hex(random_bytes(1024 * 5)); + $store->tags($tags)->forever($key, $value); + $store->tags($tags)->flush(); + } + $after = memory_get_usage(true); + $this->assertSame($before, $after); + } + private function getTestCacheStoreWithTagValues(): ArrayStore { $store = new ArrayStore;