From c69de855b4da80c0749cbd8b0dcd15e50d721bc3 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 30 Mar 2018 07:48:39 +0200 Subject: [PATCH 1/5] TTL --- README.md | 5 ++++- src/ArrayCache.php | 31 +++++++++++++++++++++++++---- src/CacheInterface.php | 3 ++- tests/ArrayCacheTest.php | 43 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 76 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 6770ec1..67874f4 100644 --- a/README.md +++ b/README.md @@ -53,10 +53,13 @@ This example fetches the value of the key `foo` and passes it to the `var_dump` function. You can use any of the composition provided by [promises](https://github.com/reactphp/promise). +If the key `foo` does not exist or when the TTL has passed, the promise will +be fulfilled with `null` as value. On any error it will also resolve with `null`. + #### set() ```php -$cache->set('foo', 'bar'); +$cache->set('foo', 'bar', 60); ``` This example eventually sets the value of the key `foo` to `bar`. If it diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 7d6f75f..e4d2561 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -47,19 +47,42 @@ public function get($key, $default = null) return Promise\resolve($default); } + if ($this->data[$key]['expires'] === null) { + // remove and append to end of array to keep track of LRU info + $item = $this->data[$key]; + unset($this->data[$key]); + $this->data[$key] = $item; + return Promise\resolve($this->data[$key]['value']); + } + + if ($this->data[$key]['expires'] < time()) { + unset($this->data[$key]); + return Promise\resolve(); + } + + // remove and append to end of array to keep track of LRU info $value = $this->data[$key]; unset($this->data[$key]); $this->data[$key] = $value; - - return Promise\resolve($value); + return Promise\resolve($this->data[$key]['value']); } - public function set($key, $value) + public function set($key, $value, $ttl = null) { + $expires = null; + + if (is_int($ttl)) { + $expires = time() + $ttl; + } + // unset before setting to ensure this entry will be added to end of array unset($this->data[$key]); - $this->data[$key] = $value; + $this->data[$key] = [ + 'value' => $value, + 'expires' => $expires, + ]; + // ensure size limit is not exceeded or remove first entry from array if ($this->limit !== null && count($this->data) > $this->limit) { diff --git a/src/CacheInterface.php b/src/CacheInterface.php index 3832fac..ca92e53 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -34,9 +34,10 @@ public function get($key, $default = null); * * @param string $key * @param mixed $value + * @param int|null $ttl * @return PromiseInterface Returns a promise which resolves to true on success of false on error */ - public function set($key, $value); + public function set($key, $value, $ttl = null); /** * Remove an item from the cache, returns a promise which resolves to true on success or diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index bacf448..4134711 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -152,4 +152,47 @@ public function testGetWithLimitedSizeWillUpdateLRUInfo() $this->cache->get('bar')->then($this->expectCallableOnceWith(null)); $this->cache->get('baz')->then($this->expectCallableOnceWith('3')); } + + /** @test */ + public function getWithinTtl() + { + $this->cache + ->set('foo', 'bar', 100); + + + $success = $this->createCallableMock(); + $success + ->expects($this->once()) + ->method('__invoke') + ->with('bar'); + + $this->cache + ->get('foo') + ->then( + $success, + $this->expectCallableNever() + ); + } + + /** @test */ + public function getAfterTtl() + { + $this->cache + ->set('foo', 'bar', 1); + + sleep(2); + + $success = $this->createCallableMock(); + $success + ->expects($this->once()) + ->method('__invoke') + ->with(null); + + $this->cache + ->get('foo') + ->then( + $success, + $this->expectCallableNever() + ); + } } From 7cbabac326a8e5e1794daf8641efd66fcfbc3c01 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sun, 1 Apr 2018 22:07:57 +0200 Subject: [PATCH 2/5] Store expiration times in a different array from the cached data --- src/ArrayCache.php | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/src/ArrayCache.php b/src/ArrayCache.php index e4d2561..8c672e2 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -8,6 +8,7 @@ class ArrayCache implements CacheInterface { private $limit; private $data = array(); + private $expires = array(); /** * The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). @@ -47,25 +48,16 @@ public function get($key, $default = null) return Promise\resolve($default); } - if ($this->data[$key]['expires'] === null) { - // remove and append to end of array to keep track of LRU info - $item = $this->data[$key]; - unset($this->data[$key]); - $this->data[$key] = $item; - return Promise\resolve($this->data[$key]['value']); - } - - if ($this->data[$key]['expires'] < time()) { - unset($this->data[$key]); + if (isset($this->expires[$key]) && $this->expires[$key] < time()) { + unset($this->data[$key], $this->expires[$key]); return Promise\resolve(); } - // remove and append to end of array to keep track of LRU info $value = $this->data[$key]; unset($this->data[$key]); $this->data[$key] = $value; - return Promise\resolve($this->data[$key]['value']); + return Promise\resolve($this->data[$key]); } public function set($key, $value, $ttl = null) @@ -73,21 +65,18 @@ public function set($key, $value, $ttl = null) $expires = null; if (is_int($ttl)) { - $expires = time() + $ttl; + $this->expires[$key] = time() + $ttl; } // unset before setting to ensure this entry will be added to end of array unset($this->data[$key]); - $this->data[$key] = [ - 'value' => $value, - 'expires' => $expires, - ]; - + $this->data[$key] = $value; // ensure size limit is not exceeded or remove first entry from array if ($this->limit !== null && count($this->data) > $this->limit) { reset($this->data); - unset($this->data[key($this->data)]); + $key = key($this->data); + unset($this->data[$key], $this->expires[$key]); } return Promise\resolve(true); @@ -95,7 +84,7 @@ public function set($key, $value, $ttl = null) public function remove($key) { - unset($this->data[$key]); + unset($this->data[$key], $this->expires[$key]); return Promise\resolve(true); } } From e283a49c753937fac4bb3e7b65496dbf4298e35e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Fri, 6 Apr 2018 07:47:45 +0200 Subject: [PATCH 3/5] Resolve with default value when key exists but has expired via @clue at https://github.com/reactphp/cache/pull/28#discussion_r179508133 --- src/ArrayCache.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 8c672e2..3c630f2 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -50,7 +50,7 @@ public function get($key, $default = null) if (isset($this->expires[$key]) && $this->expires[$key] < time()) { unset($this->data[$key], $this->expires[$key]); - return Promise\resolve(); + return Promise\resolve($default); } // remove and append to end of array to keep track of LRU info From e46a829d61e6ac44ef2fbc76416b7a35908ff14e Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 12 Apr 2018 17:44:06 +0200 Subject: [PATCH 4/5] Rewritten expiration queueing using SplPriorityQueue as suggested by @clue at https://github.com/reactphp/cache/pull/28#discussion_r179510039 --- src/ArrayCache.php | 50 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 39 insertions(+), 11 deletions(-) diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 3c630f2..99ef314 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -3,6 +3,7 @@ namespace React\Cache; use React\Promise; +use SplPriorityQueue; class ArrayCache implements CacheInterface { @@ -10,6 +11,11 @@ class ArrayCache implements CacheInterface private $data = array(); private $expires = array(); + /** + * @var SplPriorityQueue + */ + private $expiresQueue; + /** * The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). * @@ -40,16 +46,17 @@ class ArrayCache implements CacheInterface public function __construct($limit = null) { $this->limit = $limit; + $this->expiresQueue = new SplPriorityQueue(); + $this->expiresQueue->setExtractFlags(SplPriorityQueue::EXTR_BOTH); } public function get($key, $default = null) { - if (!array_key_exists($key, $this->data)) { - return Promise\resolve($default); + if (array_key_exists($key, $this->expires)) { + $this->garbageCollection(); } - if (isset($this->expires[$key]) && $this->expires[$key] < time()) { - unset($this->data[$key], $this->expires[$key]); + if (!array_key_exists($key, $this->data)) { return Promise\resolve($default); } @@ -57,7 +64,7 @@ public function get($key, $default = null) $value = $this->data[$key]; unset($this->data[$key]); $this->data[$key] = $value; - return Promise\resolve($this->data[$key]); + return Promise\resolve($value); } public function set($key, $value, $ttl = null) @@ -66,18 +73,14 @@ public function set($key, $value, $ttl = null) if (is_int($ttl)) { $this->expires[$key] = time() + $ttl; + $this->expiresQueue->insert($key, 0 - $this->expires[$key]); } // unset before setting to ensure this entry will be added to end of array unset($this->data[$key]); $this->data[$key] = $value; - // ensure size limit is not exceeded or remove first entry from array - if ($this->limit !== null && count($this->data) > $this->limit) { - reset($this->data); - $key = key($this->data); - unset($this->data[$key], $this->expires[$key]); - } + $this->garbageCollection(); return Promise\resolve(true); } @@ -85,6 +88,31 @@ public function set($key, $value, $ttl = null) public function remove($key) { unset($this->data[$key], $this->expires[$key]); + $this->garbageCollection(); return Promise\resolve(true); } + + private function garbageCollection() + { + // ensure size limit is not exceeded or remove first entry from array + while ($this->limit !== null && count($this->data) > $this->limit) { + reset($this->data); + unset($this->data[key($this->data)]); + } + + if ($this->expiresQueue->count() === 0) { + return; + } + + $this->expiresQueue->rewind(); + do { + $run = false; + $item = $this->expiresQueue->current(); + if ((int)substr((string)$item['priority'], 1) <= time()) { + $this->expiresQueue->extract(); + $run = true; + unset($this->data[$item['data']], $this->expires[$item['data']]); + } + } while ($run && $this->expiresQueue->count() > 0); + } } From b8b5446cbf4db8eb406d734efaf19c2d00b1ee73 Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Thu, 12 Apr 2018 22:27:17 +0200 Subject: [PATCH 5/5] Changed TTL to float as suggested by @clue at https://github.com/reactphp/cache/pull/28#discussion_r179683832 --- src/ArrayCache.php | 4 ++-- src/CacheInterface.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 99ef314..83f3e34 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -72,7 +72,7 @@ public function set($key, $value, $ttl = null) $expires = null; if (is_int($ttl)) { - $this->expires[$key] = time() + $ttl; + $this->expires[$key] = microtime(true) + $ttl; $this->expiresQueue->insert($key, 0 - $this->expires[$key]); } @@ -108,7 +108,7 @@ private function garbageCollection() do { $run = false; $item = $this->expiresQueue->current(); - if ((int)substr((string)$item['priority'], 1) <= time()) { + if ((int)substr((string)$item['priority'], 1) <= microtime(true)) { $this->expiresQueue->extract(); $run = true; unset($this->data[$item['data']], $this->expires[$item['data']]); diff --git a/src/CacheInterface.php b/src/CacheInterface.php index ca92e53..36968e0 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -34,7 +34,7 @@ public function get($key, $default = null); * * @param string $key * @param mixed $value - * @param int|null $ttl + * @param float|null $ttl * @return PromiseInterface Returns a promise which resolves to true on success of false on error */ public function set($key, $value, $ttl = null);