Now we use lazy builders for flag links, we need to ensure that the render cache that contains references to those builders gets cleared.

For example, if you currently view a node with a flag link, then delete that flag, then reload the node, the site crashes:

Call to a member function getLinkTypePlugin() on null in /Users/xyz/sites/drupal/modules/flag/src/FlagLinkBuilder.php on line 54

This is because the lazy builder is trying to work with a flag that's not there any more.

Adding a flag *does* clear the right caches, so we're doing half of this right! :)

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

joachim created an issue. See original summary.

joachim’s picture

Title: deleting/adding a flag should clear entity caches » deleting a flag should clear entity caches
joachim’s picture

Issue summary: View changes
Berdir’s picture

We need to make sure that the cache tag for the flag is added to the render array in hook_entity_view().

martin107’s picture

We need to make sure that the cache tag for the flag is added to the render array in hook_entity_view().

I think the problem is a layer deeper than that

ActionLinkTypeBase:buildLink() is it better that we declare the appropriate cache tags every time we build a link?

In that case we need a cache tag for both the flag and the entity.

I may be missing something!

joachim’s picture

> ActionLinkTypeBase:buildLink() is it better that we declare the appropriate cache tags every time we build a link?

No, because that is called by a lazy builder. My understanding at least is that the lazy builder is to account for the flag link changing between flagged and unflagged, but that whether the link is shown at all or not is something for which the entity cache should be cleared, so the lazy builder's anchor is no longer present in the render array.

martin107’s picture

Title: deleting a flag should clear entity caches » Deleting a flag should clear entity caches
Status: Active » Needs review
FileSize
425 bytes

Thank for the response...

I need to lay out the chain of thought .... I am missing something.

The reason for using the place holder :-

To separate the thing subject to rapid change ( the flag link )
away from the thing that would otherwise be cached ... the article - or whatever.

I get that but

By linking the #lazy_biulder element to the rapidly changing flag -
does that not invalidate the separation argument and force the article to become dependant on the flag anyway?

Regardless --- I tried implementing the suggestion....and note it does fix the problem!

I think I may have made a mistake .... it is easy to screw up the keys in a render array..
but I am going to post away just in case the problem is deeper than our understanding.

Berdir’s picture

The lazy builder exists because whether the flag shows flag or unflag varies per user (and if he can actually access it). We don't want to cache a version of the whole node for every user, so we use a lazy builder to calculate only that link.

Changing the flag doesn't happen often. If you do that, then might have results that we can't handle. For example, if you change the field to show not as an extra field but as a link? That's not something we can handle in a lazy builder.

So if that happens, we want to invalidate all currently cached nodes that show that flag.

The code looks OK to me. There are API's to collect and merge cacheability metadata but for something that's as simple as this, doing it directly is fine I think.

martin107’s picture

I went looking for a reason why the patch has no effect.

\Drupal\Core\Render\Renderer:doRender()

Line 308 is where some #lazy_builder integrity checks begin

Here is a code fragment from line 323, if I read it correctly our new #cache key is automatically be overridden.

      $supported_keys = [
        '#lazy_builder',
        '#cache',
        '#create_placeholder',
        // These keys are not actually supported, but they are added automatically
        // by the Renderer, so we don't crash on them; them being missing when
        // their #lazy_builder callback is invoked won't surprise the developer.
        '#weight',
        '#printed'
      ];
socketwench’s picture

If I'm reading this right, this issue is stuck at "needs work"?

martin107’s picture

Status: Needs review » Active

Yep, we have a patch that has no effect.

Speaking for myself ... I am still mulling things over ...

lazy builder can become stale for all sorts of reasons, which are particular to the unique circumstance.

The canonical way of lazy builder invalidation seems to assume things which don't work as soon as you want to say I know best - this is the way you optimise for our case..

As far as I have got ... it sounds like we need to provide a good trail blazing example of why and how core need to become more flexible.

Sorry if it sound like a council of despair ... I am sure we will get there :)

edurenye’s picture

The last submitted patch, 12: deleting_a_flag_should-2629426-12-test_only.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 12: deleting_a_flag_should-2629426-12.patch, failed testing.

edurenye’s picture

Fixed moving out the lazy_builder.
Moved before checks so it will work also for disabling flag, so I removed the cache resets and added disable test also.

joachim’s picture

Can you compare with the test that #2617384: missing test coverage for flag deletion adds please?

Berdir’s picture

#2617384: missing test coverage for flag deletion has been committed. But I'm not sure we should merge with that. That test is in AdminUI, it's about testing the UI. This is specifically to test render cache invalidation when editing/deleting a flag.

That said, looking through the tests, enable/disable is apparently already tested in FlagEnableDisableTest, so I think we don't need that. Given that, if only delete is remaining, then merging it into that test would work for me.

@joachim: ?

Berdir’s picture

