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..83f3e34 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -3,11 +3,18 @@ namespace React\Cache; use React\Promise; +use SplPriorityQueue; class ArrayCache implements CacheInterface { private $limit; private $data = array(); + private $expires = array(); + + /** + * @var SplPriorityQueue + */ + private $expiresQueue; /** * The `ArrayCache` provides an in-memory implementation of the [`CacheInterface`](#cacheinterface). @@ -39,10 +46,16 @@ 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->expires)) { + $this->garbageCollection(); + } + if (!array_key_exists($key, $this->data)) { return Promise\resolve($default); } @@ -51,28 +64,55 @@ public function get($key, $default = null) $value = $this->data[$key]; unset($this->data[$key]); $this->data[$key] = $value; - return Promise\resolve($value); } - public function set($key, $value) + public function set($key, $value, $ttl = null) { + $expires = null; + + if (is_int($ttl)) { + $this->expires[$key] = microtime(true) + $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); - unset($this->data[key($this->data)]); - } + $this->garbageCollection(); return Promise\resolve(true); } public function remove($key) { - unset($this->data[$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) <= microtime(true)) { + $this->expiresQueue->extract(); + $run = true; + unset($this->data[$item['data']], $this->expires[$item['data']]); + } + } while ($run && $this->expiresQueue->count() > 0); + } } diff --git a/src/CacheInterface.php b/src/CacheInterface.php index 3832fac..36968e0 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 float|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() + ); + } }