Problem/Motivation
We noticed that some of our search_api views were not correctly invalidated when a node they are containing is edited.
This happens specifically when the page containing the view is reloaded after the node has been edited and before the Solr index has been updated.
What seems to happen is:
- The node is edited so the
node:xxcache tag is invalidated. - The page containing the view has the
node:xxcache tag so it is invalidated in page cache. - The view does not have this cache tag (it only has
search_api_list:foo) so it is not invalidated in data cache. - When the user reloads the page, the node is re-rendered but with outdated data from data cache.
- Then the index is updated, which invalidates the
search_api_list:foocache tag (which invalidates both the page and view cache, but not the node render cache). - If the user reloads the page again, the view is recalculated but the node render is not (and still contains the outdated data from before).
This can only happen if nodes are not indexed immediately after they are updated (for example, if indexing is done with a cron job).
Steps to reproduce
- Create a search_api index with the
index_directlysetting disabled. - Create a search_api view listing node view modes that returns at least one node.
- Edit the node title (and do not trigger an index update).
- Reload the page containing the view: you still see the old node title.
- Trigger an index update.
- Reload the page containing the view again: you still see the old node title.
Proposed resolution
One solution would be to add the node cache tags to the view data cache.
This way, the node render would be recreated with the correct data at the first reload.
This can be done by returning the entity cache tags in SearchApiQuery::getCacheTags() (similar to what Sql::getCacheTags() does.
| Comment | File | Size | Author |
|---|
Issue fork search_api-3327645
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
prudloff commentedComment #4
idebr commentedAlternatively use the suggested Solr configuration from #3090459: Default value 1000 for 'Commit within' creates risk of stale content
Comment #5
drunken monkeyThanks for suggesting this improvement, and sorry it took me a while to get back to you!
Unfortunately, the intricacies of Views’ caching mechanism is still quite a mystery to me, and caching search results (especially in combination with Solr) is very challenging to begin with, so I fear I can’t be of much help here, but have to count on as many reviewers/testers/feedbackers as possible to help make an informed decision.
However, it seems there has been a commit to the same code in the meantime in #2905497: Views Integration - Consider compatibility with internal page cache. Does the problem still occur for you with the latest module version? If so, please re-base your MR and then post it as a patch – testing on issue forks unfortunately doesn’t work in this project, see #3190024: Problem with test dependencies when testing issue forks.
Comment #6
prudloff commentedI can still reproduce with search_api 1.29: the view cache does not have the node cache tags.
Here is a reroll based on 1.29.
Comment #7
prudloff commentedComment #9
huzookaMy team also ran into an issue very similar to this; but our case was a bit more specific. Our indexes are configured to re-index items immediately, so we use post request indexing.
We have an SAPI index view using VBO. We had to build a Views Bulk Operation (VBO) action which changes SAPI-indexed entities. Our (content manager) users are using this action on a SAPI index-based Views display. So, these users visit the views page (which is a form), pick some items from the views list, choose the action and submit the VBO form.
What we've seen is that the Views display wasn't ever updated with the right data. Every other SAPI Views list using the same index seemed to be updated accordingly, but this admin view was stuck. The only solution which worked was to force a render cache invalidation.
I've spent days trying to catch the moment when things go wrong. Now, I'm quite confident to say that the bug we ran into is indeed about wrong cache metadata.
Steps to reproduce:
This is what happened in the PHP threads in our case, after submitting the form, without any patches applied:
Thread A (the form submission's PHP process)
Thread B (starts after client browser reloads the VBO form's page)
1. Request arrives
2. The indexed entity is changed and saved.
Its cache tags are invalidated, and the entity is scheduled to be re-indexed.
3. HtmlRenderer starts
4. HtmlRenderer results in (Form API) EnforcedResponseException
5. LocalRedirectResponse is sent to the client browser, resulting in a concurrent request (and another PHP thread)
1. Request arrives
2. HtmlRenderer starts.
Since the content of the main page isn't cacheable, Drupal starts to render the VBO form. (Note: Only the outermost Views display renderable has the cache tag of the SAPI index!)
3. Changed SAPI views field re-rendered. (These have the cache tags of the entity from the index.)
The Views rows inside the form are cacheable, and so these will be stored in the render cache. But since the indexing did not happen yet, the view will re-use the previous, outdated indexed data. Views row render cache is warmed again.
6. Services starting to be destructed: SAPI indexing starts
4. HtmlRenderer::prepare() finishes.
7. SAPI list tag is invalidated after indexing.
This means that on the next request, every renderable having the index's cache tag will be re-rendered again. But since the Views rows inside the VBO SAPI index view only have the entity's cache tags, they won't be re-rendered until a full (render) cache rebuild!
5. HtmlRenderer::render() finishes.
6. Response sent back to client.
7. Kernel is terminated
8. Kernel is terminated
If we refresh the SAPI VBO views page after these above:
In my opinion:
Right now, users see the same (outdated) views rows on the VBO views form page until the render caches are rebuilt.
I agree that the problem is about corrupted / wrong cache metadata; but in my honest opinion, we should solve the problem the other way around: every SAPI index-based Views row must also include the cache tag of the index the displayed data belongs to – because that is the source of the data we display in SAPI index based views!
SearchApiQuery seems to be fine: in our case, we put our entities into different indexes, depending on the state of the entity (we have separate indexes for "published" and "unpublished" entities). If the action moves an item into the other index, the corresponding row gets removed from the view real-time.
What we did:We simply added the corresponding
search_api_list:<index-id>cache tag to the cache tags of the SAPI views row. After doing this, things started to work logically (for me): after performing the VBO action, the first response (still) returned outdated data; but after a page reload, I've seen the right results.Of course, in our case, we were able* to (and we still have to) work around things since we want to display the right results to our users even after the form submission; But I'm sure that the solution to this problem (or at least one part of the solution) is to add the index cache tag to the Views rows too.
With the additional cache tag, the "backtrace" after form submission does not change:
Thread A
Thread B
1. Request arrives
2. Entity is saved
Its cache tags are invalidated, and the entity is scheduled to be re-indexed.
3. HtmlRenderer starts
4. HtmlRenderer results in EnforcedResponseException
5. LocalRedirectResponse is sent to the client browser, resulting in a concurrent request (and another PHP thread)
1. Request arrives
2. HtmlRenderer starts
3. Changed SAPI views field re-rendered
6. Services starting to be destructed: SAPI indexing starts
4. HtmlRenderer::prepare() finishes
7. SAPI list tag invalidated after indexing
5. HtmlRenderer::render() finishes
6. Response sent back to client
7. Kernel is terminated
8. Kernel is terminated
But after reloading the VBO page, we see a bit more things to happen: the corresponding Views row gets re-rendered too!
-----
What do you think about my fix?
Comment #11
drunken monkeyThanks a lot for your detailed analysis, @huzooka!
What I still don’t understand, though, is how/why the indexed data would affect how individual result rows are rendered?
Are you using Solr and have you configured it to retrieve the results field data from the server – is that the reason? Then I still don’t understand why this fails in the tests, to be honest. Seems I still don’t understand this problem fully, but mostly your description makes a lot of sense and really helps, so thanks again.
If this is about using the fields returned from the server, then maybe we should just add the index’s list cache tag when adding such fields to the result row, not just do that always. With your solution, it would seem like this might unnecessarily impact performance for lots of other users.
(It would, of course, be even better to have specific cache tags for individual search items getting indexed, to only place those on the result rows, but that might be a bit overkill for this issue. Would be even more accurate, though, I think.)
In any case, one big extra-thanks for providing a regression test, that’s really awesome!
Feedback and, especially, manual tests from others would of course also be very much appreciated, even if we’re still unclear on the details.
Comment #12
huzooka@drunken monkey, Yeah, you're totally right! I think I got so lost in the details that I lost focus too!
Yes, we had this issue with the (SAPI) fields data fetched from the SAPI index. And your conclusion in regards of adding the cache tag to search API field plugins instead of adding it as the row's cache tag makes a lot more sense.
Moving to NW because of #11.
Comment #13
drunken monkeyThen maybe something like this? (Or is there a better way for actually adding the cache tag? Seems there is no way to do it directly in
SearchApiQuery::addResults().)Comment #15
prudloff commentedI feel like #9 is reporting a different issue than ours.
We don't return fields from Solr and our problem is not about adding cache tags to those fields, but about adding returned node cache tags to the view.
Comment #16
drunken monkeyThen maybe a combination of the two?
Comment #18
drunken monkeyThe test fails mostly look like they’d be expected now, so I’m adapting the tests.
More importantly, though, it seems like we are now accidentally half-way to fixing #3414725: Add a third Views cache plugin (tag- and time-based): we were adding the individual nodes’ cache tags, but still not the search index list cache tag.
As discussed in the other issue, it seems I have always misunderstood the purpose of the time-based cache plugin: it doesn’t invalidate the cache less often than the tag-based one (only based on the time, not on tags) but more often (based on time and tags). Therefore, our current implementation seems incorrect.
I have therefore now corrected our time-based cache plugin to also include the search index list cache tag.
However, since this does change behavior that no-one has complained about in over six years (I think?), I’m not entirely sure we should really change it. Instead, as suggested in the other issue, we could also provide a new cache plugin with this functionality. The key question is what users expect – whether they are really happy with the current behavior (time-based cache only invalidating based on time) or whether they just never noticed it and maybe (if they are savvy enough) just automatically assumed it would work like Core’s time-based cache plugin.
Feedback on this very much welcome!
What I also wondered was whether this change would add unnecessary cache tags to views that don’t load any entities. However, looking at
SearchApiQuery::getAllEntities()it seems pretty clear that only cache tags of entities that were actually loaded will be included, so this should work as expected.PS: I asked for feedback on Slack.
Comment #19
ekes commentedReplying from Slack:
I for sure can think of a few sites, date based things, where it would be helpful if the option was there to invalidate the cache if the list is updated and 'once a day' (or whatever fixed time period).
Comment #20
mkalkbrennerThe initial issue description is about Solr.
It is important to understand that any drupal internal tag based cache invalidation isn't suitable for an "external" system, where you don't have control over when the data gets indexed and when it will be delivered as result (commit strategy, invalidation of searches, etc). This is the case for Solr, Elastic, file systems (especially NFS), Redis, ...
To be concrete, after a node is edited, you would need to track when the search backend delivers that change before you invalidate the cache.
I wrote some lines about that in Search API Solr's README:
Comment #21
drunken monkeyThanks a lot again for weighing in here, Markus!
Then it’s settled, time-based caching needs to stay as-is and we’d have to add a third cache plugin to replicate the behavior of Core’s built-in “Time-based” cache plugin (which is really tag- and time-based).
The attached patch revision changes the behavior and tests accordingly. I think in
SearchApiTimeCache::getCacheTags()instead of calling the parent and then removing some of the tags from there it makes more sense to just specifically return the two cache tags we want to include here.Please test/review.
@ekes: In this case, please go to #3414725: Add a third Views cache plugin (tag- and time-based) and declare your support there. Seems it’s a separate issue then, since we’ll leave the existing time-based caching as-is.
Comment #22
drunken monkeyWould really appreciate some tests/reviews of this before merging, as I’m loath to change anything regarding caching and/or Views that wasn’t vetted by others.
Comment #23
prudloff commentedNow that #3190024: Problem with test dependencies when testing issue forks is fixed, I updated the MR with the latest patch from comment 22 in order to make this easier to review.
Comment #24
v.dovhaliuk commentedSince using MR as a patch is a security issue, the patch file has been provided to be compatible with version
1.38.Comment #25
arousseau commentedI was able to reproduce this issue on a fresh install of search_api 1.38.0 and search_api_solr 4.3.8, using the "Search API (tag-based)" and "Search API (time and tag-based)" caching methods.
MR 63 applies cleanly and fixes the issue (the view does not display stale results anymore).
The fix looks simple enough and works.
Comment #26
drunken monkey@arousseau: Thanks a lot for your feedback, good to hear this works for you! And also very important to know that this problem even exists anymore in the latest version of the module.
@prudloff: Thanks for that! So does the MR (still) work for you, did you try it?
@v.dovhaliuk: Can you also confirm that the MR works for you?
Just doing admin work on the issue without actually saying that the current patch/MR works for you doesn’t really help me move this along to finally merge it and fix the issue for everyone. As said, for issues that combine Views and caching I’m very reluctant to merge anything since a lot of changes in the past have just led to more issues.
Would anyone else here be able to test/review?
Comment #27
prudloff commentedWe are using the patch on several projects and it seems to work correctly.
Comment #28
drunken monkeyThanks for confirming!
I’m gonna leave this open another week in case anyone else wants to test and provide feedback, and will then merge the MR unless anyone objects.
Comment #29
mxr576@drunken monkey I would suggested merging this MR and then other Views caching related issues - if they still exits - could branch of from a new baseline.
Comment #30
drunken monkey@mxr576: Thanks for bumping this, seems this slipped through the cracks.
However, on reviewing the MR, do we maybe also need to make a modification to
SearchApiTimeTagCache, which was added in the meantime (see #3414725: Add a third Views cache plugin (tag- and time-based))?Other than that, I agree, this is ready to be merged.
Comment #31
lamp5The rebase is required. I have no access to make MR push.
Comment #32
drunken monkeyRebased and added test coverage for the new cache plugin as well.
I think this is good to go, but would be great to get one last confirmation before merging.
Comment #33
adebruin commentedA patch of the latest state of the MR for safe usage with composer patches
We have also tested the MR and it works for us. We tested on views with only tag based cache and without row cache.
Comment #34
drunken monkeyGood to hear, thanks for providing feedback!
Merged.
Thanks again, everyone!