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
Comment #2
jonathan1055 commentedPatch 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.
Comment #3
jonathan1055 commentedI 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.
Comment #4
tr commentedI will test this with the enable/disable functionality too.
Comment #5
jonathan1055 commentedHere is an addition to
testConfigureAndExecutewhich triggers the same rule again. This should fail at core 8.7Comment #6
jonathan1055 commentedInteresting that the exception message in the failed 8.7 test on d.o
is slightly different from the one I see locally
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
Comment #7
tr commentedComment #8
tr commentedThis 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.
Comment #10
tr commentedComment #12
jonathan1055 commentedI 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?Comment #13
tr commentedNo, 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.Comment #14
tr commentedAnd here's a version of #8 with that same commented out code. This contains everything from #6.
Comment #15
jonathan1055 commentedThanks 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.
Comment #16
jonathan1055 commentedWell, 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_beforearray 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_beforelist. We need to create an$events_afterlist 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.
Comment #17
tr commentedYes, 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 ...
I agree this is happening, but I don't know why this is a problem.
In
EventComponentResolver::getMultiple()we do this:When a Rule is disabled (via
ConfigEntityBase::disable()- this is core code, not specific to Rules ...) and the config is saved (viaConfigEntityBase::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.
Comment #18
jonathan1055 commentedThanks for the background info, it is very helpful.
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.
Comment #19
tr commentedDo we even need the
RulesComponentRepositoryanymore? 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.Comment #20
jonathan1055 commentedI 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.
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.
Comment #21
tr commentedOK, sounds reasonable. I'll work this tonight and get it committed first.
Comment #22
tr commentedHere'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.
Comment #23
jonathan1055 commentedThanks. 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
Comment #25
tr commentedCommitted. Thanks for figuring this out.