Skip to content

[Cache] Symfony PSR-6 implementation #17408

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 1 commit into from
Jan 19, 2016
Merged

Conversation

nicolas-grekas
Copy link
Member

Q A
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets -
License MIT
Doc PR -

From the provided README.md:

This component provides a strict PSR-6 implementation for adding cache to your applications. It is designed to have a low overhead so that caching is fastest. It ships with a few caching adapters for the most widespread and suited to caching backends. It also provides a doctrine/cache proxy adapter to cover more advanced caching needs.

Todo:

  • add more tests
  • add a FilesystemAdapter

{
$now = time();

if (null === $expiration) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess you wanted to write $time, and not $expiration

@nicolas-grekas nicolas-grekas force-pushed the cache branch 2 times, most recently from c9d7fae to 82324ea Compare January 17, 2016 11:31
@andrerom
Copy link
Contributor

Interesting, like the simplistic approach, and especially that there is only one backend lookup for getItem call. But missing use of multi get / set / (...) to reduce backend roundtrips further on getItems(array) and batch deferred updates. And calls to hasItem and getItem (and opposite) will result in two lookups here, but last point is maybe to be expected given the spec.

I know @Nyholm, @aequasi and others have been working on this topic, and hope to be able to contribute from eZ as well.

Could be an opportunity to join forces.


Side: On design side would perhaps opt for following approach:

  • 2 layers, the service (api) psr6 layer, and the adapter/driver layer with more low level interfaces, to avoid conflicts with future PSR's
  • Aim to support Tagging for reliable cache expiry on entity changes in most real world applications

@Nyholm
Copy link
Member

Nyholm commented Jan 17, 2016

Thank you for the ping.

I ran this implementation on our integration tests and found some issues. Like you do not protect for all the reserved characters and you have forgotten to clear the deferred items on $pool->clear().

The php-cache supports tagging, hierarchal keys and have doctrine bridges in both directions. We are currently outperforming other PSR-6 caches with a factor of 2 and we have ideas for even more performance improvements.

We would definitely like to join forces somehow. We are open for a discussion about that.

@@ -0,0 +1,8 @@
Symfony PSR-6 implementaton for caching
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

implementaton => implementation

@nicolas-grekas
Copy link
Member Author

Comments addressed, thank you @Nyholm for the link to the integration test, this will definitely help.
As I tried to state in the README, I target a strict implementation of PSR-6 here. The goal is not to replace any existing caching libraries (php-cache, stash, doctrine/cache, etc.) but only provide the features that have be standardized, so that we can build interoperability on top. This means that adding more interfaces or features (tagging) is out of scope, unless a new PSR standardize them.

@nicolas-grekas
Copy link
Member Author

I added testing with cache/integration-tests, I'll look at the failures asap. I already fixed the missing : reserved char.

@Nyholm
Copy link
Member

Nyholm commented Jan 17, 2016

I target a strict implementation of PSR-6 here. The goal is not to replace any existing caching libraries (php-cache, stash, doctrine/cache, etc.) but only provide the features that have be standardized, so that we can build interoperability on top.

If the goal is not to replace any existing cache libraries, why build another cache library? Wouldn't it be better if all Symfony components using cache relied on the PSR6 interface and then required the virtual package psr/cache-implementation: ~1.0?

That way we use the benefit of PSRs and we can let the user decide what cache library to use. If they like to use feature X they install a library with feature X. If they want a strict implementation they would choose a strict implementation.

@fabpot
Copy link
Member

fabpot commented Jan 17, 2016

@Nyholm ... and this component is a strict implementation that users can use if they want to, like any other ones. The goal of PSRs (if I get this right) is to allow several implementations with different philosophies, not to standardize on only one.

@Nyholm
Copy link
Member

Nyholm commented Jan 17, 2016

@Nyholm ... and this component is a strict implementation that users can use if they want to, like any other ones. The goal of PSRs (if I get this right) is to allow several implementations with different philosophies, not to standardize on only one.

You are correct. That is the goal of PSRs. But it was this sentence I reacted to:

The goal is not to replace any existing caching libraries

If the goal is to provide a strict implementation of PSR-6 that will replace (or be an option to) any existing library, then I agree that we need this component. Otherwise I do not see the benefits at all.

@fabpot
Copy link
Member

fabpot commented Jan 17, 2016

Understood, we definitely want to provide a strict implementation that can replace existing libraries :)

@Nyholm
Copy link
Member

Nyholm commented Jan 17, 2016

Okey, awesome.

Im happy to help. @nicolas-grekas do you want PR's with more adapters now or once this is merged?

@nicolas-grekas
Copy link
Member Author

