From d3b029c6b50b8bcae9b4271d9180930f019fa11c Mon Sep 17 00:00:00 2001 From: Terry Lin Date: Thu, 29 Oct 2020 16:21:56 +0800 Subject: [PATCH] Suggestion #2 - Clean up of expired cache items --- README.md | 67 +++++++ src/SimpleCache/Cache.php | 44 ++++- src/SimpleCache/CacheProvider.php | 75 ++++---- src/SimpleCache/Driver/Redis.php | 20 -- tests/SimpleCache/CacheTest.php | 182 ++++++++++++++++-- .../SimpleCache/DriverIntegrationTestCase.php | 6 +- tests/file.sh | 1 + tests/memcache.sh | 1 + 8 files changed, 323 insertions(+), 73 deletions(-) create mode 100644 tests/file.sh create mode 100644 tests/memcache.sh diff --git a/README.md b/README.md index 312f5d9..7062157 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ Before you use, make sure you have the required PHP modules installed on the sys - setMultiple - deleteMultiple - clear + - clearExpiredItems `(Non-PSR-16)` - Build Data Schema - MySQL - SQLite @@ -173,6 +174,7 @@ Those API methods are defined on `Psr\SimpleCache\CacheInterface`. Please check - getMultiple - deleteMultiple - clear +- clearExpiredItems *(Non-PSR-16)* ### set @@ -369,6 +371,38 @@ if ($cache->clear()) { // All cached data has been deleted successfully. ``` +### clearExpiredItems `Non-PSR-16` + +```php +public function clearExpiredItems(): array +``` + +This method will return a list of the removed cache keys. + +*Note*: **Redis** and **Memcache**, **Memcached** drivers will always return an empty array. See *Garbage Collection* section below. + +Example: + +```php +$cache->set('foo', 'bar', 300); +$cache->set('foo2', 'bar2', 5); +$cache->set('foo3', 'bar3', 5); + +sleep(6); + +$expiredItems = $cache->clearExpiredItems(); +var_dump($expiredItems); + +/* + array(2) { + ["foo2"]=> + string(4) "bar2" + ["foo3"]=> + string(4) "bar3" + } +*/ +``` + --- ## Build Data Schema @@ -402,6 +436,39 @@ CREATE TABLE IF NOT EXISTS cache_data ( ); ``` +--- + +## Garbage Collection + +For built-in drivers, you can enable the garbage collection to clear expired cache from your system automatically. + +Use those parameters: +```php +$config = [ + 'gc_enable' => true, + 'gc_divisor' => 100, // default + 'gc_probability' => 1, // default +]; +``` +It means there will be a `1%` chance of performing the garbage collection. +Do not use it as 100% chance becasue it will fetch all keys and check them one by one, totally unnecessary. + +Example: +```php +$driver = new \Shieldon\SimpleCache\Cache('file', [ + 'storage' => __DIR__ . '/../tmp', + 'gc_enable' => true, +]); +``` + +You can just use the `gc_enable` to enable garbage collection. + +### Note + +For **Redis** and **Memcache**, **Memcached** drivers, there is no need to use this method becasue that the expired items will be cleared automatically. + + + --- ## Author diff --git a/src/SimpleCache/Cache.php b/src/SimpleCache/Cache.php index 16f2762..cf8562a 100644 --- a/src/SimpleCache/Cache.php +++ b/src/SimpleCache/Cache.php @@ -51,6 +51,7 @@ public function __construct($driver = '', array $settings = []) $class = '\Shieldon\SimpleCache\Driver\\' . $class; $this->driver = new $class($settings); + $this->gc($settings); } } @@ -126,7 +127,7 @@ public function deleteMultiple($keys) } /** - * Create or rebuid the data schema. + * Create or rebuid the data schema. [Non-PSR-16] * This method is avaialbe for Mysql and Sqlite drivers. * * @return bool @@ -139,4 +140,45 @@ public function rebuild(): bool return false; } + + /** + * Clear all expired items. [Non-PSR-16] + * + * @return array The list of the removed items. + */ + public function clearExpiredItems(): array + { + return $this->gc([ + 'gc_enable' => true, + 'gc_probability' => 1, + 'gc_divisor' => 1, + ]); + } + + /** + * Performing cache data garbage collection for drivers that don't have + * ability to remove expired items automatically. + * This method is not needed for Redis and Memcached driver. + * + * @param array $settings [bool $gc_enable, int $gc_probability, int $gc_divisor] + * + * @return array The list of the removed items. + */ + protected function gc(array $settings = []): array + { + if (empty($settings['gc_enable'])) { + return []; + } + + $removedList = []; + + $probability = $settings['gc_probability'] ?? 1; + $divisor = $settings['gc_divisor'] ?? 100; + + if (method_exists($this->driver, 'gc')) { + $removedList = $this->driver->gc($probability, $divisor); + } + + return $removedList; + } } \ No newline at end of file diff --git a/src/SimpleCache/CacheProvider.php b/src/SimpleCache/CacheProvider.php index cfaa93e..abe1940 100644 --- a/src/SimpleCache/CacheProvider.php +++ b/src/SimpleCache/CacheProvider.php @@ -154,70 +154,77 @@ public function deleteMultiple($keys) return true; } - /** - * Check if the TTL is expired or not. - * - * @param int $ttl The time to live of a cached data. - * @param int $timestamp The unix timesamp that want to check. - * - * @return bool - */ - protected function isExpired(int $ttl, int $timestamp): bool - { - $now = time(); - - // If $ttl equal to 0 means that never expires. - if (empty($ttl)) { - return false; - - } elseif ($now - $timestamp < $ttl) { - return false; - } - - return true; - } - /** * Performing cache data garbage collection for drivers that don't have * ability to remove expired items automatically. * This method is not needed for Redis and Memcached driver. * - * @param int $expires The time of expiring. * @param int $probability Numerator. * @param int $divisor Denominator. * - * @return bool + * @return array */ - protected function gc(int $expires, int $probability, int $divisor): bool + public function gc(int $probability, int $divisor): array { + if ($probability > $divisor) { + $probability = $divisor; + } $chance = intval($divisor / $probability); - $hit = rand(1, $chance); + $hit = rand(1, $chance); + $list = []; if ($hit === 1) { - + + // Always return [] from Redis and Memcached driver. $data = $this->getAll(); if (!empty($data)) { foreach ($data as $key => $value) { + $ttl = (int) $value['ttl']; $lasttime = (int) $value['timestamp']; - - if (time() - $lasttime > $expires) { + + if ($this->isExpired($ttl, $lasttime)) { $this->delete($key); + + $list[] = $key; } } } - return true; } - return false; + return $list; + } + + /** + * Check if the TTL is expired or not. + * + * @param int $ttl The time to live of a cached data. + * @param int $timestamp The unix timesamp that want to check. + * + * @return bool + */ + protected function isExpired(int $ttl, int $timestamp): bool + { + $now = time(); + + // If $ttl equal to 0 means that never expires. + if (empty($ttl)) { + return false; + + } elseif ($now - $timestamp < $ttl) { + return false; + } + + return true; } /** * Fetch all cache items to prepare removing expired items. - * This method is used only in `gc()`. + * This method is not needed for Redis and Memcached driver because that + * it is used only in `gc()`. * * @return array */ - private function getAll(): array + protected function getAll(): array { return []; } diff --git a/src/SimpleCache/Driver/Redis.php b/src/SimpleCache/Driver/Redis.php index a2a4588..85d0ae5 100644 --- a/src/SimpleCache/Driver/Redis.php +++ b/src/SimpleCache/Driver/Redis.php @@ -228,26 +228,6 @@ protected function doHas(string $key): bool // @codeCoverageIgnoreEnd } - /** - * Fetch all cache items. - * - * @return array - */ - protected function getAll(): array - { - $list = []; - $keys = $this->redis->keys('sc:*'); - - if (!empty($keys)) { - foreach ($keys as $key) { - $value = $this->doGet($key); - $key = str_replace('sc_', '', $key); - $list[$key] = $value; - } - } - return $list; - } - /** * Get the key name of a cache. * diff --git a/tests/SimpleCache/CacheTest.php b/tests/SimpleCache/CacheTest.php index 69cd35b..249853e 100644 --- a/tests/SimpleCache/CacheTest.php +++ b/tests/SimpleCache/CacheTest.php @@ -15,7 +15,6 @@ use Shieldon\Test\SimpleCache\CacheTestCase; use Shieldon\SimpleCache\Driver\Mock; use Shieldon\SimpleCache\Exception\CacheArgumentException; -use DateInterval; class CacheTest extends CacheTestCase { @@ -24,20 +23,109 @@ public function testStart() $this->console('Cache Provider'); } - public function getInstance() + /** + * Test provider. + * + * @param string $type The driver's type. + * @param array $settings The driver's settings. + * + * @return Cache + */ + public function getInstance($type = 'file', $settings = []) { - $driver = new Cache('file', [ - 'storage' => create_tmp_directory() - ]); + switch ($type) { + case 'apc': + case 'apcu': + case 'memcache': + case 'memcached': + case 'mongo': + case 'redis': + $driver = new Cache($type, $settings); + break; + + case 'mysql': + $settings['dbname'] = 'shieldon_unittest'; + $settings['user'] = 'shieldon'; + $settings['pass'] = 'taiwan'; + + $driver = new Cache($type, $settings); + break; + + case 'sqlite': + $settings['storage'] = create_tmp_directory(); + $driver = new Cache($type, $settings); + break; + + case 'file': + default: + $settings['storage'] = create_tmp_directory(); + $driver = new Cache('file', $settings); + break; + } return $driver; } + /** + * Test provider. + * + * @param string $driverType The driver's type. + * @param bool $hit The GC is it or not. + * + * @return void + */ + public function garbageCollection(string $driverType, bool $hit = true) + { + $text = 'not hit'; + + if ($hit) { + $text = 'hit'; + } + + $this->console('Garbage collection test: ' . ucfirst($driverType) . ' (' . $text . ')'); + + $driver = $this->getInstance($driverType); + $driver->clear(); + $driver->set('foo', 'aa', 1); + $driver->set('foo2', 'bb', 1); + + $this->assertSame('aa', $driver->get('foo')); + $this->assertSame('bb', $driver->get('foo2')); + + sleep(3); + + // Start the garbage collection. + if ($hit) { + + $settings = [ + 'gc_enable' => true, + 'gc_divisor' => 1, + 'gc_probability' => 2, + ]; + + $driver = $this->getInstance($driverType, $settings); + + $this->assertSame(null, $driver->get('foo')); + $this->assertSame(null, $driver->get('foo2')); + } else { + $settings = [ + 'gc_enable' => true, + 'gc_divisor' => 99999999, + 'gc_probability' => 1, + ]; + + $driver = $this->getInstance($driverType, $settings); + + sleep(3); + + $this->assertSame(null, $driver->get('foo')); + $this->assertSame(null, $driver->get('foo2')); + } + } + public function testCacheInitialize() { - $driver = new Cache('file', [ - 'storage' => create_tmp_directory() - ]); + $driver = $this->getInstance(); $reflection = new \ReflectionObject($driver); $t = $reflection->getProperty('driver'); @@ -133,11 +221,7 @@ public function testIsExpired() public function testRebuildShouldReturnTrue() { - $driver = new Cache('mysql', [ - 'dbname' => 'shieldon_unittest', - 'user' => 'shieldon', - 'pass' => 'taiwan', - ]); + $driver = $this->getInstance('mysql'); $this->assertTrue($driver->rebuild()); @@ -150,8 +234,78 @@ public function testRebuildShouldReturnTrue() public function testRebuildShouldReturnFalse() { - $driver = new Cache('apcu'); + $driver = $this->getInstance('apcu'); $this->assertFalse($driver->rebuild()); } + + public function testGarbageCollectionForFileDriver() + { + $this->garbageCollection('file'); + $this->garbageCollection('file', false); + } + + public function testGarbageCollectionForRedisDriver() + { + $this->garbageCollection('redis'); + } + + public function testGarbageCollectionForMemcacheDriver() + { + $this->garbageCollection('memcache'); + } + + public function testGarbageCollectionForMemcachedDriver() + { + $this->garbageCollection('memcached'); + } + + public function testGarbageCollectionForApcDriver() + { + $this->garbageCollection('apc'); + } + + public function testGarbageCollectionForApcuDriver() + { + $this->garbageCollection('apcu'); + } + + public function testGarbageCollectionForMysqlDriver() + { + $this->garbageCollection('mysql'); + } + + public function testGarbageCollectionForSqliteDriver() + { + $this->garbageCollection('sqlite'); + } + public function testGarbageCollectionForMongodbDriver() + { + $this->garbageCollection('mongo'); + } + + public function testClearExpiredItems() + { + $this->console('Clear expired items.'); + + $driver = $this->getInstance(); + $driver->clear(); + $driver->set('foo', 'aa', 1); + $driver->set('foo2', 'bb', 1); + + $this->assertSame('aa', $driver->get('foo')); + $this->assertSame('bb', $driver->get('foo2')); + + sleep(2); + + $removedList = $driver->clearExpiredItems(); + + $this->assertSame(null, $driver->get('foo')); + $this->assertSame(null, $driver->get('foo2')); + + $this->assertSame('foo', $removedList[0]); + $this->assertSame('foo2', $removedList[1]); + } + + } \ No newline at end of file diff --git a/tests/SimpleCache/DriverIntegrationTestCase.php b/tests/SimpleCache/DriverIntegrationTestCase.php index b63dedf..c8c5ad7 100644 --- a/tests/SimpleCache/DriverIntegrationTestCase.php +++ b/tests/SimpleCache/DriverIntegrationTestCase.php @@ -75,8 +75,6 @@ public function testDriverCombinedTests() 'foo5' => 'hello', ], $result); - - // test method `deleteMultiple` $cache->deleteMultiple(['foo3']); @@ -109,11 +107,11 @@ public function testDriverCacheExpired() { $cache = $this->getCacheDriver(); - $cache->set('foo', 'bar', 5); + $cache->set('foo', 'bar', 2); $this->assertSame('bar', $cache->get('foo')); $this->assertTrue($cache->has('foo')); - sleep(6); + sleep(3); $this->assertSame(null, $cache->get('foo')); $this->assertFalse($cache->has('foo')); diff --git a/tests/file.sh b/tests/file.sh new file mode 100644 index 0000000..f980568 --- /dev/null +++ b/tests/file.sh @@ -0,0 +1 @@ +php ../vendor/phpunit/phpunit/phpunit --configuration ../phpunit.xml --filter FileTest ../tests/SimpleCache/Driver/FileTest.php \ No newline at end of file diff --git a/tests/memcache.sh b/tests/memcache.sh new file mode 100644 index 0000000..4217676 --- /dev/null +++ b/tests/memcache.sh @@ -0,0 +1 @@ +php ../vendor/phpunit/phpunit/phpunit --configuration ../phpunit.xml --filter MemcacheTest ../tests/SimpleCache/Driver/MemcacheTest.php \ No newline at end of file