Skip to content

[FrameworkBundle] fix ValidatorCacheWarmer: use serializing ArrayAdapter #23558

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

Closed
wants to merge 7 commits into from

Conversation

dmaicher
Copy link
Contributor

@dmaicher dmaicher commented Jul 17, 2017

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

The ValidatorCacheWarmer was using an ArrayAdapter with $storeSerialized=false. This is a problem as inside the LazyLoadingMetadataFactory the metaData objects are mutated (parent class constraints are merged into it) after they have been written into the cache.

So this means that currently when warming up the validator cache actually the merged metaData version is finally taken from the ArrayAdapter and written into the PhpFilesAdapter.

Which then caused some duplicate constraints as the parent constraints are merged again after fetching from the cache inside LazyLoadingMetadataFactory.

This fix makes sure we serialize objects into the ArrayAdapter.

Writing a test case for this does not seem easy to me. Any ideas?

EDIT: Maybe its even safer to just clone the object when writing it into the cache?

diff --git a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php
index 79ad1f2..88eaf33 100644
--- a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php
+++ b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php
@@ -117,7 +117,7 @@ class LazyLoadingMetadataFactory implements MetadataFactoryInterface
         }
 
         if (null !== $this->cache) {
-            $this->cache->write($metadata);
+            $this->cache->write(clone $metadata);
         }

Opinions?

@dmaicher dmaicher force-pushed the validator-cache-warmer-fix branch from ddd9afd to 9b84d72 Compare July 17, 2017 20:35
@nicolas-grekas nicolas-grekas added this to the 3.2 milestone Jul 17, 2017
@nicolas-grekas
Copy link
Member

nicolas-grekas commented Jul 18, 2017

Good catch!
I think we should do the same in AnnotationsCacheWarmer and in SerializerCacheWarmer.
There is also one thing to fix: the array_filter should happen before the array_map (misses are stored as null, which can't "unserialized" and shouldn't be cached.)
Dunno about patching LazyLoadingMetadataFactory, but anyway, I'm sure this one is needed :)

@@ -62,7 +62,7 @@ public function warmUp($cacheDir)
}

$adapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool);
$arrayPool = new ArrayAdapter(0, false);
$arrayPool = new ArrayAdapter(0);
Copy link
Member

@nicolas-grekas nicolas-grekas Jul 18, 2017

Choose a reason for hiding this comment

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

0 is the default so it can be removed also

@dmaicher dmaicher force-pushed the validator-cache-warmer-fix branch 3 times, most recently from 7eb5bf8 to d8e5fa7 Compare July 18, 2017 17:50
@dmaicher dmaicher force-pushed the validator-cache-warmer-fix branch from d8e5fa7 to 8753b06 Compare July 18, 2017 17:51
$this->assertArrayHasKey('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Category', $values);
$this->assertArrayHasKey('Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.SubCategory', $values);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nicolas-grekas due to moving the array_filter I needed to fix this test. It seems before we stored empty data in the cache?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Before the change the array contained this:

array:2 [
  "Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.Category" => "O:49:"Symfony\Component\Validator\Mapping\ClassMetadata":10:{s:11:"constraints";a:0:{}s:18:"constraintsByGroup";a:0:{}s:17:"traversalStrategy";i:1;s:7:"getters";a:0:{}s:13:"groupSequence";a:0:{}s:21:"groupSequenceProvider";b:0;s:7:"members";a:2:{s:2:"id";a:1:{i:0;O:52:"Symfony\Component\Validator\Mapping\PropertyMetadata":7:{s:11:"constraints";a:1:{i:0;O:44:"Symfony\Component\Validator\Constraints\Type":4:{s:7:"message";s:40:"This value should be of type {{ type }}.";s:4:"type";s:3:"int";s:7:"payload";N;s:6:"groups";a:2:{i:0;s:7:"Default";i:1;s:8:"Category";}}}s:18:"constraintsByGroup";a:2:{s:7:"Default";a:1:{i:0;r:12;}s:8:"Category";a:1:{i:0;r:12;}}s:17:"cascadingStrategy";i:1;s:17:"traversalStrategy";i:2;s:5:"class";s:65:"Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Category";s:4:"name";s:2:"id";s:8:"property";s:2:"id";}}s:4:"name";a:1:{i:0;O:52:"Symfony\Component\Validator\Mapping\PropertyMetadata":7:{s:11:"constraints";a:1:{i:0;O:44:"Symfony\Component\Validator\Constraints\Type":4:{s:7:"message";s:40:"This value should be of type {{ type }}.";s:4:"type";s:6:"string";s:7:"payload";N;s:6:"groups";a:2:{i:0;s:7:"Default";i:1;s:8:"Category";}}}s:18:"constraintsByGroup";a:2:{s:7:"Default";a:1:{i:0;r:32;}s:8:"Category";a:1:{i:0;r:32;}}s:17:"cascadingStrategy";i:1;s:17:"traversalStrategy";i:2;s:5:"class";s:65:"Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Category";s:4:"name";s:4:"name";s:8:"property";s:4:"name";}}}s:4:"name";s:65:"Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Validation\Category";s:10:"properties";a:2:{s:2:"id";r:10;s:4:"name";r:30;}s:12:"defaultGroup";s:8:"Category";}"
  "Symfony.Bundle.FrameworkBundle.Tests.Fixtures.Validation.SubCategory" => "b:0;"
]

Copy link
Member

Choose a reason for hiding this comment

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

the behavior should not change at all so this test must not be changed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The change you requested makes this test fail though:

There is also one thing to fix: the array_filter should happen before the array_map (misses are stored as null, which can't "unserialized" and shouldn't be cached.)