@Nyholm thank you that's much appreciated. Your test suite is already an amazing help. I think it would be easier to add more adapters after this one has been merge.
For now I have the following failures:

  1. Symfony\Component\Cache\Tests\CacheTest::testSaveWithNoData
    When saving an object with no data, we should generate an error.

I didn't read anything about this in PSR-6, did I miss something?

For the other ones, I'll check tomorrow hopefully:

  1. Symfony\Component\Cache\Tests\CacheTest::testDeferredSaveWithoutCommit
    A deferred item should automatically be committed on CachePool::__destruct().

  2. Symfony\Component\Cache\Tests\CacheTest::testExpiration
    Failed asserting that true is false.

  3. Symfony\Component\Cache\Tests\CacheTest::testIsHit
    Failed asserting that false is true.

  4. Symfony\Component\Cache\Tests\CacheTest::testIsHitDeferred
    Failed asserting that false is true.

@dunglas
Copy link
Member

dunglas commented Jan 17, 2016

Using a standardized interface for cache in Symfony will be very helpful!

What do you think about using the PSR-6 interface instead of the Doctrine Cache one in #16838 and #16917?

*/
public function save(CacheItemInterface $item)
{
if (!$item instanceof CacheItem) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you need to do this?
It breaks the usage of CacheItemInterface interface used as type hinting.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@blanchonvincent the PSR specifies that implementation are not compatible. So the type hinting is specified by the PSR interface, but the $item MUST be an instance from the same implementation (i.e: I cannot make work my CacheItem from my custom Redis implementation to work with your custom Memcached implementation of CacheItemPool).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You breaking the whole idea of PSR-6 you can interchange multiple libraries together... That's why Interfaces exists

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@GCDS did you read @xavierleune's comment just in top of yours?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the PSR itself:

Calling Libraries SHOULD NOT assume that an Item created by one Implementing Library is compatible with a Pool from another Implementing Library.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, sorry I thought differently...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW, Interoperability between implementations is possible using something like the attached ProxyAdapter

@Nyholm
Copy link
Member

Nyholm commented Jan 18, 2016

  1. When saving an object with no data, we should generate an error.

We have given some thoughts in to this. The PSR doc block on save says "True if the item was successfully persisted. False if there was an error.". I claim it is an error because null is valid data.

Consider this:

$item = $pool->getItem('key');
$pool->save($item); // Assume this is okey

$pool->hasItem('key'); // Should return true, yes? Or else, what is the point of saving?

/** 
 * Doc block for get(): 
 * If isHit() returns false, this method MUST return null. Note that null
 * is a legitimate cached value, so the isHit() method SHOULD be used to
 * differentiate between "null value was found" and "no value was found."
 * 
 * Doc block for isHit():
 * Confirms if the cache item lookup resulted in a cache hit.
 */
$pool->getItem('key')->isHit(); // Must be false because "no value was found."

// So ...
$pool->hasItem('key') !== $pool->getItem('key')->isHit(); // This makes no sense.

The pool should not have an item that never can be a hit. That would be a stale item and should be removed.
If you really want to store just the key you should just do $item->set(null);

@nicolas-grekas
Copy link
Member Author

@Nyholm I don't get your example, to me, assuming "key" is not already in the cache:

$pool->hasItem('key'); // false
$item = $pool->getItem('key');
$item->get(); // null
$item->isHit(); // false
$pool->save($item); // true
$item = $pool->getItem('key');
$item->get(); // null
$item->isHit(); // true -> a value has been found, we saved it before!

@Nyholm
Copy link
Member

Nyholm commented Jan 18, 2016

You have not saved any value. Since: "Note that null is a legitimate cached value, so the isHit() method SHOULD be used to differentiate between "null value was found" and "no value was found.""

The last two rows in you example:

$item->get(); // null
$item->isHit(); // true -> a value has been found, we saved it before!

If $item->isHit() === true and $item->get() returns null you assume that someone has done a $item->set(null) which is not the case.

The point is, null should not be treated any special.

@nicolas-grekas
Copy link
Member Author

If $item->isHit() === true and $item->get() returns null you assume that someone has done a $item->set(null) which is not the case.

My example above clearly allows to differentiate between a null a "found" vs a null as "not found". Nothing in the spec says that $item->set(null) is the only way to get a "data" null. This still looks like a non standard requirement to me.

@ghost
Copy link

ghost commented Jan 18, 2016

@nicolas-grekas Thank you for working on this - it is awesome! We should merge this.

@dunglas
Copy link
Member

dunglas commented Jan 18, 2016

+1 to merge it now. It will allow to start migrating metadata systems and other places in Symfony using Doctrine Cache internally to PSR-6.

"require-dev": {
"cache/integration-tests": "^0.6",
"doctrine/cache": "~1.6"
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it miss

provides: {
    "psr/cache-implementation": "1.0"
},

?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joelwurtz indeed, fixed thanks

@fabpot
Copy link
Member

fabpot commented Jan 19, 2016

Thank you @nicolas-grekas.

@fabpot fabpot merged commit 91e482a into symfony:master Jan 19, 2016
fabpot added a commit that referenced this pull request Jan 19, 2016
This PR was merged into the 3.1-dev branch.

Discussion
----------

[Cache] Symfony PSR-6 implementation

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

From the provided README.md:

This component provides a strict [PSR-6](http://www.php-fig.org/psr/psr-6/) implementation for adding cache to your applications. It is designed to have a low overhead so that caching is fastest. It ships with a few caching adapters for the most widespread and suited to caching backends. It also provides a `doctrine/cache` proxy adapter to cover more advanced caching needs.

Todo:
- [ ] add more tests
- [ ] add a FilesystemAdapter

Commits
-------

91e482a [Cache] Symfony PSR-6 implementation
@nicolas-grekas nicolas-grekas deleted the cache branch January 19, 2016 08:26
@Tobion
Copy link
Contributor

Tobion commented Jan 19, 2016

IMO namespaces handling should be a separate decorator and not built into the adapter directly. See doctrine/cache#96

@Tobion
Copy link
Contributor

Tobion commented Jan 19, 2016

I also want to express my objections against the recent fast merges of new components. This was merged in less than two days which does not give enough time to review and discuss a full new component at all. Also it's against our own rules which says

A pull request can be merged if: Enough time was given for peer reviews (at least 2 days for "regular" pull requests, and 4 days for pull requests with "a significant impact");
http://symfony.com/doc/current/contributing/code/core_team.html

I hope it's not going to be of the same quality as the LDAP component.

@andrerom
Copy link
Contributor

IMO namespaces handling should be a separate decorator and not built into the adapter directly. See doctrine/cache#96

True, it probably should be, but also a bit of a orange to apple comparison. In doctrine this is a namespace kept in cache and that needs to be checked on every cache lookup, that is not the case here (luckily, the extra cache lookup adds overhead, use case can be covered by tagging instead to avoid it).

@nicolas-grekas
Copy link
Member Author

@Tobion sorry if you feel uncomfortable, we merged quickly that's true... I invite you to open a PR to move the namespace concept in it's own decorator if you think it's better (taking @andrerom into account). The component will see more enhancements until 3.1 is released, but at least we don't have to discuss about its interfaces, which makes the situation totally different from the Ldap component hopefully.

if (!$item instanceof CacheItem) {
return false;
}
static $prefix = "\0Symfony\Component\Cache\CacheItem\0";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not a const?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

because a const is public...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@internal? Or a private static property? This one looks a bit weird.

@aurimasniekis
Copy link
Contributor

My suggestion for namespace would be to let Adaptor generate key with a namespace. For e.g. most Redis tools groups keys if they have : separators. If Adaptor for e.g. doesn't support some characters or uses own rules with namespaces it would be better I think in this way. Like I said for Redis e.g. to replace \ with : it would allow better viewing for redis tools...

@nicolas-grekas
Copy link
Member Author

@GCDS namespace (or prefix really) are already managed by adapters (they must deal with it in their constructor). Which means a Redis adapter could do whatever you'd need there. To me, the simple prefixing logic is rightfully in the abstract, because adding a prefix is really something common. Doing namespaces àla doctrine/cache is for a decorator, or at least not for the abstract, I agree.

@aurimasniekis
Copy link
Contributor

Ahh yeah, sounds correct to me :)

{
$this->namespace = $namespace;
$this->createCacheItem = \Closure::bind(
function ($key, $value, $isHit) use ($defaultLifetime) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding a constructor to CacheItem marked with @internal? It will be easier to understand. (Currently even PHPStorm isn't able to interpret this snippet and marks it in red).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Btw it will remove the duplication of this factory closure in ProxyAdapter.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I preferred raw speed over phpstorm compliance: a constructor would slow down things for no valid reason (adding one function call in the chain). I doubt @internal would be enough to prevent erroneous user side instantiation. The current no-constructor-nor-setter (for key and isHit) means we technically annoy users enough so that they are free to missuse but forced to think about it :)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't a constructor call or a closure call in the chain similar in term of perfs?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes of course, but making the constructor public has drawback IMHO (as detailed above). This drawback could be fixed by using a private constructor + a rebound closure, which is when an additional function call comes up. That's what I meant :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.