Problem/Motivation
While working on #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, I encountered fatal PHP errors when using Vocabulary config entities.
When writing tests that involves Vocabulary config entities, this is what you get:
1) Drupal\Tests\rest\Functional\EntityResource\Vocabulary\VocabularyJsonAnonTest::testGet
Exception: Notice: Use of undefined constant TAXONOMY_HIERARCHY_DISABLED - assumed 'TAXONOMY_HIERARCHY_DISABLED'
Drupal\Core\Cache\DatabaseBackend->prepareItem()() (Line: 138)
/Users/wim.leers/Work/drupal-tres/core/lib/Drupal/Core/Test/HttpClientMiddleware/TestHttpClientMiddleware.php:44
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/promises/src/FulfilledPromise.php:39
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/promises/src/TaskQueue.php:61
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/promises/src/Promise.php:246
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/promises/src/Promise.php:223
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/promises/src/Promise.php:266
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/promises/src/Promise.php:225
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/promises/src/Promise.php:62
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/guzzle/src/Client.php:129
/Users/wim.leers/Work/drupal-tres/vendor/guzzlehttp/guzzle/src/Client.php:87
/Users/wim.leers/Work/drupal-tres/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php:127
/Users/wim.leers/Work/drupal-tres/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php:127
If you then define it, like so:
namespace Drupal\taxonomy\Entity;
if (!defined('TAXONOMY_HIERARCHY_DISABLED')) {
define('TAXONOMY_HIERARCHY_DISABLED', 0);
}
Then it still doesn't work:
1) Drupal\Tests\rest\Functional\EntityResource\Vocabulary\VocabularyJsonAnonTest::testGet
Constant TAXONOMY_HIERARCHY_DISABLED already defined
/Users/wim.leers/Work/drupal-tres/core/modules/taxonomy/taxonomy.module:23
/Users/wim.leers/Work/drupal-tres/core/lib/Drupal/Core/Extension/Extension.php:140
/Users/wim.leers/Work/drupal-tres/core/lib/Drupal/Core/Extension/ModuleHandler.php:127
/Users/wim.leers/Work/drupal-tres/core/lib/Drupal/Core/Extension/ModuleInstaller.php:182
/Users/wim.leers/Work/drupal-tres/core/lib/Drupal/Core/ProxyClass/Extension/ModuleInstaller.php:83
/Users/wim.leers/Work/drupal-tres/core/tests/Drupal/Tests/BrowserTestBase.php:1138
/Users/wim.leers/Work/drupal-tres/core/tests/Drupal/Tests/BrowserTestBase.php:470
/Users/wim.leers/Work/drupal-tres/core/modules/rest/tests/src/Functional/ResourceTestBase.php:94
/Users/wim.leers/Work/drupal-tres/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php:51
The root cause: #1369200: Define constants for taxonomy hierarchy converted hardcoded numbers (0/1/2) all over the place to constants (yay!) but put them in taxonomy.module rather than on VocabularyInterface (sadpanda). This means that for one to use \Drupal\taxonomy\Entity\Vocabulary, taxonomy.module must also be loaded.
Proposed resolution
Move the constants from taxonomy.module to VocabularyInterface.
Remaining tasks
TBD
User interface changes
None.
API changes
The constants move.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #41 | 2807263-14.patch | 18.67 KB | alexpott |
| #24 | 2807263-24.patch | 18.92 KB | wim leers |
Comments
Comment #2
wim leersComment #3
klausiwe cannot change the constant in drupal 8 for compatibility reasons. You need to require_once taxonomy.module in the setUp() method of your unit test instead.
Should we close this as won't fix or just move it to Drupal 9?
Comment #4
wim leersThat is what I'm doing.
And @tim.plunkett suggested I do
just like in http://cgit.drupalcode.org/drupal/diff/core/modules/block/tests/src/Kern....
But that also doesn't work; it results in the same errors as above.
I'm starting to suspect somehow the test is resulting in a separate PHP environment. I don't know the internals of
BrowserTestBasewell enough to know that for sure though, but that's the only explanation I can think of atm.Continuing to debug this…
Comment #5
wim leersI did lots more digging.
This was hinting in the direction of PHPUnit:
So I looked at
TestHttpClientMiddleware.php:44. It's doing this:This is where that output is coming from (
Exception: Notice: Use of undefined constant TAXONOMY_HIERARCHY_DISABLED - assumed 'TAXONOMY_HIERARCHY_DISABLED' […])!So this is dealing with the case of there being a fatal error, or a notice — or in other words: PHP engine output.
By putting different code there, to return early, reveals that it's PHPUnit's
Php.php, line 107, that is sending this back in the first place. This lead me to the following PHPUnit issues, which were simply closed without being solved:The latter was even commented on in great detail by our very own sun!
Both of these PHPUnit issues indicate that the problem is related to global state being preserved or not.
BrowserTestBasedoes this:This also explains why the recommendations by klausi & tim.plunkett don't have the desired effect.
Still no solution yet, continuing my debugging…
Comment #6
wim leersI've found the root cause. You won't believe this. This is both hilarious and sad. But it's definitely epic.
Stay tuned for the full explanation.
Comment #7
wim leersDebug process (a successful one, after many attempts)
I personally found it very very tricky to debug this PHP notice at
Drupal\Core\Cache\DatabaseBackend->prepareItem()() (Line: 138), because it was happening inside a request made by Guzzle, which made it impossible for me to step through it with a debugger. Worse, it was also very difficult to be able to add meaningful debug output, because it was happening in a place that got many dozens of function calls, and I couldn't figure out which call was the problem.In the end, I settled on this:
This allowed me to see what exactly was happening, with useful debug output:
Analysis
So, unserialization of a
Vocabularyentity is occurring and failing not inside the test, but as part of a Page Cache hit!ResourceResponseapparently contains a serializedVocabularyentity, and it cannot be unserialized, so BAM, everything explodes.The root cause is simple: the
PageCachemiddleware runs before theKernelPreHandlemiddleware. The latter is what loads modules. So:taxonomy.modulehas not yet been loaded by the time that we try to unserialize a serialized, cachedVocabularyentity that uses a constant defined intaxonomy.module.Solution: still move
TAXONOMY_HIERARCHY_*constants toVocabularyInterface, but keep the existing constants, and keep them in sync. This ensures BC is not broken for code that is doing things likeif ($vocabulary->getHierarchy() === TAXONOMY_HIERARCHY_*. But then all other code must useVocabularyInterface::HIERARCHY_*.To convince yourself this works, try this:
See https://3v4l.org/K3LRM to see the results of that on all PHP versions. It works.
Performance
Coincidentally, this revealed that
ResourceResponsestill contains the data it must serialize! And so, whenever (Dynamic) Page Cache retrieves its cache items, the cache system unserializes the response, and so it must also unserializeResourceResponse::$responseData.Fixing this leads to a 60% performance boost (for anonymous REST responses). See #2807501: ResourceResponse::$responseData is serialized, but is never used: unserializing it on every Page Cache hit is a huge performance penalty.
Patch
Attached patch includes test coverage that shows the problem.
Comment #9
wim leersAnd here's the fix.
catch independently proposed the same solution yesterday in IRC:
Moving to 8.2.x, because this is breaking REST and blocking #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, which is necessary for REST to become truly stable/reliable in 8.2.x.
Comment #10
wim leersComment #11
wim leersComment #12
klausiLooks good, just some minor notpicks:
should be {@inheritdoc}
this makes the test case very hard to read. Keep it simple: it is perfectly fine to hard-code the path in a test case in drupalGet().
use ->responseHeaderEquals() instead.
->responseHeaderEquals()
And then we need the change record draft. After that I would RTBC it :)
We should probably open a follow-up to deprecate all constants in all modules. Otherwise we might run into this problem again.
Comment #13
wim leersThanks! Follow-up issue created: #2807785: Move global constants from *.module files into interfaces.
Comment #14
wim leersAddressed everything in #14. Thanks for
responseHeaderEquals(), I didn't know that one!Comment #15
wim leersCR created: https://www.drupal.org/node/2807795.
Comment #16
klausiCR looks good and contains instructions for contrib maintainers. I verified manually that the old constants are not used anymore.
I think we can ignore the missing doc blocks on the classes/methods of the test module, nothing useful to say there I guess?
5 stars, would RTBC again.
Comment #17
klausiComment #18
dawehnerTo be honest we should really start with a more generic issue, I'm working on one
Comment #19
dawehner#2807961: Replace usages of deprecated global constants with the new interface constants
Comment #20
dawehnerShould we just add VocabularyInterface::HIERARCHY_DISABLED as concrete reference to show people what a proper transition path looks like?
Comment #21
alexpottLet's make these equal the new class constants.
Comment #22
wim leers#20: Oh, I like that!
#21: that's the same as what #20 says, right?
Comment #23
dawehnerYeah exactly.
Comment #24
wim leersMuch better. Thanks! :)
Comment #26
klausiRandom test fail, queuing again.
Looks good otherwise!
Comment #28
alexpottWe need to do this the other way around. :( the module file is loaded before the classloader is updated.
Comment #29
klausiSo we cannot use the class in the global scope because the class loader for the module is not there when the module is installed/uninstalled.
Back to hard code the numbers then?
Comment #30
klausi@alexpott: we cannot do it the other way around because then the module would have to be loaded all the time, which it is not :)
I think it is fine to duplicate the constant values and not care too much about it.
Comment #31
alexpottSo let's go back to #14 because then we don't have to change anything when we drop support for the old constants.
Comment #32
alexpottCommitted 8a97a26 and pushed to 8.3.x. Thanks!
Fixed on commit.
Comment #34
klausiThis should also be committed to 8.2.x, right?
Comment #35
klausialexpott said this cannot happen in 8.2.x for now, maybe after 8.2.0. Marking as fixed for now, will also publish the change notice for 8.3.x.
Comment #36
wim leersThis must be ported to 8.2.x. It's a hard blocker for #2737719: EntityResource: Provide comprehensive test coverage: for every entity type, every format, every method, which we need to make REST as stable/reliable as possible in 8.2.x. Alex and catch have been marking those issues that still need to be committed to 8.2.x after 8.2.0 as "patch to be ported" and assigned to 8.2.x. So doing the same here.
Please don't yet publish the CR, until we know in which version of 8.2.x this lands, so that we don't need to retroactively update it after publishing.
Comment #37
alexpottThis is the sum disruption of the issue. The old constants are still in place - just deprecated. I think doing this in 8.2.1 is acceptable given what it blocks.
Comment #38
catchYes I also think it's reasonable to backport the new class constants.
Comment #39
wim leersYep, they're just deprecated, but they'll continue to work just fine.
Comment #41
alexpottDoh - we committed #14... that's why the last patch should always be the committed one :( Re-uploading to make this so.
Comment #42
alexpottMaking the actual status clear
Comment #43
alexpottCommitted fc4b8d3 and pushed to 8.2.x. Thanks!
Comment #45
wim leersPublished https://www.drupal.org/node/2807795.
Comment #47
xjmComment #48
wim leersSimilar problem is now being seen elsewhere: #2825801: Node type objects (and other objects that depend on *.module files) cannot be part of response objects due to Page Cache: Document or fix!.