Updated: Comment #N

Problem/Motivation

Currently RulesPlugin:save() clears the whole cache if the no original entity was found
or if the variables from the original entity don't match with the current ones.
Problem with this approach is that the property original never exists
because it's deleted by EntityAPIController::save().
In our environment with a looooot of rules (that are triggered in the frontend) this causes some serious trouble (stampeding) when rebuilding the whole cache.
Especially since the "cache" rules_event_whitelist is handled as variable: #2042851: PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'rules_event_whitelist' for key 'PRIMARY
This would be less of a problem if it would be handled by cache_get() because we use redis and that wouldn't fail this way.
However, I guess the performance problem is already enough annoying to justify a fix ;)

Proposed resolution

Fetch the original entity on our own and make it reusable for the other entity controllers.
This pattern is already implemented in several other locations e.g. RulesEntityController::save().
Question:
Is there a way to make the cache flush even more atomic?
E.g. just removing the current component from the cache data array.
The patch rules-rules-plugin-cache-clearing-fix-flush-specific-component.patch contains a POC to test such an approach. The tests so far show a dramatic performance gain.

Remaining tasks

Reviews needed.

User interface changes

None

API changes

None

Comments

das-peter’s picture

Issue summary: View changes
das-peter’s picture

Worked a little bit on the POC (hey, it's green too *yay*).
There are still some cases I fear, like changing component variables will trigger again a whole cache clear. But thankfully this case should occur not that often.
Also the event caching / white-listing looks like it could be optimized in a similar way.
We've a pretty clean cache structure, that should allow us to implement more atomic operations in the cache clearing / building. And there's always the sledgehammer method if something goes west - full rebuild.

das-peter’s picture

Another adjustment for the POC. With the new cache clearing for components it should be possible to keep the cache even if a new component is created. The cache of the new component will then be built on demand.

Status: Needs review » Needs work

das-peter’s picture

Assigned: Unassigned » das-peter

I'm still working on this. I've to evaluate how I can adjust the cache to be more lazy.

das-peter’s picture

Here we go. No interdiff since it would be almost bigger than the patch.
I still don't like the fact that the cache has no "fallback": cache miss = fail instead cache miss = rebuild
But for now the adjustments should speed up all the stuff quite a bit.
Locally all tests pass.

Status: Needs review » Needs work
das-peter’s picture

Status: Needs work » Needs review
StatusFileSize
new698 bytes
new16.4 KB

Fixed issue. Seems to be php version dependent.

Paul B’s picture

Status: Needs review » Needs work

With the patch in #9, if I try to add a loop to an action set, I get a fatal error "Call to undefined function rules_rule_build_action_info()"

das-peter’s picture

Status: Needs work » Needs review
StatusFileSize
new491 bytes
new16.49 KB

Oh, indeed. Unfortunately it's not granted that the newly used functions in _rules_rebuild_component_cache() are available.
Attached patch adds an include to the file that takes care of including all that stuff.

fago’s picture

Status: Needs review » Needs work
  1. +++ b/includes/rules.core.inc
    @@ -339,7 +339,14 @@ abstract class RulesExtendable extends FacesExtendable {
    +      $this->cache += rules_fetch_data($this->hook);
    

    How can this happen / be necessary? The cache is just initialized above, does that mean the cache is built "broken"?

  2. +++ b/includes/rules.core.inc
    @@ -1064,11 +1071,32 @@ abstract class RulesPlugin extends RulesExtendable {
    +  public function getCid(){
    

    Good addition. Let's better name this getCacheId() though.

  3. +++ b/includes/rules.core.inc
    @@ -1064,11 +1071,32 @@ abstract class RulesPlugin extends RulesExtendable {
    +      // Clear cache of this child element if this isn't a rebuild.
    +      if (empty($this->is_rebuild)) {
    

    Why would we skip this if it is a rebuild? That does not say there are no changes.

  4. +++ b/includes/rules.core.inc
    @@ -1113,20 +1147,31 @@ abstract class RulesPlugin extends RulesExtendable {
    +          if (!empty($original) && $this->componentVariables() != $original->componentVariables()) {
    

    When adding a new one, we need to rebuild caches as well or the new action won't be available.

  5. +++ b/includes/rules.core.inc
    @@ -1113,20 +1147,31 @@ abstract class RulesPlugin extends RulesExtendable {
    +          // Make sure any remove event is updated too.
    +          if (!empty($original)) {
    +            foreach (array_diff($original->events, $this->events()) as $event_name) {
    

    Let's better aggregate event names first and then rebuild caches for all of them. This should make the code a bit simpler.

  6. +++ b/modules/rules_core.rules.inc
    @@ -229,49 +229,85 @@ function rules_rules_core_data_processor_info() {
    +function rules_rule_build_condition_info(RulesConditionInterface $rules_config) {
    +  static $defaults;
    +  // Ensures that t() is called just once for this.
    +  if (!$defaults) {
    

    What's that about? Why would it be called more often?

  7. +++ b/rules.module
    @@ -388,8 +401,24 @@ function _rules_rebuild_component_cache() {
    +        $data_cache['action_info']['component_' . $component->name] = rules_rule_build_action_info($component);
    +        $data_cache['action_info'] += rules_discover_plugins('RulesActionHandlerInterface');
    +        drupal_alter('rules_action_info', $data_cache['action_info']);
    

    Uhm, fuddling around with the built action/condition info cache seems to be dangerous, i.e. could quickly lead to obscure bugs.
    Couldn't we just improve it to rebuild whole condition/action info caches when not more is necessary? Otherwise, if you need to save the cache rebuild I could see us adding an opt-out of cache-rebuilding when you add a new component?

  8. +++ b/rules.module
    @@ -420,13 +449,48 @@ function rules_flush_caches() {
    +function rules_is_event_configured($event_name, $reset = FALSE) {
    +  $rules_event_whitelist = rules_get_cache('rules_event_whitelist');
    +  if ($rules_event_whitelist === FALSE || $reset) {
    

    Hm, that adds quite a few function call for *every* rules_invoke_event() what's bad. Also, I'm not sure about adding the separate cache vs the previous variable cache - why is that better?
    Reading #2042851: PDOException: SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry 'rules_event_whitelist' for key 'PRIMARY - I see, but could we handle that over there? One issue by one? :-)

das-peter’s picture

Status: Needs work » Needs review
StatusFileSize
new5.04 KB
new17.26 KB
  1. Well, that's actually just like a cache should work. if something is missing in the cache build and cache it now. I'm not even sure if we hit this case. It's more about defensive coding.
  2. Done.
  3. If it is a child and a rebuilt the parent will flush the whole cache anyway.
  4. Not sure if I understand that problem. The whole change in the approach has the goal to build up missing elements on demand. So if something is new, it's missing in the cache and should be built when requested.
  5. Done.
  6. As far as I remember I came across this when I was profiling. But I can't remember the exact stack trace.
  7. I know that's the worst part. But frankly I've no clue how I could solve that by not falling back to clear all the stuff again :| This is exactly where having a "compiled" monolith cache becomes reeeeally awful.
  8. Hmm, okay. I'll create a spin off. However, in the updated patch I still kept it and changed the approach a bit. What do you think about that?
das-peter’s picture

Damn, I forgot to adjust an error I found yesterday.

joelpittet’s picture

Looks like it needs a re-roll. I'll try and do so.

patching file includes/rules.core.inc
patching file includes/rules.plugins.inc
patching file modules/rules_core.rules.inc
patching file rules.module
Hunk #1 FAILED at 329.
Hunk #2 succeeded at 396 (offset 22 lines).
Hunk #3 succeeded at 426 (offset 22 lines).
Hunk #4 succeeded at 474 (offset 22 lines).
Hunk #5 FAILED at 1049.
Hunk #6 FAILED at 1082.
3 out of 6 hunks FAILED -- saving rejects to file rules.module.rej

joelpittet’s picture

Hmm. I gave it a try but it looks like a different approach was taken instead of static cache to use a $conf instead.

joelpittet’s picture

StatusFileSize
new18.05 KB

FWIW, here's a the reroll, though I'm not totally sure I got it correct, @das-peter thoughts?

joelpittet’s picture

Linking up a different approach to the same problem

joelpittet’s picture

StatusFileSize
new17.84 KB

@das-peter This needed another re-roll. Again not sure if I got the conflicts merged correctly. Could you have a peek here? Or maybe this is not needed anymore?

fago’s picture

Having a closer look at this one, here are my thoughts:

  • Updating action/condition info with the new component data is trouble-some as this cache is per language. We could do so for the current language, but then the problem still persists for the other languages.
  • Rebuild component cache per component I think would be a good improvement - that way the operation will take less time and the lock will be more granular. I'd not updated it immediately though, such that the update operation of the cache triggers the proper lock.
  • As the patch starts, same we can do for events: Rebuild them separately, thus resulting in more fine-granular locking. Looking up the locks one some life sites, most of them are in rules_event_whitelist - thus splitting that rebuild process up should help a lot.
  • Related, I checked the queries for loading the uncached configuration and noted that the loading active reaction rules is not indexed (no index on 'active'). So we can improve this a bit also.
  • Finally, rules_get_cache() locking has a re-duced wait time of 10 seconds but starts to duplicate efforts after it - what is counter-productive if the rebuild is not finished in time. Let's increase waiting time to the default of 30s.

However, all of this improvements do not address this issue - still, a component change including variables, or an addition triggers clearing of the complete rules cache. We can easily improve this to only clear the rules 'data' cache + the cache of the component changed, once we have done the improvements above. Hopefully, that will be enough to solve the problems. If not, I think we should split up the rules 'data' cache monolith into separate chunks per (condition_info, action_info, ..) which can be built + cleared separately by converting it to a DrupalCacheArray (as it was done with the ThemeRegistry).

fago’s picture

Created #2650886: Reduce the likeliness of cache rebuilding locks for the improvements not necessarily related to this and leaving this issue open for

>We can easily improve this to only clear the rules 'data' cache + the cache of the component changed, once we have done the improvements above.

fago’s picture

Status: Needs review » Needs work
fago’s picture

Title: Full cache cleared whenever a rules component is edited » Avoid full cache clear whenever a rules component or reaction rule is edited
Status: Needs work » Needs review
StatusFileSize
new11.01 KB

ok, I worked over the patch to make it work as described before (points 1+2) from #20 + #21.

>I'd not updated it immediately though, such that the update operation of the cache triggers the proper lock.
I figured it should be fine to update immediately and did so for events + components. As there should be old, active cache entries that are used meanwhile, there is no need for a lock while building the cache entry when updating per rule-save.

Patch attached. In my tests it worked fine.

joelpittet’s picture

Assigned: das-peter » Unassigned

I've been using this patch for a while, so far so good. +1 but could use a last code-review before going in

Status: Needs review » Needs work

The last submitted patch, 23: d7_component_caches.patch, failed testing. View results

tr’s picture

Status: Needs work » Needs review
StatusFileSize
new11.01 KB

Patch just needed a little tweak/re-roll to apply against the current HEAD. I haven't reviewed this yet - this is just a re-roll to bring it up to date.

tr’s picture

StatusFileSize
new11.02 KB

The testbot shows that the patch introduces two coding standards errors, so here is a re-roll with those two fixed:

joelpittet’s picture

Thanks for the re-roll @TR, I've reapplied it.

joelpittet’s picture

Status: Needs review » Reviewed & tested by the community

So far it's on two production sites and no issues to speak of

The last submitted patch, 13: interdiff-2189645-11-13-do-not-test.diff, failed testing. View results

The last submitted patch, 14: interdiff-2189645-13-14-do-not-test.diff, failed testing. View results

delacosta456’s picture

hi
i try #17 and #19 with phpstorm and patch has some chunk issues

tr’s picture

@delacosta45:

i try #17 and #19 with phpstorm and patch has some chunk issues

#17 is from the year 2014 and #19 is from 2015 - of course they don't apply anymore! I really don't understand the purpose of your post. #17 and #19 are obsolete patches, so why bother testing them?

Also, those are NOT the patches that we care about and those patches will NOT be committed. Fixes to these patches were made in #23, and that patch was re-rolled and improved in #26 and #27.

The patch under consideration is #27, which is more than a year old and needs a re-roll to apply against the current Rules HEAD.

It would be helpful if you could re-roll #27 and try it yourself, then post the new patch and your review here.

delacosta456’s picture

@TR

hi ok .. as those patch were not hidden and doesn't have the same naming i though i had to apply the one before the other.

Applying #27 directly applied successfully .

jonathan1055’s picture

Re-roll of #27.

Other patches hidden to avoid the mistake like #32

Do we need to consider this for 8.x? I think the caching is done in a completely different way, and it is not a problem at 8.x but just wanted to ask the question.

tr’s picture

No, none of this should be considered for D8. Caching in D8 is mostly a function of core. If someone provides specific tests/use-cases to demonstrate that caching in D8 can be improved, that will require a completely different solution and should be handled in a separate issue. At this time I have seen no indication that caching is currently a problem in D8.

The only thing bothering me about this patch is that it has the potential to be disruptive to the >200,000 users of D7 Rules. The D7 version of Rules is very stable, and we ought to take great pains to do nothing to break sites that have been using Rules for many years. While it's great we have a patch for people who DO need it, I question whether this should be pushed into the codebase to be used by people who DON'T need it. A set of test cases would go a long way to prove that this won't adversely affect existing users, especially since our D7 version doesn't have very good coverage to being with.