Confirmed Inefficient reaction cache rebuild on username change

mattrogowski

Well-known member
Affected version
2.2.16
On a large forum with some 22 million posts and very high reaction usage, coupled with high volume of username changes (mostly from a high volume of account deletions), the process to update the reaction user cache on content is painfully inefficient and leads to frequent table locks that then cause performance and stability issues on the site.

We have an addon that allows users to delete their own accounts, which are queued and fed in in small batches (3 at a time one an hour) so as not to overwhelm the database. However even with queuing 3 at a time, we were seeing table locks every time it ran.

The query that seems to always be causing table locks as part of UserDeleteCleanUp is this from XF\Reaction\AbstractHandler::updateRecentCacheForUserChange:

SQL:
UPDATE (
  SELECT content_id FROM xf_reaction_content
  WHERE content_type = ?
  AND reaction_user_id = ?
) AS temp
INNER JOIN xf_post AS reaction_table ON (reaction_table.`post_id` = temp.content_id)
SET reaction_table.`reaction_users` = REPLACE(reaction_table.`reaction_users`, ?, ?)
WHERE reaction_table.`reaction_users` LIKE ?

This was recently updated following this report from @Xon: https://xenforo.com/community/threa...andler-updaterecentcacheforuserchange.217245/

However, this query might work on smaller forums but on large ones, probably the number of rows returned in the join coupled with the seemingly expensive LIKE condition mean that it frequently causes table locks. We had 650+ instances of this in one week alone and it causes slowdowns and errors that causes a negative experience for users.

Would it not be better to not try and do this in one query with a sub-query and finding rows where reaction_users contains a string, and instead update based on content ID? I think it would be more efficient to query the content that needs updating and target the rows by ID to update. Even if there were several thousand IDs, chunking them into batches and running the query multiple times would be more efficient.

As a proof of concept I changed it to this:

PHP:
$contentIds = \XF::db()->fetchAllColumn("
    SELECT content_id FROM xf_reaction_content
    WHERE content_type = ?
    AND reaction_user_id = ?
", [$this->contentType, $newUserId], 'content_id');

foreach (array_chunk($contentIds, 500) as $ids) {
    $ids = implode(',', $ids);
    \XF::db()->query("
        UPDATE {$table}
        SET `{$recentField}` = REPLACE(`{$recentField}`, ?, ?)
        WHERE `{$primaryKey}` IN ({$ids})
    ", [$oldFind, $newReplace]);
}

I’m not sure you actually need the LIKE on it as it's only selected content IDs we know the user has already reacted to, and now that the WHERE condition is simply based on a subset of content IDs it shouldn’t need to try and limit the query scope any further.

This seems to work as expected, and some 72 hours after making the change, with account deletes being processed at a rate of 5 every 5 minutes instead of 3 every 60 minutes, I have seen exactly 0 table locks logged by this new query. I think it’s a case of more queries being better than fewer if they’re all more efficient.
 
 
It was Kirby who made that report.

I did however report it would be a possible performance issue to update the entire table at once instead of via the job system to incrementally do renames back in XF2.0.0 developer previews.

At the time, the sites I admin'ed had a vastly weaker database so I was much more sensitive to that issue.

The actual rename/cleanup tasks already work via the job system so making it re-enter some of the steps should be doable. Even if they need to be via a different method name to avoid re-executing custom code.
 
It was Kirby who made that report.
Huh, I can’t tell an X from a K apparently lol.

Either way, it’s definitely an inefficient process and needs a rethink one way or another.

Even without an account delete addon that we’re using, users changing their username on very active forums would have a similar impact. We have 1000+ signups a day and tens of thousands of active users so username changes are frequent, and not in our control like the account delete addon is.
 
I’m not sure you actually need the LIKE on it as it's only selected content IDs we know the user has already reacted to, and now that the WHERE condition is simply based on a subset of content IDs it shouldn’t need to try and limit the query scope any further.
The LIKE makes sense with the current code flow:
The rebuild is triggered after the user has been deleted, eg. $newUserId is 0 at this point so the inner query selects reactions by all guests / deleted users - without the LIKE the UPDATE would possibly process many unrelated rows.

A proper fix would probably require to run the rebuild before updating xf_reaction_content.reaction_user_id to 0 so the inner query really only selects content IDs the user had reacted to.

And avoid updating twice anyway as pointed out in https://xenforo.com/community/threa...ce-when-renaming-a-user-upon-deletion.217244/
 
Back
Top Bottom