Problem/Motivation
Issue found in 31d1114a36321403610c8167c6227f91ea9bc637.
When leaving a reply to a comment, any comments that follow the reply in the subsequent page reload will be indented incorrectly. For example, let's say we have three comments on a node.
|- A
|- B
|- C
A user responds to comment A with comment a1. We expect the indenting to look like this:
|- A
|--a1
|- B
|- C
Instead, all comments (B and C in this example) that follow a1 will be indented to the level of a1.
|- A
|-- a1
|-- B
|-- C

Clearing the site cache will bring the indentation back to the expected pattern:
|- A
|--a1
|- B
|- C
Investigation so far
Wim and I looked into this issue and found the following clues:
-
We are missing a closing div on the indented comment, which causes all the following comments to indent as the browser "corrects" the malformed HTML and wraps the following siblings in the unclosed div:
-
If you delete a reply comment (
a1) and then reply to the same comment (essentially recreatea1), the indentation is correct on the subsequent page load. -
We think the problem is somewhere in
comment.module:comment_get_threadandcomment.module:comment_prepare_thread. That's where the inclusion of closing divs is calculated.
Proposed resolution
Investigate further.
User interface changes
None.
API changes
None expected.
Original report by @jessebeach
| Comment | File | Size | Author |
|---|---|---|---|
| #59 | comment_indentation_bug-2254181-59.patch | 12.75 KB | wim leers |
| missing-div.png | 85.06 KB | jessebeach | |
| indented.png | 62.31 KB | jessebeach |
Comments
Comment #1
wim leersI think this might be major, because it leaves the (potentially anonymous) commenter with a very jarring experience.
Comment #2
andypostActually 'divs' are added in
\Drupal\comment\CommentViewBuilder::alterBuildInteresting bug anyway
Comment #3
wim leers#2: sure, but the calculations to determine if and how many closing
</div>s happens incomment_prepare_thread().Comment #4
Anonymous (not verified) commentedI'm not able to reproduce this in a fresh 8.x-dev pull.
Comment #5
blueminds commentedStill an issue. One hint though - if you clear the cache the comments tree is rendered correctly.
Comment #6
blueminds commentedOh, missed the part in the issue summary that the clear cache solves the problem...
I compared the values of $comment->divs and $comment->depth before and after the clear cache and they are identical.
before clear cache:
after clear cache:
Also $comment->div_final before and after clear cache has the same value. So it looks like the problem is not in the comment_prepare_thread().
Comment #7
blueminds commentedI maybe located the problem.
Debugged CommentViewBuilder::alterBuild() and all seems to be built correctly including the closing divs. A closing div is added by the following comment that is supposed to jump out of the indentation. That leads me to an idea that as the following comment has not been updated the cache for it is still valid and therefore the old cached output is used that does not contain the "trailing closing" div. But what confuses me, if it is cached, why afterBuild() then triggers.
Comment #8
andypost@blueminds awesome reseach!
Probably the problem in render cache, so some comments are rendered from cache and some (all parents of the new posted comment).
Maybe comment should clear render cache for all its parents?
Comment #9
blueminds commented@andypost Actually not the parent is the problem, but the following comment in the tree as that is the one that adds the closing div
Comment #10
wim leersIt cannot be caused by the render cache; it *can* be caused by incorrect invalidation, i.e. missing cache tags.
This got me thinking, and sure enough… that's all it was. A super simple patch fixes the problem here.
A comment must not only return its "own" cache tag, but also the commented entity's cache tag, because it depends on that. In fact, it even refers to the commented entity:
Comment #11
wim leersThanks a *lot* to blueminds for debugging this — without his analysis, I wouldn't have thought of this :)
Comment #13
wim leers#10 was a bit too coarse. A comment itself doesn't
Comment #14
berdira node => the commented entity?
Comment #15
jessebeach commentedNice work blueminds!
Comment #16
wim leers#14: ugh, yes, of course. (I was doing this in between other things…)
This comment should be better.
Comment #17
berdirDo we need a test that the indentation is now correct? There might be some existing tests that we can extend?
I was confused by the first patch which added the commented entity manually to getCacheTags(), this should already happen through the referenced entities tag collection, right? That's why the new patch just makes sure that this cache tags also exists on the comment, so that we rebuild all comments if any comment or the commented entity changes? That's kind of sad because it means a lot of re-generating on every point (which means that the by-comment cache is hardly useful at all?)
Comment #18
wim leersIndeed. We already have
CommentThreadingTestwhich tests threading already, but doesn't test the presence of the correct<div class="indented">tags.@blueminds, would you like to work on that? :)
Yes, the first patch was wrong.
Correct.
This is why I also considered just adding a
comment_on_<entity type>:<entity ID>cache tag, which would have the same effect, but would be less confusing. That being said, every rendered comment must also be tagged with the commented entity's cache tag, because if the commented entity changes its URL, then each comment permalink will also need to be changed. Hence the need for what the current patch is doing.Correct & correct.
I think it'd make sense to disable render caching on comments for this reason. Done in this reroll.
Comment #19
blueminds commentedyup, will take a look at tests tonight or tomorrow morning.
Comment #20
catchI think we should only add the cache tag for the commented entity when threading is enabled. Can't see a reason to invalidate the cache when a comment isn't in the thread.
Comment #21
wim leers#20: Indeed.
Comment #22
blueminds commentedHere comes the test.
Comment #24
blueminds commentedComment #25
wim leers@blueminds: Thanks! :) And sorry for having taken so long to provide a review — I lost track of this issue :( Next time, feel free to ping me on Twitter or IRC!
How does this help with ?
Above this point, you correctly incremented everything, to compensate for the new "comment #2" that you added.
But starting here, you forgot to update the comment number in code comments and variable names. This is very confusing.
Also: let's prefix each comment subject with the "#ID" we use in the code comments, that makes it much easier to understand.
Attached is a reroll to make the patch apply to HEAD again. In
Comment,changed to
That's it :)
Comment #26
andypostComment #27
andypostSuppose the patch is good to go, except the reason to disable render cache. Suppose the problem here that comment renders indent but should not.
@Wim seem there should be a follow-up "enable render cache for comments and remove indent from single comment template"....
OTOH there's #1962846: Use field instance name for header of comment list, drop comment-wrapper template so new field template (comment wrapper) would care about comment indent.
It's not clear do we need both changes?
Comment #30
cbr commentedRerolled
Comment #31
wim leersYou're right theoretically. Strange that none of us realized that before. Therefore, I agree with a follow-up in theory. Though having tried to do this just now, I think I know why: it's very tricky. We essentially need to render the comment, render cache it, and THEN add the prefix and suffix. That's not really supported.
Combined with the fact that we almost never render individual comments (they're rendered as part of the commented entity 99% of the time), I don't think this is worthwhile to pursue.
The first is needed for correct cache invalidation (see #10).
The reasons for the second: please see berdir's remarks in #17 and my response in #18.
I think this is ready.
Comment #32
andypostThe issue fixes the bug, RTBC!
I agree that populating the render cache for comments is useless and could be only needed for forum (nonthreaded now)... probably
OTOH there's a log of issues to clean-up render for comments:
#2031883: Markup for: comment.html.twig
#2002094: Improve performance of comment.html.twig
#1920044: Move comment field settings that relate to rendering to formatter options
#1857946: Comment parent template variables are built twice
So let's get this in!
Comment #33
andypostThe issue #2318579: Remove comment_prepare_thread() moves the code to view builder
Comment #34
catchDon't we only need to do this when threading is enabled for the field?
Comment #35
arla commentedRerolled and fixed #34. Discussed with @Berdir whether it is really only with threading enabled that we want to add the cache tags, and found no other case.
We also found out that in order to test the behaviour, you have to use a different user to trigger the cache clear, because the caches for rendered comments are tagged by user. Plus, to disable threading, you need to
drush ceditor equivalent because of a bug in CommentItem.Comment #37
arla commentedNow that we merge in the tags of the entity, there is a fail because they are duplicated for the commented entity (since it in turn merges the tags of the comments). We could change drupal_merge_cache_tags() to eliminate duplicates, but I'm not sure that would be of advantage, and I'm guessing the simplicity of the function is justified. So this patch just makes the test expect the duplication.
Comment #38
wim leersdrupal_merge_cache_tags()already removes duplicates by design (Drupal core stores cache tags using their prefix as a key in an array), so I wonder how that's possible.Could you debug this to find the root cause of this tag duplication problem?
Comment #39
arla commentedDid some debugging with @Berdir. Let me see if I get this straight.
Cache tags can be arrays of values, typically IDs of related entities. Such tags are sometimes expected to have their values as keys, e.g.
array(1 => '1', 3 => '3'). Indrupal_merge_cache_tags()they are, but inEntity::getCacheTag()they are not. This inconsistency is causing the duplication in this case, and might be problematic in other cases as well.More specifically, if the
$tagsargument ofdrupal_merge_cache_tags()has something likearray('entity_test' => array(0 => '1'))and the$otherargument hasarray('entity_test' => array (1 => '1'))then the result will contain the value'1'twice.The attached patch contains a fix for each of the two mentioned functions, as well as a passing version of CommentDefaultFormatterCacheTagsTest. The fixes work separately, i.e. you can apply any one of them to make the test pass. On the other hand, we expect fails from other tests, which expect cache tags to be set in some specific way or other.
To explain the
drupal_merge_cache_tags()fix, this version does not expect arrays in$tagsto already have their values as keys.Let's have some feedback on this and then possibly create a new issue addressing the format of cache tag arrays.
Comment #41
wim leersWow, excellent detective work!
This is why I've been wanting to open an issue to change all cache tags across core, to just be strings. i.e.
node:5instead of['node' => [5 => 5]](or['node' => [5]], which is what HEAD does). Then we can justarray_unique(array_merge($tags, $more_tags))and not worry about the array aspects.Because there's also the problem that if any code does
['foo' => TRUE'], then any other code no longer can do['foo' => [35 => 35]]. We've had core bugs because of that. When using strings, we avoid all that.Thoughts?
Comment #42
arla commentedI'm still quite new to the caching mechanism, but using strings only makes sense as far as I can see. I suppose that the following example (from Cache API doc):
would then be:
Comment #43
wim leersExactly!
Comment #44
wim leersOpened an issue for this: #2340123: Setting cache tags can be tricky: use strings instead of nested arrays to improve DX. Discussed with catch, he's on board with this change.
Comment #45
berdirOk, how do we continue here then? I guess we can go with a minimal fix as we hope it will get simplifed in that issue?
Maybe we can get back to #37, with a @todo on the new issue?
Comment #46
andypostMaybe it's time to fix a cause (wrong cache clean for indented comments) and left @todo
Indenting should be a field level post render callback somehow...
let's move that to other issue with @todo
is this related to bug?
this setting lives on field level but prefer at formatter level.
I see no reason to cache indented comments and remove render cache fr comments, this is a formatter level cache that needs proper cache clean
Comment #47
wim leers#2340123: Setting cache tags can be tricky: use strings instead of nested arrays to improve DX now has a patch! Please help land it by reviewing!
#45: I'm fine with committing #37.
#46:
This is a bit difficult to parse. I don't understand everything, but I do understand you don't want render caching to be disabled on comments. But… comments are never rendered independently (except when replying to a specific comment, which is relatively rare). Rendered comments are already render cached as part of a render cached parent entity (usually a node). So what's the value in keeping render caching for comments?
Comment #48
arla commentedThis patch continues from #37 and adds a @todo issue reference.
Comment #49
wim leersThank you!
Comment #50
catch298-comment long issue thread on Drupal.org, where threading is not enabled.
298 comments are on that thread, they're cached in both the node render cache as a single chunk of HTML, and individually.
Comment 299 is posted.
With render caching of comments, the 298 comments are loaded from render cache. Without, we have to separately render each one.
Same principle applies to a blog or other site that tends to have one or two highly commented articles at a time.
If this is set.
Then why do we also need this?
I'd prefer to keep the render caching and just set the cache tags. It's more cache sets but that has limited impact on the sites that don't need it and could keep the sites that do up.
Comment #51
berdirWondering if we should have an easier way to conditionally disable the render cache per-entity, right now, all we have is the global flag and by-view mode.
But yes, layered caching makes a lot of sense when you don't show threaded comments. Wondering if we can find a relatively easy way to block caching completely under certain conditions, then we could prevent it just for threaded comments?
Comment #52
wim leersThat is an excellent point.
BUT: the indentation wrapper for a comment is also render cached. So we can only do this when threading is disabled.
This is also what Berdir alludes to: .
Comment #53
catch@Wim yes I also said this in #20 ;)
Won't this unset the cache tags as well? Also this needs an explicit comment about the indentation markup being part of the rendered comment.
Comment #55
wim leers#53:
This should be green again.
Comment #56
berdirDid test this manually, turns out that the type safe check breaks things on a real installation because the schema of default_mode currently says boolean, while the constants are TRUE/FALSE.
So we either have to fix the schema, the constants or not do a type safe check. I'm OK with changing the schema, but I guess we could also change the constants? Type safe check is a good idea, although not sure if we should then do it everywhere consistently, which might expose new test fails, making the patch bigger again...
Also, as mentioned on IRC, should we open a follow-up issue to add a kill switch to #cache? It would be easy for someone implementing hook_entity_build_defaults_alter() to re-add a key and then it would be cached, just based on that key ;)
A kill switch is much harder to accidentally overwrite.
(I also think this is more complex than it needs to be, the system just cares about the existence of 'keys', but @WimLeers prefers it to be like this and I don't care that much)
Comment #57
larowlan+1 for changing the schema, allows for other options in contrib that a boolean doesn't
Comment #58
catchFollow-up to add a kill-switch sounds great.
Comment #59
wim leersComment #60
berdirThanks, I tested the same fix locally, worked for me then.
Comment #61
catchCommitted/pushed to 8.0.x, thanks!
Comment #62
catchComment #64
berdirGreat. Opened #2344243: Support a kill flag in $render_array[#cache'].
Comment #65
wim leers#62: yeah, this turned out to not be a "quick fix" after all…