#2202185: Statically cache Role entities to speed up (User|UserSession)::hasPermission() wants to enable entity static caching for user roles. #1885830: Enable static caching for config entities wants to do it by default for all config entities. Both issues have run into the problem that the cache is broken as per this issue's title, since it is not yet used by any config entities in core.

Regardless of which, if any, core config entities we want to statically cache, it needs to be possible for contrib to enable static caching for their own entities, or turn it on for core ones.

Therefore, this issue is to add a test that is independent of core usages and fix the found bugs.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

effulgentsia’s picture

Status: Active » Needs review
FileSize
5.92 KB

Here's the test to demonstrate the failures.

effulgentsia’s picture

And here's the fix.

Berdir’s picture

+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -255,6 +266,46 @@ protected function has($id, EntityInterface $entity) {
+            $entities[$id] = $this->entities[$id][$key];

This does change the structure of $this->entities, which was recently improved to document what it contains. So it would be different for this. Not sure how to properly document that.

The last submitted patch, 1: config_entity-static_cache-tests-only.patch, failed testing.

effulgentsia’s picture

Re #3: #2 contains:

+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -82,6 +82,17 @@ class ConfigEntityStorage extends EntityStorageBase implements ConfigEntityStora
+   * Static cache of entities, keyed first by entity ID, then by an extra key.
+   *
+   * The second layer cache key is to maintain separate caches for different
+   * states of config overrides.
+   *
+   * @var array
+   * @see \Drupal\Core\Config\ConfigFactoryInterface::getCacheKey().
+   */
+  protected $entities = array();

Do you think additional docs are needed somewhere?

Berdir’s picture

Ouch, no idea why I missed that, yes that looks great. You're just too good at this, only a bit thinking out loud below, no real feedback :)

Would also appreciate your thoughts in #1885830: Enable static caching for config entities in regards to enabling it by default vs. only doing it for config entities where we know it's worth it, like roles.

  1. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    @@ -742,12 +738,9 @@ protected function doSave($id, EntityInterface $entity) {
         $this->invokeFieldMethod($is_new ? 'insert' : 'update', $entity);
         $this->saveFieldItems($entity, !$is_new);
    -    $this->resetCache($cache_ids);
     
    

    Apparently it's not a problem that we're moving when exactly the caches are cleared, and there's almost nothing after that, so that seems fine, even logical to do it after the translation hooks were called too.

    That said, there is #1729812: Separate storage operations from reactions, not exactly sure what this would mean. I guess it would still be fine as the hooks already happen in the parent and the current implementation needs to override save() to be able to have them inside the transaction.

  2. +++ b/core/modules/config/src/Tests/ConfigEntityStaticCacheTest.php
    @@ -0,0 +1,96 @@
    +   */
    +  public function testCacheHit() {
    +    $entity_1 = entity_load($this->entityTypeId, $this->entityId);
    +    $entity_2 = entity_load($this->entityTypeId, $this->entityId);
    +    // config_entity_static_cache_test_config_test_load() sets _loadToken to a
    +    // random string. If they match, it means $entity_2 was retrieved from the
    +    // static cache rather than going through a separate load sequence.
    +    $this->assertIdentical($entity_1->_loadToken, $entity_2->_loadToken);
    +  }
    

    It worked pretty well to use unit tests for cache hit/miss testing in #597236: Add entity caching to core because you can just mock the inner load method and easily define if and how often it should be called. Maybe that would be an easier way to test it?

    Downside is that unit tests are always a bit constructed, and it's kind of an integration test here...

    That said, very nice work on the tests.

  3. +++ b/core/modules/config/tests/config_entity_static_cache_test/config_entity_static_cache_test.module
    @@ -0,0 +1,31 @@
    +  static $random;
    +  if (!$random) {
    +    $random = new Random();
    +  }
    

    Was wondering if this is really necessary for tests. But it is, because only if we do it like this, we ensure that the random strings are unique.

effulgentsia’s picture

Re #6.0: #1885830-112: Enable static caching for config entities

Re #6.1: #1729812: Separate storage operations from reactions doesn't have a patch yet, so hard to predict what it will mean in relation to which if any hooks should fire before cache clearing and which if any should happen after. As you say, I think what's implemented in this patch, in terms of moving cache clearing to after translation hooks makes sense, and then we can deal with the rest of the question in that issue.

Re #6.2: I prefer it as-is here, since the other tests are also integration tests. But good to know we have a pattern for doing a unit test version if we decide at some point we want that. (and by the way, zOMG, and THANK YOU!!! for driving #597236: Add entity caching to core through to completion!!!!)

Anything else?

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

Ok, looks good to me, let's get this in, so we can get back the other issue.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
@@ -255,6 +266,46 @@ protected function has($id, EntityInterface $entity) {
+          $key = $this->configFactory->getCacheKey($this->getConfigPrefix() . $id);
...
+        $key = $this->configFactory->getCacheKey($this->getConfigPrefix() . $id);

I wonder if it is worth delegating cache key generation to a protected method and moving outside the foreach since there is no need for each ID to be passed to the ConfigFactory::getCacheKey() - we could pass an empty string for all we care.

I also think it might be worth adding a test for disabling overrides missing the cache and then enabling overrides and getting a cache hit and then disabling again and getting a cache hit.

alexpott’s picture

Also needs a reroll for #2306455: ConfigEntityStorage adds too many dots since ConfigEntityStorage::getConfigPrefix has become protected and now called getPrefix()

alexpott’s picture

Status: Needs work » Needs review
FileSize
2.94 KB
11.67 KB

Something like this.

effulgentsia’s picture

there is no need for each ID to be passed to the ConfigFactory::getCacheKey() - we could pass an empty string for all we care

While that happens to work for ConfigFactory's implementation, doesn't that violate the contract of ConfigFactoryInterface, which documents the method as needing a config object name? Even ConfigFactory itself doesn't optimize in that way in its own loadMultiple(). Should we change the API of ConfigFactoryInterface::getCacheKey() to not take a parameter at all and return a key suffix rather than a full key?

alexpott’s picture

Yeah I think that is a good option. Plus we could look at what actually needs to be on the public ConfigFactory interface.

effulgentsia’s picture

Wim Leers’s picture

effulgentsia’s picture

FileSize
11.78 KB
4.79 KB

Fixed to incorporate that issue.

Also, renamed _loadToken to _loadStamp, because I think that more accurately describes it.

Wim Leers’s picture

The test coverage looks solid to me. Only minor stuff and nitpicks.

  1. +++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityStorage.php
    @@ -271,6 +282,46 @@ protected function has($id, EntityInterface $entity) {
    +    if ($this->entityType->isStaticallyCacheable() && !empty($this->entities)) {
    

    Isn't this tied to a single entity type, i.e. ConfigEntityType? Is it then still sensible to perform this check?

  2. +++ b/core/lib/Drupal/Core/Entity/ContentEntityDatabaseStorage.php
    index 3dc3bb6..5b831bd 100644
    --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
    
    --- a/core/lib/Drupal/Core/Entity/EntityStorageBase.php
    +++ b/core/lib/Drupal/Core/Entity/EntityStorageBase.php
    

    I like how everything moves from ContentEntityStorageBase to EntityStorageBase: consistency FTW! :)

  3. +++ b/core/modules/config/src/Tests/ConfigEntityStaticCacheTest.php
    @@ -0,0 +1,110 @@
    +class ConfigEntityStaticCacheTest extends KernelTestBase {
    

    Why can't this be a unit test?

  4. +++ b/core/modules/config/src/Tests/ConfigEntityStaticCacheTest.php
    @@ -0,0 +1,110 @@
    +    $entity_1 = entity_load($this->entityTypeId, $this->entityId);
    

    I've been told that I should never use entity_load() anymore, but <EntityTypeId>::load() instead. Since this tests always uses $this->entityTypeId === 'config_test, can't this be changed to ConfigTest::load()?

effulgentsia’s picture

Status: Needs review » Needs work

Thanks for the review. Setting to needs work for #17.4.

Re #17.1: yes, that's still needed, because some config entity types (e.g., Role) might want to enable static cache while others don't.

Re #17.3: I can't find any examples in HEAD where testing anything on the ConfigTest / config_test entity type is a unit test. I'm guessing cause unit tests can't depend on modules? Or maybe there's a way and all those other ConfigTest tests are just old? If you think it's doable, can you point me in the right direction for how? But additionally, we're doing load() and save() as part of these tests, so I don't know how hard it would be to mock all that.

Wim Leers’s picture

RE: #17.1: ok, that makes sense, thanks.

RE: #17.3: then let's not hold up this patch on that.

#17.4 is a nitpick, so it'd be silly to delay this patch for that. But at the same time, I'm not at all deeply familiar with the config system, so it'd be great if somebody more familiar with the config system could do the final review.

Gábor Hojtsy’s picture

Status: Needs work » Reviewed & tested by the community

I think its better to not do this as a unit test since we would need to mock way too much of what we actually want to test here. This is about interaction of several pieces. Otherwise looks good and resolves prior concerns.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 16: 2303881.16.patch, failed testing.

Gábor Hojtsy queued 16: 2303881.16.patch for re-testing.

Gábor Hojtsy’s picture

Status: Needs work » Reviewed & tested by the community

Random fail.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 16: 2303881.16.patch, failed testing.

Gábor Hojtsy queued 16: 2303881.16.patch for re-testing.

Gábor Hojtsy’s picture

Status: Needs work » Reviewed & tested by the community
webchick’s picture

Status: Reviewed & tested by the community » Fixed

Committed and pushed to 8.x. Thanks!

  • webchick committed 6b5c18d on 8.0.x
    Issue #2303881 by alexpott, effulgentsia: Fixed Config entity static...
mradcliffe’s picture

Status: Fixed » Needs work
Issue tags: +PostgreSQL

This change broke Postgresql even more. I think this is due to the changes from ContentEntityDatabaseStorage to EntityDatabaseStorage.

I think it's possible that the resetCache call in ContentEntityDatabaseStorage will create the cache table for the first time, and since that doesn't happen, it fails on other things in the installation.

mradcliffe’s picture

Adding the WIP patch for mimicking implicit commits in transactions allows Postgresql to continue through the install, but that requires a lot of testing because of what it does. See #2181291: Prevent a query from aborting the entire transaction in pgsql.

I guess that this patch clears the cache inside a transaction (like most entity save operations), which is going to fail hard because the cache table has not been created yet. The transaction is completely rolled back and no other queries are allowed.

mradcliffe’s picture

I'll keep this open, but we shouldn't revert the commit.

bzrudi71’s picture

Yup, this needs some work ;-) PostgreSQL testbot exceptions before patch ~240 - after patch ~1440. All errors are about:

A non well formed numeric value
    encounteredException->__construct('SQLSTATE[25P02]: In failed sql
    transaction: 7 ERROR:  current transaction is aborted, commands ignored
    until end of transaction block', '25P02', Object)
effulgentsia’s picture

I guess that this patch clears the cache inside a transaction (like most entity save operations)

Maybe I'm missing something, but from what I can see, HEAD was doing the same prior to #16 as well. The only change I can see is that the cache reset is now happening after $this->invokeTranslationHooks($entity); (which is also currently inside the transaction, both before and after the patch) instead of before.

mradcliffe’s picture

Status: Needs work » Closed (fixed)

Thanks for keeping this open as a reminder. Closing as other issue was committed.