Skip to content

[FrameworkBundle] Dont store cache misses on warmup #40645

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
Apr 1, 2021

Conversation

Nyholm
Copy link
Member

@Nyholm Nyholm commented Mar 30, 2021

Q A
Branch? 4.4
Bug fix? yes
New feature? no
Deprecations? no
Tickets Fix #38694
License MIT
Doc PR symfony/symfony-docs#15172

When we are warming the annotation cache, we are reading all annotation into an ArrayAdapter. When we are done we move the values to a PhpArrayAdapter to store them in a file.

@Seldaek found out that when you are using a custom constraint with a Symfony\Component\Validator\Constraints\Callback, there is a strange error like:

Can use "yield from" only with arrays and Traversables

That is because the Closure in the Symfony\Component\Validator\Constraints\Callback cannot be serialised and saved to cache. But since the ArrayAdapter is also storing misses as null, the null values are understood as real values.

When all values are moved to the PhpArrayAdapter and we ask the cache for a value (via Doctrine's CacheProvider), it will return null as a value instead of false as a cache miss. And null is not something one could "yield from".

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

@carsonbot could you find me a reviewer please?

@carsonbot
Copy link

I'm sorry. I could not find any suitable reviewer.

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

Oh. I guess it was too long ago @dmaicher wrote this class =)

David, do you mind having a look at this PR?

@Seldaek
Copy link
Member

Seldaek commented Mar 31, 2021

Thanks for the quick fix :)

If I understand correctly though, this means using lambdas like that will prevent caching of the resolved config and it'll reload on every request needing it. I guess it's thus worth avoiding lambda callbacks..

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

Yes, that is correct.

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

I updated the docs with this observation: symfony/symfony-docs#15172

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Mar 31, 2021

Storing misses is an important part of the performance provided by system caches.
We should look for another solution if possible.

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

Yes, but these "misses" are interpreted as "hits". A "hit" with null is not the same as a miss. There should never be "null" stored in this cache.

I have two alternative solutions. The first would be to find a way to serialize a \Closure. The second one is to throw an exception on cache warmup when you fail to save a value in cache.

@nicolas-grekas
Copy link
Member

nicolas-grekas commented Mar 31, 2021

The test case passes even without the change if I didn't mess up :)

Yes, but these "misses" are interpreted as "hits". A "hit" with null is not the same as a miss. There should never be "null" stored in this cache.

For system caches, aka the ones sourced by the code itself, we can do this - cache misses.
We built this logic because it saves CPU cycles, eg you don't want to read the annotations again on methods that don't have any annotation.

I have two alternative solutions. The first would be to find a way to serialize a \Closure. The second one is to throw an exception on cache warmup when you fail to save a value in cache.

Why a closure ends up in the cache? That's the main question I have here indeed. An annotation should not lead to a closure because then of course it cannot be cached - while the source is a plain descriptive annotation.

The second solution would be a major regression actually.

Can you try this patch instead? It tries to not store misses for failures-to-save().

--- a/src/Symfony/Component/Cache/Traits/ArrayTrait.php
+++ b/src/Symfony/Component/Cache/Traits/ArrayTrait.php
@@ -145,6 +145,7 @@ trait ArrayTrait
             try {
                 $serialized = serialize($value);
             } catch (\Exception $e) {
+                unset($this->values[$key]);
                 $type = \is_object($value) ? \get_class($value) : \gettype($value);
                 $message = sprintf('Failed to save key "{key}" of type %s: ', $type).$e->getMessage();
                 CacheItem::log($this->logger, $message, ['key' => $key, 'exception' => $e]);

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

The test case passes even without the change if I didn't mess up :)

You did mess up =)

❯ ./phpunit src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer 
#!/usr/bin/env php
PHPUnit 9.5.3 by Sebastian Bergmann and contributors.

Testing /src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer
....F..............                                               19 / 19 (100%)

Time: 00:00.091, Memory: 16.00 MB

There was 1 failure:

1) Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer\AnnotationsCacheWarmerTest::testWarmupRemoveCacheMisses
Failed asserting that actual size 3 matches expected size 2.

/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php:149

Why a closure ends up in the cache?

Because we cache an object of Symfony\Component\Validator\Constraint\Callback. That object is given a closure by the user in its constructor.

Can you try this patch instead? It tries to not store misses for failures-to-save().

I think that would work. But the end result would be the same. We would reread the annotations at runtime.
Note that we still cache the empty array. Which means "we have read the annotations, and there weren't any".

@nicolas-grekas
Copy link
Member

You did mess up =)

I confirm :P

Because we cache an object of Symfony\Component\Validator\Constraint\Callback. That object is given a closure by the user in its constructor.

I just read the doc PR, nice one.

I think that would work. But the end result would be the same. We would reread the annotations at runtime.

True, but we would still store misses, and this is important for the other system caches.

Note that we still cache the empty array. Which means "we have read the annotations, and there weren't any".

OK great, then I suppose other system caches need this. Annotations are not the only system caches, but the current patch changes the behavior for all of them.

javiereguiluz added a commit to symfony/symfony-docs that referenced this pull request Mar 31, 2021
… (Nyholm)

This PR was squashed before being merged into the 4.4 branch.

Discussion
----------

[Validator] Add warning about closure not being cachable

Follow up from symfony/symfony#40645

The annotation cache will not work with `Closure`.

If I understand correctly, it is not possible to have external dependencies in your custom validator AND get your annotation cached. You have to chose one or the other.

Commits
-------

ebddb78 [Validator] Add warning about closure not being cachable
Comment on lines 82 to 85
$values = array_filter($values, function ($val) {
return $val !== null;
});

// make sure we don't cache null values
parent::warmUpPhpArrayAdapter($phpArrayAdapter, array_filter($values));
parent::warmUpPhpArrayAdapter($phpArrayAdapter, $values);
Copy link
Member Author

Choose a reason for hiding this comment

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

array_filter($values) will also remove everything that is empty.

https://3v4l.org/0m3In

There is no test for this code and I am not sure why null should be removed. I can only assume it is because of the callback =)

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

I was wrong patching AbstractPhpFileCacheWarmer. What I really wanted to do is to only modify the behaviour of the AnnotationCacheWarmer.

Can you try this patch instead? It tries to not store misses for failures-to-save().

This is not reliable. If a user does something like this, the bug will occur again.

$i = $cache->getItem('foo');
$i->set('data');
$cache->save($i); // Save failed for some reason.

// ...
$cache->getItem('foo');

@Nyholm
Copy link
Member Author

Nyholm commented Mar 31, 2021

The PR is updated

Nyholm added a commit that referenced this pull request Mar 31, 2021
…pter (nicolas-grekas)

This PR was merged into the 4.4 branch.

Discussion
----------

[Cache] skip storing failure-to-save as misses in ArrayAdapter

| Q             | A
| ------------- | ---
| Branch?       | 4.4
| Bug fix?      | yes
| New feature?  | no
| Deprecations? | no
| Tickets       | Fix #38694
| License       | MIT
| Doc PR        | -

In addition to #40645

Commits
-------

ba66987 [Cache] skip storing failure-to-save as misses in ArrayAdapter
@dmaicher
Copy link
Contributor

dmaicher commented Apr 1, 2021

Oh. I guess it was too long ago @dmaicher wrote this class =)

David, do you mind having a look at this PR?

I think my work on that was related to #23558

But now I see you changed the PR anyway 😊

@nicolas-grekas nicolas-grekas force-pushed the 4.4-annotation-cache-warmer branch from d249500 to 27a22b3 Compare April 1, 2021 10:17
@nicolas-grekas
Copy link
Member

Thank you @Nyholm.

@nicolas-grekas nicolas-grekas merged commit cc7d126 into symfony:4.4 Apr 1, 2021
@Nyholm Nyholm deleted the 4.4-annotation-cache-warmer branch April 6, 2021 09:47
This was referenced May 1, 2021
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.

6 participants