This is a follow-up to #3105760: Empty Rules are firing where this commit to only fire rules where a $config exists can cause a caching exception:

Drupal\Core\Entity\EntityStorageException
SQLSTATE[21S01]: Insert value list does not match column list:
1136 Column count doesn't match value count at row 1:
INSERT INTO {cache_rules} (cid, expire, created, tags, checksum, data, serialized) VALUES () ON DUPLICATE KEY UPDATE cid = VALUES(cid), expire = VALUES(expire), created = VALUES(created), tags = VALUES(tags), checksum = VALUES(checksum), data = VALUES(data), serialized = VALUES(serialized); Array()
in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 847 of core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php

In RulesComponentRepository::getMultiple() we try to find and store any missing components. As resolvers[$provider]->getMultiple (EventComponentResolver::getMultiple in this case) can now return an empty result, we need to avoid calling cacheBackend->setMultiple($cache_items) in these cases, as that is what causes the exception when $cache_items is empty.

Comments

jonathan1055 created an issue. See original summary.

jonathan1055’s picture

Status: Active » Needs review
StatusFileSize
new1.28 KB

Patch attached. See #3105760-8: Empty Rules are firing for more details and a debug output file.

The exception was only produced at core 8.7 because at core 8.8 and 8.9 DatabaseBackend::doSetMultiple() now has a check to exit early if $values is empty, which is not in the 8.7 code. However, the change to Rules is good to keep for 8.8+ as it makes sense not to call the cache update anyway if there is no data.

jonathan1055’s picture

Status: Needs review » Needs work

I will use the knowledge of test failures from #3106316: Scheduler Rules integration - failures at Core 8.7 - Rules cache update problem to amend Rules tests to cover this problem.

tr’s picture

I will test this with the enable/disable functionality too.

jonathan1055’s picture

Status: Needs work » Needs review
StatusFileSize
new994 bytes

Here is an addition to testConfigureAndExecute which triggers the same rule again. This should fail at core 8.7

jonathan1055’s picture

StatusFileSize
new2.25 KB

Interesting that the exception message in the failed 8.7 test on d.o

1) Drupal\Tests\rules\Functional\ConfigureAndExecuteTest::testConfigureAndExecute
Exception: Warning: array_fill(): Number of elements must be positive
Drupal\Core\Database\Query\Upsert->getInsertPlaceholderFragment()() (Line: 172)

is slightly different from the one I see locally

