-
Notifications
You must be signed in to change notification settings - Fork 54
PSR-16 Simple Cache Interface implementation #126
Conversation
8837458
to
c1af969
Compare
It might be worth checking out https://github.com/php-cache/integration-tests - their goal is ensuring interop between implementations and they've just added a bunch of PSR-16 tests. |
ab443e7
to
f7c31e7
Compare
*/ | ||
public function clear() | ||
{ | ||
if ($this instanceof FlushableInterface) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Swap the conditional to make the successful operation be the last one
public function get($key, $default = null) | ||
{ | ||
$result = $this->getItem($key); | ||
return $result === null ? $default : $result; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Some yodaism needed, IMO
@@ -1577,4 +1579,74 @@ protected function normalizeKeyValuePairs(array & $keyValuePairs) | |||
} | |||
$keyValuePairs = $normalizedKeyValuePairs; | |||
} | |||
|
|||
// PSR-16 Simple Cache Interface implementation |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This comment is not needed
// PSR-16 Simple Cache Interface implementation | ||
|
||
/** | ||
* @inheritdoc |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{@inheritDoc}
, not just @inheritdoc
(also check casing)
@@ -1252,4 +1253,94 @@ protected function waitForFullSecond() | |||
$interval = (microtime(true) - time()) * 1000000; | |||
usleep((int) $interval); | |||
} | |||
|
|||
// PSR-16 Simple Cache Interface compliance tests |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment is not needed
*/ | ||
public function getMultiple($keys, $default = null) | ||
{ | ||
return $this->getItems($keys); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Default value is not contemplated by your implementation - needs to be addressed for compliance (test also needed)
*/ | ||
public function setMultiple($values, $ttl = null) | ||
{ | ||
return ! $this->setItems($values); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TTL is not contemplated by your implementation - needs to be addressed for compliance. The correct way of doing that is to set the TTL in the options, then restore it to original value after the operation was performed.
*/ | ||
public function set($key, $value, $ttl = null) | ||
{ | ||
return $this->setItem($key, $value); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
TTL is not contemplated by your implementation - needs to be addressed for compliance. The correct way of doing that is to set the TTL in the options, then restore it to original value after the operation was performed.
7baa59c
to
b5f1e97
Compare
@Ocramius do let me know if the TTL implementation was what you had in mind. Thanks for the advice. |
Maybe I missed something, but it doesn't look like you are handling the different capabilities of the underlying adapters - for instance, some can't store objects, some store integers as string. This was the biggest pita when I did the PSR-6 adapter last year. The simplest approach would be to serialize() everything, but of course I didn't do that ;) If not you'll need to figure out what data types you can save natively, or start dropping support for some adapters. See the src/Psr stuff on develop for examples of that. Finally, it doesn't look like the new PSR has much to say about errors thrown by the underlying storage. PSR-6 was explicit that they shouldn't stop the calling code carrying on. That was one of the few things that really made sense to me ;) Has there been any discussion of that? What are other implementers doing? |
Hi @Sam-Burns, thanks for your work on this but for me it doesn't look correct to just add all PSR-16 methods directly into all adapters (implementing into Lines line the following are no a good interface. if ($ttl && ! $this instanceof TtlUsedAtWriteTimeInterface) {
throw new BadMethodCallException('TTL not supported by cache adapter');
} It would be better to ship wrapper for PSR-16 like it was done for PSR-6. Also as @kynx already pointed out it's not the correct way adding an interface to mark TTL support as this interface doesn't define anything and you can (theoretically) add this feature as a storage plugin. Something like the following should be a better approach: namespace Zend\Cache\Psr;
use Psr\SimpleCache\CacheInterface as PsrCacheInterface;
use Psr\SimpleCache\CacheException as PsrCacheException;
use Psr\SimpleCache\InvalidArgumentException as PsrInvalidArgumentException;
use Zend\Cache\Storage\StorageInterface as ZendStorageInterface;
use Zend\Exception\InvalidArgumentException as ZendInvalidArgumentException;
use Exception;
use RuntimeException;
use InvalidArgumentException;
class SimpleCacheWrapper implements PsrCacheInterface
{
private $storage;
public function __construct(ZendStorageInterface $storage)
{
// TODO: test if the storage is suitable for matching PsrCacheInterface
$this->storage = $storage;
}
public function get($key, $default = null)
{
try {
$rs = $this->adapter->getItem($key, $success);
if (!$success) {
$rs = $default;
}
return $rs;
} catch (Exception $e) {
throw static::translateException($e);
}
}
// ...
private static function translateException(Exception $e)
{
$psrExceptionClass = SimpleCacheException::class;
if ($e instanceof ZendInvalidArgumentException) {
$psrExceptionClass = PsrInvalidArgumentException::class;
}
return new $psrExceptionClass($e->getMessage(), $e->getCode(), $e);
}
}
class SimpleCacheException extends RuntimeException implements PsrCacheException
{
}
class SimpleCacheInvalidArgumentException extends InvalidArgumentException implements PsrInvalidArgumentException
{
} Cheers |
@Sam-Burns: Btw. this PR should target the |
I found the following definitions in PSR-16 spec: http://www.php-fig.org/psr/psr-16/#cache
This doesn't make sense to me but it is in the spec and would mean we can just ignore the TTL in the storage doesn't support TTL on writing. http://www.php-fig.org/psr/psr-16/#cache
This on the other hand needs to be handled by checking http://www.php-fig.org/psr/psr-16/#cacheexception
For me this means the storage is allowed to throw exceptions (even on |
Yeah, but if the options object supports TTL, then we should set the TTL in the options anyway. Huge WTF for the spec ignoring this stuff silently. |
|
||
namespace Zend\Cache\Storage; | ||
|
||
interface TtlUsedAtWriteTimeInterface |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This smells a lot... What does it say? Is it just a marker? What's the intended usage?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You suggested set the TTL in the options, then restore it to original value after the [write] operation was performed
. I was trying to do that, but it will only work with some adapters - those that use the setting at write time, e.g. memcached. It won't work with those that use the TTL setting at read time, e.g. file cache. I'll see if I can come up with another approach for you, but any further suggestions would be very welcome.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Sam-Burns one possible alternative is to keep TtlUsedInterface
, and make it something like:
interface TtlUsedInterface
{
public function withTtl(int $ttl, callable $callback);
}
Where the return value would be the return value of the executed callback, and an example implementation as following:
public function withTtl(int $ttl, callable $callback)
{
$originalTtl = $this->options->getTtl();
try {
$this->options->setTtl($ttl);
return $callback();
} finally {
$this->options->setTtl($originalTtl);
}
}
*/ | ||
public function setMultiple($values, $ttl = null) | ||
{ | ||
if ($ttl && ! $this instanceof TtlUsedAtWriteTimeInterface) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned by @marc-mabe, this is to be handled silently.
public function testPsr16SetMultipleImplementationWithTtl() | ||
{ | ||
$this->_storage->setMultiple(['key' => 'value'], 1); | ||
usleep(2000001); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really a good test solution IMO :|
public function testPsr16SetImplementationWithTtl() | ||
{ | ||
$this->_storage->set('key', 'value', 1); | ||
usleep(2000001); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really a good test solution IMO :|
Recently realized that there are issues with storage systems which does not provide auto expire ability. We realized that the filesystem storage is aware of the ttl option BUT the Example: $cache = Zend\Cache\StorageFactory::factory(array(
'adapter' => array(
'name' => 'filesystem',
'options' => [ 'ttl' => 1, 'cache_dir' => '/tmp' ]
),
));
$cache->set('foo', 'bar', 3);
for ($i=1; $i <= 4; $i++) {
sleep(1);
echo "Second $i: " . ($cache->getItem('foo') ?: 'not-found') . PHP_EOL;
} This will output:
If the configuration does not set any TTL, the value would be interpreted as |
@boesing PSR-16 explicitly described to ignore the specified TTL if the underlying storage doesn't support it. This doesn't make sense to me but it's described that way. For the filesystem adapter the issue is that it will handle TTL in reading the item based on current time, last modification time and defined TTL. That means the TTL defined on "set()" will be ignored and on reading it will use the TTL defined in options. So all your items gets expired after 1 second. |
@marc-mabe I'm aware of that. However, there could be an implementation for the Filesystem storage which can ensure that expired files are not returned if stored with PSR-16. I think there will be bug reports for the filesystem storage if someone stores data with PSR-16 interface. I know, the answer will be "PSR-16 describes that behavior" but I think there could be a solution for some adapters. |
@boesing I don't like the idea to special case the PSR-16 methods on the filesystem adapter. It would be weird if setItem/getItem are having different behavior then set/get. In my opinion we should investigate to implement a plugin to add "TTL on write" support in general. This would be reusable by all storages not supporting TTL. Then in PSR-16 throwing an exception on TTL usage is not supported no matter what the spec describes. PS: The current TTL handling of the filesystem adapter helps to reduce disc access a lot in some cases. I would not like to rewrite it to something slower ;) |
@marc-mabe I kind of agree with you. What you are describing is basically the intention behind the commit 0e4c9a4 . Please note that the file caching doesn't seem to be the only one where there are problems. There are other cache adapters where the TTL is used at read time, and it doesn't matter what the setting was at write time. I wanted to use an interface to label 'adapter where TTL is used on write'. For our purpose, that is very different from 'adapter where TTL is used on read' or 'adapter where TTL isn't implemented'. This approach was the subject of some criticism in this PR. I'm more than happy to make any changes you can suggest to this PR for your project - I'm just not sure what I should do. I guess a few of the options are as follows:
On your point about throwing an exception: the PR was doing that a while ago, instead of silently ignoring the TTL if it's not going work. I can put it back if that is what you want. If you, @Ocramius or anyone else would like to tell me if any of those approaches are something you would like to see in this PR, I'm very happy to take your guidance on how to implement this. Please let me know. |
I've added some logic a few days ago but I thought that it's not useful since it was just a case I implemented for the use case we are using in our business. I've created some interfaces about a year ago (because of lack in zend-cache): interface TtlPerValueCapableInterface extends StorageInterface
{
/**
* Sets the given value with a specific ttl
*
* @param string $key
* @param mixed $value
* @param null|integer $timeout
*
* @return boolean
*
* @triggers setItem.pre(PreEvent)
* @triggers setItem.post(PostEvent)
* @triggers setItem.exception(ExceptionEvent)
*/
public function setItemWithTtl($key, $value, $timeout = null);
} As you can see, this is kinda PSR-16 behavior. As I implemented that, I did not even realized that there is any PSR like that. We did not used Filesystem cache that often but as we started to use it, we realized that there is no auto expire at all. After that, I dig into the Adapter and realized: of course, how could the adapter know what TTL was used as the item was stored. Therefore, I've implemented a new interface for our module today: interface WithoutTtlAutoExpiring extends TtlPerValueCapableInterface
{
/**
* @param string $key
*
* @return boolean
*/
public function isExpiredByTtl($key);
} I've implemented that to the /**
* @inheritdoc
*/
public function setItemWithTtl($key, $value, $timeout = null)
{
// handleTtlPerValueCall is a trait which does the same thing this PR does in `set` method
$stored = $this->handleTtlPerValueCall($this, $key, $value, $timeout);
if ($stored === false || $timeout === null) {
return $stored;
}
$this->normalizeKey($key);
$ttlFile = $this->getTtlFileName($key);
// delete related ttl file (if present)
$this->unlink($ttlFile);
$wouldblock = null;
$this->putFileContent($ttlFile, $timeout, true, $wouldblock);
if ($wouldblock) {
$this->putFileContent($ttlFile, $timeout);
}
return true;
}
/**
* @inheritdoc
*/
public function isExpiredByTtl($key)
{
$this->normalizeKey($key);
if (!$this->internalHasItem($key)) {
return false;
}
$options = $this->getOptions();
$ttlFile = $this->getTtlFileName($key);
if (!file_exists($ttlFile)) {
if ($options->getTtl() === 0) {
return false;
}
return true;
}
$ttl = $this->getFileContent($ttlFile);
$currentTtl = $options->getTtl();
$options->setTtl($ttl);
// Let base logic execute the filemtime stuff, e.g.
$expired = !$this->internalHasItem($key);
$options->setTtl($currentTtl);
return $expired;
}
/**
* Creates the filename for the ttl information
*
* @param string $normalizedKey
*
* @return string
*/
private function getTtlFileName(&$normalizedKey)
{
return $this->getFileSpec($normalizedKey) . '.ttl';
} This actually works as expected, since I've added a new Plugin I've created: class DynamicTtlCacheClear extends AbstractPlugin
{
/**
* @inheritdoc
*/
public function attach(EventManagerInterface $events, $priority = 1)
{
$this->listeners[] = $events->attach('hasItem.pre', [$this, 'hasItem'], $priority);
$this->listeners[] = $events->attach('hasItems.pre', [$this, 'hasItems'], $priority);
$this->listeners[] = $events->attach('getItem.pre', [$this, 'getItem'], $priority);
$this->listeners[] = $events->attach('getItems.pre', [$this, 'getItems'], $priority);
}
/**
* @param Event $event
*
* @return boolean
*/
public function hasItem(Event $event)
{
$storage = $event->getStorage();
if (!$storage instanceof WithoutTtlAutoExpiring) {
return true;
}
$expired = $storage->isExpiredByTtl($event->getParam('key'));
// If key is expired, stop propagation and return false
if ($expired === true) {
$event->stopPropagation();
return false;
}
return true;
}
/**
* @param Event $event
*
* @return void
*/
public function hasItems(Event $event)
{
// Trigger getItems so we can trigger item deletion if the key is expired
$this->getItems($event);
}
/**
* @param Event $event
*
* @return void
*/
public function getItem(Event $event)
{
$storage = $event->getStorage();
if (!$storage instanceof WithoutTtlAutoExpiring) {
return;
}
$key = $event->getParam('key');
if (!$storage->isExpiredByTtl($key)) {
return;
}
$storage->removeItem($key);
return;
}
/**
* @param Event $event
*
* @return void
*/
public function getItems(Event $event)
{
$keys = $event->getParam('keys');
$storage = $event->getStorage();
foreach ($keys as $key) {
$pre = new Event('getItem.pre', $storage, new \ArrayObject([
'key' => $key,
]));
$this->getItem($pre);
}
}
} Not quite sure if this is the best way of how to do this, but actually this fixed our problem with the |
@Sam-Burns
What I mean is using the wrapper approach (as described in #126 (comment)) and throw an exception on constructor if the given storage doesn't support TTL on write. Also adding an interface for TTL support is a no go if we want to implement this feature configurable by plugin for all storage adapters because a plugin can't add an interface.
@boesing |
@marc-mabe Well, I dont like the idea that the plugin has to read tons of bytes which are probably not used because of TTL expiration. I still don't get where the difference is between tags support and TTL support with the |
If you analyze normal cache use-cases you will notice that a cache should have at least a hit rate of 80%. That means you will only read unused cached data for max. 20% but you will not make additional IO for reading TTL in 80%. Of course both ways would be possible, storing expiration time in data file or as an own file. Without real world benchmaks nobody can tell ;) But even on going the way to store as separate file you shouldn't add weird public exposed methods to the filesystem adapter for making a pligin work. It should all be done within the plugin. |
@boesing |
I really like the idea of this patch, and fully support implementing PSR-16. However, I also agree with @marc-mabe that we should do so via a decorator class instead, as he detailed in a previous comment. That approach is simpler to test, and more in line with how we have supported standards in our existing packages in the past (PSR-6 support in this package, PSR-3 support in zend-log, etc.). (The approach also allows users to continue using zend-cache as-is, and then decorate it to use as a PSR-16 dependency for 3rd party code.) |
This patch builds on both the approach made in zendframework#126 as well as a comment to that patch made by @marc-mabe (zendframework#126 (comment)) in order to provide a decorator for zend-cache storage adapters that fulfills the PSR-16 `SimpleCacheInterface`. Usage is as follows: ```php // $adapter is an instance of StorageInterface $cache = new SimpleCacheDecorator($adapter); ``` From there, you can then use `$cache` anywhere you would normally use a PSR-16 instance.
Closing in favor of #152, which provides an approach consistent with existing PSR-6 support. |
This patch builds on both the approach made in zendframework#126 as well as a comment to that patch made by @marc-mabe (zendframework#126 (comment)) in order to provide a decorator for zend-cache storage adapters that fulfills the PSR-16 `SimpleCacheInterface`. Usage is as follows: ```php // $adapter is an instance of StorageInterface $cache = new SimpleCacheDecorator($adapter); ``` From there, you can then use `$cache` anywhere you would normally use a PSR-16 instance.
This patch builds on both the approach made in zendframework#126 as well as a comment to that patch made by @marc-mabe (zendframework#126 (comment)) in order to provide a decorator for zend-cache storage adapters that fulfills the PSR-16 `SimpleCacheInterface`. Usage is as follows: ```php // $adapter is an instance of StorageInterface $cache = new SimpleCacheDecorator($adapter); ``` From there, you can then use `$cache` anywhere you would normally use a PSR-16 instance.
This patch builds on both the approach made in zendframework#126 as well as a comment to that patch made by @marc-mabe (zendframework#126 (comment)) in order to provide a decorator for zend-cache storage adapters that fulfills the PSR-16 `SimpleCacheInterface`. Usage is as follows: ```php // $adapter is an instance of StorageInterface $cache = new SimpleCacheDecorator($adapter); ``` From there, you can then use `$cache` anywhere you would normally use a PSR-16 instance.
This patch builds on both the approach made in zendframework#126 as well as a comment to that patch made by @marc-mabe (zendframework#126 (comment)) in order to provide a decorator for zend-cache storage adapters that fulfills the PSR-16 `SimpleCacheInterface`. Usage is as follows: ```php // $adapter is an instance of StorageInterface $cache = new SimpleCacheDecorator($adapter); ``` From there, you can then use `$cache` anywhere you would normally use a PSR-16 instance.
This patch builds on both the approach made in zendframework#126 as well as a comment to that patch made by @marc-mabe (zendframework#126 (comment)) in order to provide a decorator for zend-cache storage adapters that fulfills the PSR-16 `SimpleCacheInterface`. Usage is as follows: ```php // $adapter is an instance of StorageInterface $cache = new SimpleCacheDecorator($adapter); ``` From there, you can then use `$cache` anywhere you would normally use a PSR-16 instance.
Adds standards compliance for PSR-16: Simple Cache Interface.