-
Notifications
You must be signed in to change notification settings - Fork 61
Symfony taggable cache #373
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
Conversation
@@ -36,6 +36,8 @@ | |||
"php-http/mock-client": "^0.3.2", | |||
"symfony/process": "^2.3||^3.0", | |||
"symfony/http-kernel": "^2.3||^3.0", | |||
"symfony/lock": "^3.4@dev", |
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.
These are require-dev
.
doc/symfony-cache-configuration.rst
Outdated
SurrogateInterface $surrogate = null, | ||
array $options = [] | ||
) { | ||
$store = new TaggableStore($kernel->getCacheDir()); |
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 sure if that's the best way to document but basically you just have to provide TaggableStore
instead of having Symfony calling createStore()
.
@@ -52,10 +53,26 @@ public function refresh($url, array $headers = []) | |||
protected function configureOptions() | |||
{ | |||
$resolver = parent::configureOptions(); | |||
$resolver->setDefaults([ |
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.
I've unified here so it matches the PurgeListener
options.
src/ProxyClient/Symfony.php
Outdated
*/ | ||
public function invalidateTags(array $tags) | ||
{ | ||
// TODO: how should we escape best here? |
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.
I would like to escape ,
and maybe also new line
but we need to unescape again in the PurgeListener
so I'm not sure if using the same method as Varnish
does is a good idea. Especially because Varnish
just escapes by replacing both with a _
which makes it impossible to unescape again. To be honest, I find tags containing a ,
strange anyway so I'd prohibit this in the ResponseTagger
upfront so it cannot happen at all but I guess that would be a BC break. Wdyt?
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 far as i remember the logic with escaping was that we don't care about unescaping. the worst case would be that foo,bar and foo_bar collide, which would mean we invalidate too many things. its an edge case and one we decided we can live with, as its semantically correct (invalidating more then necessary is ok, the opposite would be a problem)
this works as long as escaping is done both when setting the tags and when creating invalidation requests.
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.
I see. So I'll just escape and don't care about unescaping, right? It's an edge case anyway, I really don't know why I would ever use a comma in a tag.
We could, however, also use the same header multiple times. There's nothing wrong in using
X-Cache-Tags: foobar
X-Cache-Tags: other tag
But I don't know if http-plug supports this?
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.
i'd replace all ,
with _
but then collapse the tags into one header with , separation. in the listener, you can then still split by ,
.
if i remember correctly, the varnish client even splits this into multiple requests if the list of tags is getting too long, to avoid problems with too long headers.
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.
Implemented in b452710. Sorry, I'm too used to phpunit, I never used mockery so it was way faster for me to use the "native" mock stuff for consecutive argument assertions :)
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.
Ah, and I implemented it so it's a more general solution. I thought that might be needed by other proxies too in the future. I did not adjust Varnish though because there's some regex stuff going on there and I figured that's maybe too specific.
src/SymfonyCache/PurgeListener.php
Outdated
&& $store instanceof TaggableStore | ||
) { | ||
// TODO: need to unescape again here | ||
$tags = explode(',', $request->headers->get($this->purgeTagsHeader)); |
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.
See my comment about unescaping in Symfony
.
src/SymfonyCache/TaggableStore.php
Outdated
/** | ||
* @return TagAwareAdapter | ||
*/ | ||
public function getCache() |
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.
Basically this is only needed for unit tests but it doesn't harm either.
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.
i would use reflection in the unit test rather than expose this. we don't want people to interact with the tag cache from outside.
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.
I agree. Fixed in d56f6a4.
* | ||
* @author Yanick Witschi <yanick.witschi@terminal42.ch> | ||
*/ | ||
class TaggableStore implements StoreInterface |
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.
Do you like that name? I wanted it to remain simple. The one the Symfony core provides is called Store
so I just simply went for TaggableStore
. Note that there's quite some stuff going on here. Proof that it works is a pretty extensive unit test suite for that class 😄
de5267b
to
d7f239c
Compare
$cacheKey = $this->getCacheKey($request); | ||
|
||
if (isset($this->locks[$cacheKey])) { | ||
return false; |
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.
why not trying to acquire this lock ?
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.
Basically, I checked the implementation of Store
(see https://github.com/symfony/symfony/blob/master/src/Symfony/Component/HttpKernel/HttpCache/Store.php#L69) so that's why it's the way it is. How should that whole method look like in your opinion?
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.
So if you lock then release. You'll never be able to lock again... Is it the expected behavior?
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.
I guess not :) You mean either we drop the isset()
there or if we keep it as is, we need to unset($this->locks[$cacheKey]);
in unlock()
? :)
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.
Yep, either unset in the release method. Or try to acquire it here. As you want
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.
Fixed and covered by unit test in 5f7a1ea.
src/SymfonyCache/TaggableStore.php
Outdated
{ | ||
/** @var LockInterface $lock */ | ||
foreach ($this->locks as $lock) { | ||
$lock->release(); |
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 should wrap in a try/catch block
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.
True, will add!
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.
Added in 2f4b534.
src/SymfonyCache/TaggableStore.php
Outdated
|
||
$this->purgeTagsHeader = $purgeTagsHeader; | ||
$this->cache = $cache = new TagAwareAdapter(new FilesystemAdapter('fos-http-cache', 0, $cacheDir)); | ||
$this->lockFactory = new Factory(new FlockStore($cacheDir)); |
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.
The issue with Flock, is that we need to create an empty file for each key, and don't delete theme (for race condition).
Given we will create a lot of keys here, it will consume a lot of inode. I suggest tu use semaphore here and fallback to flock if semaphore is not supported. See https://github.com/symfony/symfony/blob/3ff0ff02accafaeb6f35eca5323bcb6be0f3311e/src/Symfony/Component/Console/Command/LockableTrait.php#L46-L50
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.
Yeah, that's a general problem that is not specific to this implementation (it's the same in the default Store
of HttpCache
). I'll add semaphore though, thanks for the hint!
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.
Fixed in 486008e.
src/SymfonyCache/TaggableStore.php
Outdated
} | ||
|
||
/** | ||
* @return TagAwareAdapter |
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.
what's about TagAwareAdapterInterface
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.
See my comment in __construct()
. We use a hardcoded implementation because we don't want other adapters to be used :)
src/SymfonyCache/TaggableStore.php
Outdated
} | ||
|
||
$this->purgeTagsHeader = $purgeTagsHeader; | ||
$this->cache = $cache = new TagAwareAdapter(new FilesystemAdapter('fos-http-cache', 0, $cacheDir)); |
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.
Adapter could be injected instead of built (if you inject it, you should inject the lock too, because the injected cache can be a shared cache like redis, and the Lock should be shared too)
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.
Both are not injectable on purpose. Otherwise it would be a fully PSR-6 compatible Store
which was rejected in Symfony core as no other adapter except for file makes sense and might be dangerous using because of concurrency. See the discussion in #286 and the referenced PR in Symfony.
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 can TypeHint with TagAwareInterface ? What if someone want to use redis tombe share cache between multiple front?
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.
I guess you did not really understand: The users must not use any other adapter like redis etc. It must be forbidden. That's why it's hardcoded to the adapters that make sense.
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.
The point of Fabien is to not use PSR6 and simply use $cached = include(...)
. (His comment give 3 good reasons).
As soon as you include cache component and use PSR6, I don't understand why adapters MUST be the Filesystem?
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.
Here's the comment: symfony/symfony#20061 (comment)
Quote:
The cache contains some PHP (when using ESI for instance) and storing PHP in anything else than a filesystem would mean eval()ing strings coming from Redis/Memcache/...,
So why would you allow anything else than Filesystem
?
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.
Because here you use a cache adapter on top of filesystem and you must deal with the same issue.
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 soon as you start using PSR6, you are dealing with the same interface (same output, same evaluate things...) The only difference between adapter are performance vs scalability
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.
Hmm, I still don't get your point, sorry 😄 So trying to sum up: There was a pull request for Symfony core allowing to use any PSR-6 cache back end. It was rejected because it would allow to use Redis, Memcache etc. as cache back ends. Ergo: everything except for a filesystem cache does not make sense. However, for me this does not mean we cannot use the cache component. We just have to make sure nobody can use anything else than the Filesystem
adapter.
* | ||
* @return bool|string true if the lock is acquired, the path to the current lock otherwise | ||
*/ | ||
public function lock(Request $request) |
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.
seems to be not used
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.
What do you mean? It's part of the StoreInterface
.
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.
Oops.. nevermind
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.
great job! i commented on a couple of things, but this looks really good to me.
i guess the most difficult question right now is about allowing to inject the cache implementation or not. i can see scenarios where it would indeed be a security problem to use memcache or redis, but i think there are also valid scenarios where its just as save as the filesystem (at least with redis which does password protect the connection)
doc/symfony-cache-configuration.rst
Outdated
Symfony's `HttpCache` does not support cache invalidation by tags by default. | ||
However, this library ships with a `TaggableStore` that provides exactly that. | ||
If you want to use this, adjust your `AppCache` as follows:: | ||
|
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.
can you add a comment that specifies the filename? i guess // app/AppCache.php
?
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.
There are 2-3 examples before that all talk about the AppCache
, I think it's redundant information :)
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.
ah ok, the section above does the same. i find it still useful, but am ok with consistency here for now. might go over that doc at some point again.
doc/symfony-cache-configuration.rst
Outdated
|
||
use FOS\HttpCache\SymfonyCache\TaggableStore(); | ||
|
||
// ... |
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.
please have the class AppCache extends HttpCache
line here
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.
Same here, I guess the people know about app/AppCache.php
but I added a hint in 0e3d337 anyway (for the first example to not mess up with all other examples).
doc/symfony-cache-configuration.rst
Outdated
HttpKernelInterface $kernel, | ||
StoreInterface $store, | ||
SurrogateInterface $surrogate = null, | ||
array $options = [] |
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.
where do these parameters come from?
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.
I took the very same example as already used in the Cache event listeners
section.
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.
if we would use the one from FrameworkBundle, we could just overwrite the createStore method. i guess the reason is that here we are in the library and not the symfony bundle, so we don't want any extra dependencies. so this makes sense (apart from the $store parameter)
doc/symfony-cache-configuration.rst
Outdated
*/ | ||
public function __construct( | ||
HttpKernelInterface $kernel, | ||
StoreInterface $store, |
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.
can we remove this parameter? its never used as you then create the store manually.
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.
Yeah well, these parameters depend on whether you use the HttpCache
of the FrameworkBundle
or HttpKernel
directly. Not sure what to do here best but I mean, it's kind of obvious what you have to do there, isn't it? 😄
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.
$store makes sense for normal constructor but not for this class, because its unused as you define $store below in the constructor code.
src/ProxyClient/Symfony.php
Outdated
*/ | ||
public function invalidateTags(array $tags) | ||
{ | ||
// TODO: how should we escape best here? |
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 far as i remember the logic with escaping was that we don't care about unescaping. the worst case would be that foo,bar and foo_bar collide, which would mean we invalidate too many things. its an edge case and one we decided we can live with, as its semantically correct (invalidating more then necessary is ok, the opposite would be a problem)
this works as long as escaping is done both when setting the tags and when creating invalidation requests.
src/SymfonyCache/PurgeListener.php
Outdated
$store = $event->getKernel()->getStore(); | ||
|
||
if ($request->headers->has($this->purgeTagsHeader) | ||
&& $store instanceof TaggableStore |
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.
should we not fail if there is a request to purge tags but we don't have a store supporting purge?
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.
Matter of preference. I thought about that too. Either fail or ignore silently. You prefer failing?
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.
its a basic setup error, not a runtime error that could occasionally happen, so yes i would explode loudly.
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.
Implemented with the new listener now.
src/SymfonyCache/PurgeListener.php
Outdated
if ($event->getKernel()->getStore()->purge($request->getUri())) { | ||
$store = $event->getKernel()->getStore(); | ||
|
||
if ($request->headers->has($this->purgeTagsHeader) |
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.
why do we do tag invalidation in the same listener as purging? i feel like it should be a separate listener. in varnish, its glued into the ban handling because its a ban operation. for httpcache it seems to make sense to consider it not a BAN because its not a blacklist but a direct action onto the cache store. but purge sounds wrong to me too, because the path of the tag invalidation request does not matter. could we use a separate HTTP method like PURGETAGS? (this is an open question, i am not convinced it would be the best. but the mixup looks odd to me here)
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.
Well, I can just tell what my idea behind this was: PURGE
is not an official HTTP method by specification. If I was to pick one that exists, I would've probably gone with DELETE
. Anyway, the spec allows to invent custom ones so yeah, we could use PURGETAGS
. But then again, to purge, means to clean and so I checked the docs of Varnish (which I personally consider to be somewhat a market leader here) and it says:
A purge is what happens when you pick out an object from the cache and discard it along with its variants. Usually a purge is invoked through HTTP with the method PURGE.
And "tagging" to me is an object from the cache so imho it's perfectly fine. Also having it in the same listener is simple, doesn't change any behavior at all if you don't send that header, keeps things in place. I really like it where it is to be honest.
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.
my problem is that just depending on the tags header, this either uses the url to purge a specific page, or it uses the header to purge all tagged things, and ignores the url. so basically the two things have no code in common. it shows in the nested if/elseif/else. i would prefer a separate listener for that reason.
for the naming i agree that the term purge is correct. but as the semantics of the purge payload is completely different, i'd rather use a separate http method name.
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.
Okay, it's now in it's own PurgeTagsListener
and the docs are updated accordingly.
src/SymfonyCache/TaggableStore.php
Outdated
/** | ||
* @return TagAwareAdapter | ||
*/ | ||
public function getCache() |
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.
i would use reflection in the unit test rather than expose this. we don't want people to interact with the tag cache from outside.
src/SymfonyCache/TaggableStore.php
Outdated
try { | ||
$this->locks[$cacheKey]->release(); | ||
unset($this->locks[$cacheKey]); | ||
} catch (LockReleasingException $e) { |
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.
should we still unset if we get an exception here?
src/SymfonyCache/TaggableStore.php
Outdated
} | ||
|
||
/** | ||
* Cleanups storage. |
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.
i am a bit confused by the name of the method, and this non-telling phpdoc. but just noticed that its the same on the interface. lets do {@inheritDoc}
and then add a line Release all locks.
I agree. Which is also why I supported the original PR and actually did a PR to finish it. I think it's not the task of this implementation to dictate a developer what to do. But then again it's also pretty fine if it helps you avoiding stupid mistakes 😄 |
I guess all the comments are addressed. |
69233c1
to
be66e36
Compare
I noticed in Symfony 3.4, the filesystem type cache adapters received a |
Unit tests failing are not related to the lib. Seems like |
i will look at this on monday. |
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.
great work! i have a couple of comments on the documentation and some other small change requests. but overall, this looks good to me.
it adds quite some more to the library, i am thinking whether it would make sense to provide a separate repository for the symfony HttpCache integration instead, where we can properly model the dependencies.
doc/response-tagging.rst
Outdated
If you need a different behavior, you can provide your own implementation of | ||
the ``TagHeaderFormatter`` interface. But be aware that your | ||
:ref:`Varnish configuration <varnish_tagging>` has to match with the tag on the response. | ||
As explained already, if you need a different behavior, you can provide your own |
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.
lets not say that we repeat here - it feels unfriendly ;-)
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.
Done.
doc/response-tagging.rst
Outdated
the ``TagHeaderFormatter`` interface. But be aware that your | ||
:ref:`Varnish configuration <varnish_tagging>` has to match with the tag on the response. | ||
As explained already, if you need a different behavior, you can provide your own | ||
implementation of the ``TagHeaderFormatter`` interface. But again, be aware that your |
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.
lets not add "again".
maybe "provide your own TagHeaderFormatter
instance"? does not need to be a custom implementation of the interface, like in the example below. (i know, that is just reformatting, but while we touch it...)
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.
Done.
doc/symfony-cache-configuration.rst
Outdated
|
||
You need at least Symfony 3.4 to use this feature! | ||
|
||
|
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.
extra blank line, please remove
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.
Done.
|
||
Purge tags (cache invalidation using tags) | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
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.
please add a .. versionadded:: 2.1.0
that says that cache tagging for AppCache has been added in FOSHttpCache 2.1.0
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.
Done.
doc/symfony-cache-configuration.rst
Outdated
|
||
.. warning:: | ||
|
||
You need at least Symfony 3.4 to use this feature! |
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.
i think this is not strictly correct. you need symfony/lock and symfony/cache at 3.4, but afaik you can use symfony 3.3 together with those components, or am i wrong?
we should tell the composer require line for those two components, because they are optional dependencies.
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.
Right, done.
src/SymfonyCache/TaggableStore.php
Outdated
* | ||
* @param string $cacheDir The cache directory | ||
* @param int $pruneThreshold The number of write operations until orphan data gets pruned (default: 500) | ||
* @param string $purgeTagsHeader The tags header name |
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.
lets use an options resolver for the prune threshold and the header
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.
To be honest, I don't like the options resolver. It's way too verbose. Real arguments are self documenting and better to read.
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.
i disagree. the problem is that now if i want to change the header, i MUST specify some threshold. and if we add more options, the problem increases. imho optionsresolver is a very clean way to specify what is allowed, and includes validation like the threshold must be > 0 or the header must be a string without whitespace.
src/SymfonyCache/TaggableStore.php
Outdated
* | ||
* @return TaggableStore | ||
*/ | ||
public function setCache(TagAwareAdapterInterface $cache) |
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.
lets move this into the options. with the current implementation, the file system adapter is created and then replaced, which is just odd. i agree that we put a warning there - we should mention the "very good reasons" however, so that people can make an informed decision ;-)
src/SymfonyCache/TaggableStore.php
Outdated
* that the local adapters are used by default and you do not inject them in | ||
* the constructor but via setter injection instead. This is to protect you | ||
* as a developer! Only use this method if you're really sure your lock | ||
* implementation meets the needs of Symfony's HttpCache. |
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.
and same with the lock factory. mention that lock factory and cache storage must have the same scope e.g. on multi-server installations.
src/SymfonyCache/TaggableStore.php
Outdated
/** | ||
* @param string $cacheDir | ||
* | ||
* @return \Symfony\Component\Lock\StoreInterface |
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.
please use use
statements for phpdoc as well.
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.
I didn't feel like creating an alias for this one time usage. StoreInterface
is already taken.
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.
i would actually alias this to LockStoreInterface then. the point is that the use
statements better list what things this class interacts with.
* This only happens during write operations so cache retrieval is not | ||
* slowed down. | ||
*/ | ||
private function pruneExpiredEntries() |
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.
good idea! i think its ok to have this active by default. but it should be possible to disable this by setting the threshold to -1, in which case we would not even get the cache item with the counter.
we should think of some way to trigger cache pruning out of band, like in a symfony command or such. that would be the better implementation if people really care about performance.
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.
I chose 0
instead of -1
but it's now possible :-)
and i will look at the build error. aparently varnish changed where they provide their downloads :-( |
Yeah, their packages are now hosted at packagecloud.io. |
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.
Addressed your comments. Generally I agree to most of it, I don't want to add the options resolver though, that only makes things more complicated to understand in my opinion.
it adds quite some more to the library, i am thinking whether it would make sense to provide a separate repository for the symfony HttpCache integration instead, where we can properly model the dependencies.
I think that belongs to this library and I'm happy to help maintaining it :-)
doc/response-tagging.rst
Outdated
If you need a different behavior, you can provide your own implementation of | ||
the ``TagHeaderFormatter`` interface. But be aware that your | ||
:ref:`Varnish configuration <varnish_tagging>` has to match with the tag on the response. | ||
As explained already, if you need a different behavior, you can provide your own |
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.
Done.
doc/response-tagging.rst
Outdated
the ``TagHeaderFormatter`` interface. But be aware that your | ||
:ref:`Varnish configuration <varnish_tagging>` has to match with the tag on the response. | ||
As explained already, if you need a different behavior, you can provide your own | ||
implementation of the ``TagHeaderFormatter`` interface. But again, be aware that your |
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.
Done.
|
||
Purge tags (cache invalidation using tags) | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
|
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.
Done.
doc/symfony-cache-configuration.rst
Outdated
|
||
.. warning:: | ||
|
||
You need at least Symfony 3.4 to use this feature! |
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.
Right, done.
doc/symfony-cache-configuration.rst
Outdated
|
||
You need at least Symfony 3.4 to use this feature! | ||
|
||
|
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.
Done.
|
||
* **purge_tags_header**: HTTP Header that contains the comma-separated tags to purge. | ||
|
||
**default**: ``X-Cache-Tags`` |
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.
I copied it from within the same doc here (chapter Refresh
).
doc/symfony-cache-configuration.rst
Outdated
This means that after every `500` HTTP cache writes, your file system directory | ||
will be cleaned up and thus kept in good shape. | ||
You can configure the prune threshold by providing a different threshold as | ||
second argument to the constructor of `TaggableStore`. |
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.
Updated.
src/SymfonyCache/TaggableStore.php
Outdated
* | ||
* @param string $cacheDir The cache directory | ||
* @param int $pruneThreshold The number of write operations until orphan data gets pruned (default: 500) | ||
* @param string $purgeTagsHeader The tags header name |
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.
To be honest, I don't like the options resolver. It's way too verbose. Real arguments are self documenting and better to read.
src/SymfonyCache/TaggableStore.php
Outdated
/** | ||
* @param string $cacheDir | ||
* | ||
* @return \Symfony\Component\Lock\StoreInterface |
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.
I didn't feel like creating an alias for this one time usage. StoreInterface
is already taken.
* This only happens during write operations so cache retrieval is not | ||
* slowed down. | ||
*/ | ||
private function pruneExpiredEntries() |
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.
I chose 0
instead of -1
but it's now possible :-)
); | ||
} | ||
|
||
/** |
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.
can you add some text here? actually, i would like to change this method to getDefaultLockFactory and let it return a lock factory instead. with a phpdoc:
Build and return a default lock factory for when no explicit factory is specified.
The default factory uses the best quality lock store that is available on this system.
src/SymfonyCache/TaggableStore.php
Outdated
* Constructor. | ||
* | ||
* @param string $cacheDir The cache directory | ||
* @param int $pruneThreshold The number of write operations until orphan data gets pruned (default: 500) |
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.
the magic value 0 needs to be mentioned here
src/SymfonyCache/TaggableStore.php
Outdated
private $locks = []; | ||
|
||
/** | ||
* Constructor. |
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.
please remove this line, it does not add any information ;-)
35f4d93
to
454a6b1
Compare
@dbu addressed all your comments and switched to options resolver. Tests still failing due to wrong package source which I don't like to fix in that PR because it does not belong here :-) |
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.
Tests still failing due to wrong package source which I don't like to fix in that PR because it does not belong here :-)
i work on that in #375, agreed to not mix this into this PR.
i have some more comments on the documentation. don't want to make things complicated, but its tricky to make understandable documentation, i am afraid. i hope to figure out #375 and then you can rebase this on master and we should be ready.
can you please add an entry to CHANGELOG.md too?
doc/symfony-cache-configuration.rst
Outdated
|
||
.. versionadded:: 2.1 | ||
|
||
The `TaggableStore` has been added in version 2.1. |
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.
in rst, you need to use two backticks for code formatting. a single backtick does not work (it only works in markdown). can you check that for all of the doc you are adding please?
doc/symfony-cache-configuration.rst
Outdated
to use this feature! Add the following lines to your `composer.json` and run | ||
`composer update`:: | ||
|
||
"symfony/lock": "^3.4", |
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.
please indent this by another 4 spaces
doc/symfony-cache-configuration.rst
Outdated
Note that there are very good reasons that the local adapters are used by | ||
default. This is to protect you as a developer! Only override it if you're | ||
really sure your lock implementation meets the needs of Symfony's HttpCache. | ||
Type: Factory |
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.
i think we need a fully qualified name with namespace here
doc/symfony-cache-configuration.rst
Outdated
Note that there are very good reasons that the local adapters are used by | ||
default. This is to protect you as a developer! Only override it if you're | ||
really sure your cache implementation meets the needs of Symfony's HttpCache. | ||
Type: TagAwareAdapterInterface |
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.
lets use a FQN here
doc/symfony-cache-configuration.rst
Outdated
|
||
.. _taggablestore: | ||
|
||
The TaggableStore |
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.
i think this location makes the taggable store a sub section of user context, which is not correct. i would make it a title on the same level as those, using ~. and lets place it right after purge tags.
|
||
parent::__construct($kernel, $store, $surrogate, $options); | ||
|
||
$this->addSubscriber(new PurgeTagsListener()); |
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.
i would put the code example with the purge tags section, and mention the options of the PurgeTagsListener there as well (purge http method and such)
but keep the information on options for the taggable store here.
src/SymfonyCache/TaggableStore.php
Outdated
public function cleanup() | ||
{ | ||
try { | ||
/** @var LockInterface $lock */ |
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.
the docblock @var LockInterface[]
on the $locks property should be enough. at least for phpstorm it is...
quick question to coordinate the FOSHttpCacheBundle release: do we need additional configuration for the tag purge header in the symfony cache proxy configuration? if so, i will wait with tagging a new version of the bundle until we wrapped this up and i tagged a library release. |
ok, fixed the varnish install in master. can you please rebase? |
7e6ceda
to
56e2d74
Compare
Rebased. Updated docs. Added Changelog entry.
Yeah would be a great addition. But I don't feel like this is a must. The default works out of the box and if you want to adjust the header name (for whatever reason you would like to do that) then you have to submit a PR adding support for that in the bundle 😄 |
thanks a lot. this looks good now! i want to do some cleanups and will then merge this later this week and ideally tag a release. |
Cool! I think we're not in a hurry here as Symfony 3.4 is still 1.5 months away. |
@Toflar for those of us who can't review this code or follow this long discussion, could you please summarize as concisely as possible the advantages of this feature over the existing Symfony Http Cache and also, if there's any, the drawbacks or shortcomings compared to Symfony? Thanks! |
Hey @javiereguiluz, sure I can give it a try :) As concisely as possible: It supports tag based cache invalidation and auto-pruning of local cache entries and builds on top of the latest Symfony components that were not available by the time the And here are some more details, if you have time: First off: If you are not familiar with the
|
@Toflar fantastic explanation! Thanks!! |
@javiereguiluz toflar tried a pull request to make the default store of symfony use the cache + lock components, but that got refused. for the tag capabilities, we need something and i like this solution a lot more as its built on existing and tested components. but if you have any kind of feedback on it, i would be glad to hear it, now we can still easily change things. |
@dbu I don't care about tags because my HTTP Cache is super basic ... but "auto-pruning of the expired entries" is a super-killer feature to me. Otherwise, I must clean up the http_cache/ dir from time to time because nothing is deleted and it grows to GB of data every day :( We've received in the Symfony repository countless reports about this behavior. |
Thinking about it, it would maybe make sense to have the |
Ping @javiereguiluz: any comment on having a standalone store implementation? |
I can't make technical comments because I haven't reviewed this in detail ... but what I need (and lots of others, according to the community feedback) is a Symfony HttpCache store which doesn't grow in size without limits and which doesn't store expired entries forever. Ideally, your changes would be merged in Symfony itself so we can benefit from it without having to do or install anything. Thanks! |
at this point, i think a separate repository would be really cool! then the store should just be called Toflar/PsrCacheStore imho. the fact that it supports tags is free from the fact that it uses PSR-6. its important for FOSHttpCache but the main feature of the store is using PSR-6 caches. @javiereguiluz toflar first tried a PR against symfony core but that got refused by fabien with this comment. i can see the concern, but think its a bit over-cautious and seeing how TaggableStore has less code thanks to the lock and cache components i feel it would be progress. lets see if this will change sometimes. @Toflar i did a bit of refactoring on the store. i will push as soon as i got all tests fixed. if you agree, i suggest you then copy them from my branch into a repo, along with the documentation on the store. your composer.json can even properly specify that it requires symfony/cache and symfony/lock. and then i can adjust the FOSHttpCache doc to tell people to install toflar/symfony-cache-store (or whatever you want to call it) and reference the correct namespace. the tag listener for HttpCache would stay in this repository i think. would that make sense? |
continued in #376. |
Proposal for #286.
I actually did most of the work already a few months ago when trying to bring it to Symfony core so the
TaggableStore
was 80% ready and covered by all kinds of unit tests already 😄It has a lot of unit tests, especially for proper
Vary
handling which I implemented quite different from what the Symfony defaultStore
does. I did not use it in a project yet, so I have no idea if everything works as expected, I'm just pretty sure because of the unit tests. So it would be great if someone that actually has a project where he/she could use cache invalidation with tags could test this integration and see if the docs are clear 😄I'll add a few comments/annotations to my own code in a few minutes.