Status: Needs review » Needs work
  1. +++ b/flag.module
    @@ -168,6 +169,8 @@ function flag_entity_view(array &$build, EntityInterface $entity, EntityViewDisp
       foreach ($flags as $flag) {
    +    $build['#cache']['tags'] = Cache::mergeTags($build['#cache']['tags'], $flag->getCacheTags());
    +
         // Do not display the flag if disabled.
    

    Note that we moved it before we check if it's enabled and its setting and have it outside of the lazy builder. It has by design a different effect in there and doesn't bubble up.

  2. +++ b/src/FlagService.php
    @@ -231,12 +231,6 @@ class FlagService implements FlagServiceInterface {
    -    $this->entityManager
    -      ->getViewBuilder($entity->getEntityTypeId())
    -      ->resetCache([
    -        $entity,
    -      ]);
    

    We went through all resetCache() calls and removed all that we could. You might want to move some into separate issues.

    This one is pointless since we switched to a lazy builder. invalidating the entity cache after flagging doesn't help us in any way and is unneeded.

  3. +++ b/src/Form/FlagDisableConfirmForm.php
    @@ -91,11 +91,6 @@ class FlagDisableConfirmForm extends ConfirmFormBase {
    -    // Invalidate the flaggable render cache.
    -    \Drupal::entityManager()
    -      ->getViewBuilder($this->flag->getFlaggableEntityTypeId())
    -      ->resetCache();
    

    this is duplicated anyway by the code in pre (or post) Save and double so by the new cache tag that we add to the output.

    I think it would be nice to remove the one in preSave() too but that would require that we also add a list cache tag to the view alter. Which would be a small overhead since we'd invalidate everything unless we define a per-entity type list cache tag for flags that we invalidate. Also, other ways of displaying the flag would need that cache tag too.

  4. +++ b/src/Tests/AdminUITest.php
    @@ -246,7 +246,6 @@ class AdminUITest extends FlagTestBase {
         // Load the vocabularies from the database.
         $flag_storage = $this->container->get('entity.manager')->getStorage('flag');
    -    $flag_storage->resetCache();
         $updated_flags = $flag_storage->loadMultiple();
    

    This was never needed, config entities have no static cache. Also a wrong comment above this.

  5. +++ b/src/Tests/FlagSimpleTest.php
    @@ -219,6 +221,7 @@ class FlagSimpleTest extends FlagTestBase {
     
    +    $this->drupalLogout();
         $user_1->delete();
    

    deleting the user we're currently logged in with confuses the test system. I still thinks that we are logged in as that (and has no way of knowing otherwise). on the next drupalLogin(), it then tries to log out first, which gives an access denied.

edurenye’s picture

Merged the tests and fixed the comment of the point 4.

The last submitted patch, 19: deleting_a_flag_should-2629426-19.patch, failed testing.

The last submitted patch, 19: interdiff-deleting_a_flag_should-2629426-15-19.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 20: deleting_a_flag_should-2629426-20.patch, failed testing.

Berdir’s picture

  1. +++ b/src/Tests/AdminUITest.php
    @@ -258,6 +257,12 @@ class AdminUITest extends FlagTestBase {
    +    // Flag node.
    +    $this->drupalGet('node/' . $this->nodeId);
    +    $this->clickLink('Flag this item');
    +    $this->assertResponse(200);
    +    $this->assertLink('Unflag this item');
    

    I don't think we need to flag. Just update the test to look for Flag this item instead.

  2. +++ b/src/Tests/FlagSimpleTest.php
    @@ -233,6 +233,7 @@ class FlagSimpleTest extends FlagTestBase {
     
         // Delete the user.
    +    $this->drupalLogout();
         $user_1->delete();
    
    @@ -316,6 +317,7 @@ class FlagSimpleTest extends FlagTestBase {
         $this->assertEqual(1, $count_flags_before);
     
         // Delete  user 1.
    +    $this->drupalLogout();
         $user_1->delete();
     
    

    you can remove those.

edurenye’s picture

Berdir’s picture

Status: Needs review » Needs work
+++ b/src/Tests/AdminUITest.php
@@ -257,12 +257,6 @@ class AdminUITest extends FlagTestBase {
   public function doFlagDelete() {
-    // Flag node.
-    $this->drupalGet('node/' . $this->nodeId);
-    $this->clickLink('Flag this item');
-    $this->assertResponse(200);
-    $this->assertLink('Unflag this item');

Now you deleted too much IMHO. We want to have a positive test, and make sure that we look for a specific string (like Flag this item) before deleting and then the same after deleting.

assertNo* assertions are always problematic. E.g. the string you're not expecting could change and then you wouldn't assert anything anymore.

edurenye’s picture

Ok, I readded the positive test and a test that actually fails.

The last submitted patch, 27: deleting_a_flag_should-2629426-27-test_only.patch, failed testing.

Berdir’s picture

Status: Needs review » Reviewed & tested by the community

I think this is good to go then...

  • edurenye authored ac42560 on 8.x-4.x
    Issue #2629426 by edurenye, martin107: Fixed deleting a flag should...
socketwench’s picture

Thanks everyone!

socketwench’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.