// the ArrayAdapter stores the values serialized
// to avoid mutation of the data after it was written to the cache
// so here we un-serialize the values first
$values = array_map(function ($val) { return unserialize($val); }, array_filter($arrayAdapter->getValues()));
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@nicolas-grekas is it fine to apply array_filter here as well for the other cache warmers? Tests are fine although the AnnotationsCacheWarmer does not have any tests 😢

Copy link
Member

Choose a reason for hiding this comment

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

$values = array_map(function ($val) { return null !== $val ? unserialize($val) : null; }, $arrayAdapter->getValues());
and array_filter should be called only for ValidatorCacheWarmer, and only when feeding phpArrayAdapter.
the foreach then should iterate over all values, null or not

}

/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
public function isOptional()
Copy link
Member

Choose a reason for hiding this comment

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

this could also be moved to the parent I guess

use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

abstract class AbstractPhpFileCacheWarmer implements CacheWarmerInterface
Copy link
Member

Choose a reason for hiding this comment

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

should be marked @internal

@dmaicher dmaicher force-pushed the validator-cache-warmer-fix branch from ff47aa1 to 71b41f7 Compare July 18, 2017 20:50
@dmaicher
Copy link
Contributor Author

@nicolas-grekas changes done 👍

use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;

/**
* @internal This class is meant for internal use only.
Copy link
Member

Choose a reason for hiding this comment

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

usually we don't add the additional comment so we should remove it I guess

abstract class AbstractPhpFileCacheWarmer implements CacheWarmerInterface
{
protected $phpArrayFile;
protected $fallbackPool;
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't these both be private?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Indeed 👍


return;
return false;
Copy link
Member

Choose a reason for hiding this comment

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

this makes me think that doWarmUp doesn't need the $phpArrayAdapter argument: it's used only here, but returning !== false here would ship the same behavior without the need for this arg, isn't it?
which then triggers: shouldn't we have a clean "bool" return type for all dowarmup methods? ie return true on success? (and here also then)


spl_autoload_register(array($phpArrayAdapter, 'throwOnRequiredClass'));
try {
$this->doWarmUp($cacheDir, $arrayAdapter);
Copy link
Member

Choose a reason for hiding this comment

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

you still need the if (!doWarmUp) then return; check

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

@nicolas-grekas nicolas-grekas Jul 18, 2017

Choose a reason for hiding this comment

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

it will if we make dowarmup return true or false to allow/prevent the warmup

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah now I see what you meant 😄 👍 Done.

@dmaicher dmaicher force-pushed the validator-cache-warmer-fix branch from add8c87 to 8ab2ad6 Compare July 18, 2017 21:09
$phpArrayAdapter = new PhpArrayAdapter($this->phpArrayFile, $this->fallbackPool);
$arrayAdapter = new ArrayAdapter();

spl_autoload_register(array($phpArrayAdapter, 'throwOnRequiredClass'));
Copy link
Member

Choose a reason for hiding this comment

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

since we don't need the instance yet, let's use PhpArrayAdapter::class here instead, and create the object later, (or never when dowarmup returns false)

$annotatedClassPatterns = $cacheDir.'/annotations.map';

if (!is_file($annotatedClassPatterns)) {
$adapter->warmUp(array());

return;
Copy link
Member

Choose a reason for hiding this comment

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

return true

}

/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
{
if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) {
return;
Copy link
Member

Choose a reason for hiding this comment

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

return false

}

/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter)
{
if (!method_exists($this->validatorBuilder, 'getLoaders')) {
return;
Copy link
Member

Choose a reason for hiding this comment

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

return false

Copy link
Member

@nicolas-grekas nicolas-grekas left a comment

Choose a reason for hiding this comment

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

LGTM, thanks for your reactivity tonight :)

@fabpot
Copy link
Member

fabpot commented Jul 19, 2017

Thank you @dmaicher.

fabpot added a commit that referenced this pull request Jul 19, 2017
…g ArrayAdapter (dmaicher)

This PR was squashed before being merged into the 3.2 branch (closes #23558).

Discussion
----------

[FrameworkBundle] fix ValidatorCacheWarmer: use serializing ArrayAdapter

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

The `ValidatorCacheWarmer` was using an `ArrayAdapter` with `$storeSerialized=false`. This is a problem as inside the `LazyLoadingMetadataFactory` the metaData objects are mutated (parent class constraints are merged into it) after they have been written into the cache.

So this means that currently when warming up the validator cache actually the merged metaData version is finally taken from the `ArrayAdapter` and written into the `PhpFilesAdapter`.

Which then caused some duplicate constraints as the parent constraints are merged again after fetching from the cache inside `LazyLoadingMetadataFactory`.

This fix makes sure we serialize objects into the `ArrayAdapter`.

Writing a test case for this does not seem easy to me. Any ideas?

EDIT: Maybe its even safer to just clone the object when writing it into the cache?

```diff
diff --git a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php
index 79ad1f2..88eaf33 100644
--- a/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php
+++ b/src/Symfony/Component/Validator/Mapping/Factory/LazyLoadingMetadataFactory.php
@@ -117,7 +117,7 @@ class LazyLoadingMetadataFactory implements MetadataFactoryInterface
         }

         if (null !== $this->cache) {
-            $this->cache->write($metadata);
+            $this->cache->write(clone $metadata);
         }
```

Opinions?

Commits
-------

c0556cb [FrameworkBundle] fix ValidatorCacheWarmer: use serializing ArrayAdapter
@fabpot fabpot closed this Jul 19, 2017
This was referenced Aug 1, 2017
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.

4 participants