Drupal\Core\Entity\EntityStorageException
SQLSTATE[21S01]: Insert value list does not match column list:
1136 Column count doesn't match value count at row 1:
INSERT INTO {cache_rules} (cid, expire, created, tags, checksum, data, serialized) VALUES () ON DUPLICATE KEY UPDATE cid = VALUES(cid), expire = VALUES(expire), created = VALUES(created), tags = VALUES(tags), checksum = VALUES(checksum), data = VALUES(data), serialized = VALUES(serialized); Array()
in Drupal\Core\Entity\Sql\SqlContentEntityStorage->save() (line 847 of core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php

They both stem from Drupal\Core\Database\Query\Upsert but the d.o. exception is found eralier in the processing. Probably some difference in php version or other backend stuff. No matter, we are not checking the exception, our test would still fail the new assertion. Here is a patch with the combined code from #2 and #5

tr’s picture

tr’s picture

This is patch #6 plus the enable/disable Rule code (and test) I had to remove from #3104328: Move list builders from D8RE into Rules because of this problem.

My expectation is that the patch in #6 will make the enable/disable functionality work again, so I'm testing that here.

Status: Needs review » Needs work
tr’s picture

Status: Needs work » Needs review
StatusFileSize
new4.29 KB

Status: Needs review » Needs work
jonathan1055’s picture

Status: Needs work » Needs review

I can see that between #8 and #10 you have taken out the new bit of test code from #6. The failure has moved from line 115 to 107 so I think it is actually in the new test code lines. The first $assert->pageTextNotContains('Title matched "Test title"!') after disabling the rule. Does the cache need to be cleared for the disable to become effective?

tr’s picture

No, a cache clear should not be necessary. We don't want to have to do that.

This was all working in D8RE, including the tests, before I made the changes in #3105760: Empty Rules are firing (&BTW it is STILL working in D8RE for a reason I can't remember right now ...)

The controller does this when disable is pressed: $rules_reaction_rule->disable()->save();
Then in the storage, save() recomputes the registered events.

But there is an 'optimization' in save() that only rebuilds the container under certain circumstances. If I comment out that optimization I think it works properly - here's a new version of patch #10.

tr’s picture

And here's a version of #8 with that same commented out code. This contains everything from #6.

jonathan1055’s picture

Thanks for the explanation and background. It has allowed me to investigate and I have solved this. Will explain and post patch tomorrow, sorry no time now, but did not want you to waste time doing what I have just done.

jonathan1055’s picture

Well, I may have been a bit hasty, I have partially solved this. Enabling a rule works fine, just like adding a rule. If the event is not in the $events_before array then the drupalKernel->rebuildContainer() is called as required. However, when disabling a rule, the opposite check now also needs to be done. The event will always be in $events_before list. We need to create an $events_after list and if the event is not in that array, i.e. the disabled rule removed the last use of that event, then we also do the drupalKernel->rebuildContainer().

Attached is a patch which contains this amendment, and the tests should pass. This properly fixes the optimization for registered events, but it is not the end of the problem. If two rules both react on the same event, then when the first is disabled the container is not rebuilt. The disabled rule still fires and we need to clear the cache to make it not fire. #2769511: Cache is not invalidated when Rules are changed is the likely cause/solution. It is only when the second rule is disabled and the event is no longer used at all, then we get neither rule firing.

If this patch passes, I would recommend committing it, as it does most definitely fix the original problem - that of attempting to update the cache with null parameter, which is causing real errors at 8.7. Then we can work on the optimisation/kernel rebuild/cache-clear problem separately. Extra test coverage will also be needed to test adding/disabling/enabling/deleting two rules with the same event.

tr’s picture

If this patch passes, I would recommend committing it, as it does most definitely fix the original problem

Yes, I didn't mean to imply that this patch should be held up until it fixes enable/disable. I was just using this issue to test if it fixed enable/disable because the symptoms and errors of that were the exact same as the symptoms/errors this issue was designed to fix, and it was a convenient place to test to see if it also fixed enable/disable - that would have been nice, but not necessary for this patch.

As for the rest of #16 ...

If two rules both react on the same event, then when the first is disabled the container is not rebuilt. The disabled rule still fires

I agree this is happening, but I don't know why this is a problem.
In EventComponentResolver::getMultiple() we do this:

      // Only load active reaction rules - inactive (disabled) Rules should
      // not be executed, so we shouldn't even load them.
      $configs = $this->entityStorage->loadByProperties([
        'events.*.event_name' => $event_id,
        'status' => TRUE,
      ]);

When a Rule is disabled (via ConfigEntityBase::disable() - this is core code, not specific to Rules ...) and the config is saved (via ConfigEntityBase::save()), that should invalidate the config entity cache for that config. Then even if the container hasn't been rebuilt, the event component resolver should only return configs for enabled Rules, right?

I agree, it sounds a bit like #2769511: Cache is not invalidated when Rules are changed is involved, which is a legacy code issue IMO - Rules in D7 had to micromanage caching because core didn't do it, but in D8 Rules is still trying to take control of caching even though core is caching everything by default (core didn't do this early in D8 when Rules was first ported, so I understand why Rules took this approach initially in D8). My sense is that if we just let core do its thing without interference then this wouldn't be an issue. Maybe at most we'd have to invalidate a cache tag here and there to let core know, but other than that I'm not sure Rules should be much involved in caching anymore.

As far as the container goes, the only reason we're rebuilding the container is because of the event subscriber. Event subscribers register the events they are interested in receiving, and this registration only happens when the container is built. So if we want to change the list of events we are listening to we need to rebuild the container. But the rebuild doesn't clear the config entity cache AFAIK, so I'm not sure why the rebuild would have an effect on which configs the EventComponentResolver finds.

jonathan1055’s picture

Thanks for the background info, it is very helpful.

I agree this is happening, but I don't know why this is a problem.

Maybe at most we'd have to invalidate a cache tag here and there to let core know ...

I have done some work on the caching and posted a patch in #2769511-14: Cache is not invalidated when Rules are changed which fixes this. You are right that the container does not need to be rebuilt in between, but we do need to invalidate the cache tags.

Unfortunately this patch clashes with that one, so which ever is committed first, the other will need re-rolling. Hopefully we can get both of these issues committed soon.

tr’s picture

Do we even need the RulesComponentRepository anymore? Couldn't the caching of Rules Components be handled by core now instead of by our own static cache of cids? #6 does seem to fix a bug in the RulesComponentRepository code, but if we used core caching then that core code is constantly being tested by tens of thousands of people and is being actively maintained by core maintainers. Using core would make for more robust and reliable code and free us up to deal with other issues rather than having to re-invent caching for our own needs.

I think the idea with RukesComponentRepository was to make Rules more efficient by caching components rather than reloading them on each page request. Loading them from the cache when needed, instead of rebuilding them from information scattered in the database, would indeed be more efficient, but it seems it wasn't being done correctly here or in #2769511: Cache is not invalidated when Rules are changed and I don't look forward to finding out where else and having to fix all the future problems with this. It seems to me that we could use cache tags when we do the $this->cacheBackend->setMultiple() and invalidate the tags at the appropriate times, then we wouldn't need most of the code in RukesComponentRepository.

jonathan1055’s picture

Do we even need the RulesComponentRepository anymore?

I don't know. I have no idea and no way of telling at present. But it sounds like a piece of work which is out of scope of this issue. If we do embark on that, then it would be important to be starting from a place where relevent known bugs are fixed and we have test coverage for it.

The patch in #16 above demonstrably fixes two bugs (updating cache with null value and not rebuilding the container on change of registered events), includes test coverage for both the bugs and also turns on the enable/disable buttons functionality, so it would be very nice if you could commit this. Then I can extend my patch on #2769511: Cache is not invalidated when Rules are changed to add a new test for creating two rules reacting on the same event and edit/disable/re-enable/delete and verify that the changes are effective immediately without any manual cache-clear.

It seems to me that we could use cache tags when we do the $this->cacheBackend->setMultiple() and invalidate the tags at the appropriate times

Yes, that is exactly what I have added on patch #14 in that other issue.

I think both of these issues should be done first, then we will have fixed three bugs and added test coverage, which will allow a proper investigation into removing RulesComponentRepository if we decide to do that.

tr’s picture

OK, sounds reasonable. I'll work this tonight and get it committed first.

tr’s picture

Here's patch #16 without the enable/disable. I will handle the enable/disable in #3104328: Move list builders from D8RE into Rules after this issue and #2769511: Cache is not invalidated when Rules are changed are committed.

jonathan1055’s picture

Status: Needs review » Reviewed & tested by the community

Thanks. Yes, that likewise sounds reasonable. All good here so RTBC and when committed I will finish off #2769511: Cache is not invalidated when Rules are changed

  • TR committed 99baa90 on 8.x-3.x authored by jonathan1055
    Issue #3108494 by jonathan1055: Exception when updating cache with empty...
tr’s picture

Status: Reviewed & tested by the community » Fixed

Committed. Thanks for figuring this out.

Status: Fixed » Closed (fixed)

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