Skip to content

Add cache expiration support (TTL) #29

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 8, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,13 @@ provide alternate implementations.

#### get()

The `get(string $key, mixed $default = null): PromiseInterfae` method can be used to
The `get(string $key, mixed $default = null): PromiseInterface` method can be used to
retrieve an item from the cache.

This method will resolve with the cached value on success or with the
given `$default` value when no item can be found or when an error occurs.
Similarly, an expired cache item (once the time-to-live is expired) is
considered a cache miss.

```php
$cache
Expand All @@ -55,15 +57,25 @@ This example fetches the value of the key `foo` and passes it to the

#### set()

The `set(string $key, mixed $value, ?float $ttl = null): PromiseInterface` method can be used to
store an item in the cache.

This method will resolve with `true` on success or `false` when an error
occurs. If the cache implementation has to go over the network to store
it, it may take a while.

The optional `$ttl` parameter sets the maximum time-to-live in seconds
for this cache item. If this parameter is omitted (or `null`), the item
will stay in the cache for as long as the underlying implementation
supports. Trying to access an expired cache item results in a cache miss,
see also [`get()`](#get).

```php
$cache->set('foo', 'bar');
$cache->set('foo', 'bar', 60);
```

This example eventually sets the value of the key `foo` to `bar`. If it
already exists, it is overridden. To provide guarantees as to when the cache
value is set a promise is returned. The promise will fulfill with `true` on success
or `false` on error. If the cache implementation has to go over the network to store
it, it may take a while.
already exists, it is overridden.

#### remove()

Expand Down
34 changes: 29 additions & 5 deletions src/ArrayCache.php
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -43,6 +44,11 @@ public function __construct($limit = null)

public function get($key, $default = null)
{
// delete key if it is already expired => below will detect this as a cache miss
if (isset($this->expires[$key]) && $this->expires[$key] < microtime(true)) {
unset($this->data[$key], $this->expires[$key]);
}

if (!array_key_exists($key, $this->data)) {
return Promise\resolve($default);
}
Expand All @@ -55,24 +61,42 @@ public function get($key, $default = null)
return Promise\resolve($value);
}

public function set($key, $value)
public function set($key, $value, $ttl = null)
{
// unset before setting to ensure this entry will be added to end of array
// unset before setting to ensure this entry will be added to end of array (LRU info)
unset($this->data[$key]);
$this->data[$key] = $value;

// sort expiration times if TTL is given (first will expire first)
unset($this->expires[$key]);
if ($ttl !== null) {
$this->expires[$key] = microtime(true) + $ttl;
asort($this->expires);
}

// 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)]);
// first try to check if there's any expired entry
// expiration times are sorted, so we can simply look at the first one
reset($this->expires);
$key = key($this->expires);

// check to see if the first in the list of expiring keys is already expired
// if the first key is not expired, we have to overwrite by using LRU info
if ($key === null || $this->expires[$key] > microtime(true)) {
reset($this->data);
$key = key($this->data);
}
unset($this->data[$key], $this->expires[$key]);
}

return Promise\resolve(true);
}

public function remove($key)
{
unset($this->data[$key]);
unset($this->data[$key], $this->expires[$key]);

return Promise\resolve(true);
}
}
29 changes: 24 additions & 5 deletions src/CacheInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ interface CacheInterface
*
* This method will resolve with the cached value on success or with the
* given `$default` value when no item can be found or when an error occurs.
* Similarly, an expired cache item (once the time-to-live is expired) is
* considered a cache miss.
*
* ```php
* $cache
Expand All @@ -29,14 +31,31 @@ interface CacheInterface
public function get($key, $default = null);

/**
* Store an item in the cache, returns a promise which resolves to true on success or
* false on error.
* Stores an item in the cache.
*
* This method will resolve with `true` on success or `false` when an error
* occurs. If the cache implementation has to go over the network to store
* it, it may take a while.
*
* The optional `$ttl` parameter sets the maximum time-to-live in seconds
* for this cache item. If this parameter is omitted (or `null`), the item
* will stay in the cache for as long as the underlying implementation
* supports. Trying to access an expired cache item results in a cache miss,
* see also [`get()`](#get).
*
* ```php
* $cache->set('foo', 'bar', 60);
* ```
*
* This example eventually sets the value of the key `foo` to `bar`. If it
* already exists, it is overridden.
*
* @param string $key
* @param mixed $value
* @return PromiseInterface Returns a promise which resolves to true on success of false on error
* @param mixed $value
* @param ?float $ttl
* @return PromiseInterface Returns a promise which resolves to `true` on success or `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
Expand Down
41 changes: 41 additions & 0 deletions tests/ArrayCacheTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,45 @@ public function testGetWithLimitedSizeWillUpdateLRUInfo()
$this->cache->get('bar')->then($this->expectCallableOnceWith(null));
$this->cache->get('baz')->then($this->expectCallableOnceWith('3'));
}

public function testGetWillResolveWithValueIfItemIsNotExpired()
{
$this->cache = new ArrayCache();

$this->cache->set('foo', '1', 10);

$this->cache->get('foo')->then($this->expectCallableOnceWith('1'));
}

public function testGetWillResolveWithDefaultIfItemIsExpired()
{
$this->cache = new ArrayCache();

$this->cache->set('foo', '1', 0);

$this->cache->get('foo')->then($this->expectCallableOnceWith(null));
}

public function testSetWillOverwritOldestItemIfNoEntryIsExpired()
{
$this->cache = new ArrayCache(2);

$this->cache->set('foo', '1', 10);
$this->cache->set('bar', '2', 20);
$this->cache->set('baz', '3', 30);

$this->cache->get('foo')->then($this->expectCallableOnceWith(null));
}

public function testSetWillOverwriteExpiredItemIfAnyEntryIsExpired()
{
$this->cache = new ArrayCache(2);

$this->cache->set('foo', '1', 10);
$this->cache->set('bar', '2', 0);
$this->cache->set('baz', '3', 30);

$this->cache->get('foo')->then($this->expectCallableOnceWith('1'));
$this->cache->get('bar')->then($this->expectCallableOnceWith(null));
}
}
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy