From 9c9776cd2d1ffbee384023b6af7f077c9fd7e4ad Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Thu, 25 Jun 2026 10:18:06 -0700 Subject: [PATCH 1/5] Comments: Add wp_update_comment_counts() to recalculate stored counts. wp_update_comment_count_now() only refreshes a post's stored comment_count when that post's comments change, so an existing count can become stale after the set of excluded comment types changes (for example when a plugin registers a type that opts out of default listings via default_excluded_comment_types). Registration runs on every request and is not a state transition, so there is no safe automatic trigger; the established core pattern for this is an explicit recount, like flush_rewrite_rules() for rewrite rules. Add a bulk recount helper that recomputes one or more posts' counts through wp_update_comment_count_now(), so it honors the same exclusion filter. A plugin that changes the excluded set calls it once, typically on activation. See #35214, #65537. --- src/wp-includes/comment.php | 49 ++++++ .../tests/comment/wpUpdateCommentCounts.php | 143 ++++++++++++++++++ 2 files changed, 192 insertions(+) create mode 100644 tests/phpunit/tests/comment/wpUpdateCommentCounts.php diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index acf634e2bab02..6819de6402427 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -2926,6 +2926,55 @@ function wp_update_comment_count_now( $post_id ) { return true; } +/** + * Recalculates the stored comment count for one or more posts. + * + * Each post's `comment_count` is recomputed with wp_update_comment_count_now(), + * so the result honors the {@see 'default_excluded_comment_types'} filter. + * + * This is the recount counterpart to wp_update_comment_count(): the stored count + * is only refreshed for a post when its comments change, so an existing count can + * become stale after the set of excluded comment types changes (for example when + * a plugin registers a type that opts out of default listings). A plugin that + * changes that set should call this once, typically from its activation routine, + * the same way rewrite rules are flushed with flush_rewrite_rules(). + * + * Recalculating every post is proportional to the number of posts that have + * comments and can be expensive on large sites. Pass a specific list of post IDs + * to limit the work, or run it from a maintenance context such as WP-CLI. + * + * @since 7.1.0 + * + * @global wpdb $wpdb WordPress database abstraction object. + * + * @param int[]|int|null $post_ids Optional. Post ID or array of post IDs to recalculate. + * Default null, which recalculates every post that has + * at least one comment. + * @return int Number of posts whose comment count was recalculated. + */ +function wp_update_comment_counts( $post_ids = null ) { + global $wpdb; + + if ( null === $post_ids ) { + $post_ids = $wpdb->get_col( "SELECT DISTINCT comment_post_ID FROM $wpdb->comments" ); + } + + $post_ids = array_unique( array_filter( array_map( 'absint', (array) $post_ids ) ) ); + + if ( empty( $post_ids ) ) { + return 0; + } + + $recalculated = 0; + foreach ( $post_ids as $post_id ) { + if ( wp_update_comment_count_now( $post_id ) ) { + ++$recalculated; + } + } + + return $recalculated; +} + // // Ping and trackback functions. // diff --git a/tests/phpunit/tests/comment/wpUpdateCommentCounts.php b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php new file mode 100644 index 0000000000000..6ca08be44125f --- /dev/null +++ b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php @@ -0,0 +1,143 @@ +update( $wpdb->posts, array( 'comment_count' => $count ), array( 'ID' => $post_id ) ); + clean_post_cache( $post_id ); + } + + /** + * @ticket 65537 + */ + public function test_returns_zero_when_there_is_nothing_to_recalculate() { + $this->assertSame( 0, wp_update_comment_counts( array() ) ); + $this->assertSame( 0, wp_update_comment_counts( 0 ) ); + } + + /** + * @ticket 65537 + */ + public function test_recalculates_only_the_given_posts() { + $post_a = self::factory()->post->create(); + $post_b = self::factory()->post->create(); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_a, + 'comment_approved' => 1, + ) + ); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_b, + 'comment_approved' => 1, + ) + ); + + // Corrupt both stored counts, then recalculate only the first post. + $this->set_stored_count( $post_a, 99 ); + $this->set_stored_count( $post_b, 99 ); + + $recalculated = wp_update_comment_counts( $post_a ); + + $this->assertSame( 1, $recalculated ); + $this->assertSame( '1', get_comments_number( $post_a ) ); + $this->assertSame( '99', get_comments_number( $post_b ) ); + } + + /** + * @ticket 65537 + */ + public function test_deduplicates_repeated_post_ids() { + $post_id = self::factory()->post->create(); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + $this->set_stored_count( $post_id, 99 ); + + $recalculated = wp_update_comment_counts( array( $post_id, $post_id ) ); + + $this->assertSame( 1, $recalculated ); + $this->assertSame( '1', get_comments_number( $post_id ) ); + } + + /** + * @ticket 65537 + */ + public function test_null_recalculates_all_posts_with_comments() { + $post_a = self::factory()->post->create(); + $post_b = self::factory()->post->create(); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_a, + 'comment_approved' => 1, + ) + ); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_b, + 'comment_approved' => 1, + ) + ); + $this->set_stored_count( $post_a, 99 ); + $this->set_stored_count( $post_b, 99 ); + + wp_update_comment_counts(); + + $this->assertSame( '1', get_comments_number( $post_a ) ); + $this->assertSame( '1', get_comments_number( $post_b ) ); + } + + /** + * The headline scenario: a type that joins the excluded set after comments + * already exist must not keep inflating a previously stored count. + * + * @ticket 65537 + */ + public function test_recalculation_drops_a_newly_excluded_type() { + $post_id = self::factory()->post->create(); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_type' => 'review', + 'comment_approved' => 1, + ) + ); + + // Baseline: 'review' is counted, so the stored count is 1. + wp_update_comment_count_now( $post_id ); + $this->assertSame( '1', get_comments_number( $post_id ) ); + + // A plugin now excludes 'review'. The stored count stays stale until a recount. + $filter = static function ( $types ) { + $types[] = 'review'; + return $types; + }; + add_filter( 'default_excluded_comment_types', $filter ); + + $this->assertSame( '1', get_comments_number( $post_id ) ); + + $recalculated = wp_update_comment_counts( $post_id ); + + remove_filter( 'default_excluded_comment_types', $filter ); + + $this->assertSame( 1, $recalculated ); + $this->assertSame( '0', get_comments_number( $post_id ) ); + } +} From 5f41b0896121636c2e12a59f86bc1ee044291dab Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Sat, 11 Jul 2026 11:01:46 -0700 Subject: [PATCH 2/5] Comments: Harden wp_update_comment_counts() for production-scale use. - Bump the comment 'last_changed' cache key: comment query results are salted only by that key, so on a persistent object cache the activation-time flow (register the exclusion filter, then recount) would otherwise keep serving results cached under the previous excluded set. - Include posts with a nonzero stored count but no remaining comment rows in the null path: SELECT DISTINCT on the comments table cannot see a post whose rows were all deleted while its stored count is stale. - Iterate the null path in keyset batches of 1000 instead of materializing every post ID in memory. - Skip negative IDs instead of silently recounting their absolute value, and document the per-post side effects (edit_post hooks fire for cache purgers and indexers) alongside the cache-invalidation behavior. - Tests: null-path return value, invalid/negative/nonexistent IDs, stale-count reset for commentless posts on both paths, and the last_changed bump. --- src/wp-includes/comment.php | 78 +++++++++++++++---- .../tests/comment/wpUpdateCommentCounts.php | 67 +++++++++++++++- 2 files changed, 129 insertions(+), 16 deletions(-) diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index bcc5491ac2800..d18da94b70883 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -2997,16 +2997,22 @@ function wp_update_comment_count_now( $post_id ) { * Each post's `comment_count` is recomputed with wp_update_comment_count_now(), * so the result honors the {@see 'default_excluded_comment_types'} filter. * - * This is the recount counterpart to wp_update_comment_count(): the stored count + * This is the bulk counterpart to wp_update_comment_count(): the stored count * is only refreshed for a post when its comments change, so an existing count can * become stale after the set of excluded comment types changes (for example when * a plugin registers a type that opts out of default listings). A plugin that * changes that set should call this once, typically from its activation routine, * the same way rewrite rules are flushed with flush_rewrite_rules(). * + * Cached comment query results are salted only by the comment `last_changed` + * key, so it is bumped here: on a persistent object cache, queries cached before + * the excluded set changed would otherwise keep serving the old results. + * * Recalculating every post is proportional to the number of posts that have - * comments and can be expensive on large sites. Pass a specific list of post IDs - * to limit the work, or run it from a maintenance context such as WP-CLI. + * comments and can be expensive on large sites. Each recalculation also fires + * the usual post-update hooks (`wp_update_comment_count`, `edit_post`), so cache + * purgers and search indexers run once per post. Pass a specific list of post + * IDs to limit the work, or run it from a maintenance context such as WP-CLI. * * @since 7.1.0 * @@ -3014,29 +3020,71 @@ function wp_update_comment_count_now( $post_id ) { * * @param int[]|int|null $post_ids Optional. Post ID or array of post IDs to recalculate. * Default null, which recalculates every post that has - * at least one comment. + * at least one comment or a nonzero stored count. * @return int Number of posts whose comment count was recalculated. */ function wp_update_comment_counts( $post_ids = null ) { global $wpdb; - if ( null === $post_ids ) { - $post_ids = $wpdb->get_col( "SELECT DISTINCT comment_post_ID FROM $wpdb->comments" ); - } + /* + * Invalidate cached comment query results: they may reflect the previous + * set of default-excluded comment types. + */ + wp_cache_set_last_changed( 'comment' ); - $post_ids = array_unique( array_filter( array_map( 'absint', (array) $post_ids ) ) ); + $recalculated = 0; - if ( empty( $post_ids ) ) { - return 0; - } + if ( null !== $post_ids ) { + $post_ids = array_map( 'intval', (array) $post_ids ); + $post_ids = array_unique( + array_filter( + $post_ids, + static function ( $post_id ) { + return $post_id > 0; + } + ) + ); - $recalculated = 0; - foreach ( $post_ids as $post_id ) { - if ( wp_update_comment_count_now( $post_id ) ) { - ++$recalculated; + foreach ( $post_ids as $post_id ) { + if ( wp_update_comment_count_now( $post_id ) ) { + ++$recalculated; + } } + + return $recalculated; } + /* + * Visit every post that has at least one comment row, plus every post with + * a nonzero stored count (whose comment rows may all have been deleted), in + * keyset batches so the full ID list is never materialized in memory. + */ + $batch_size = 1000; + $last_post_id = 0; + + do { + $batch = $wpdb->get_col( + $wpdb->prepare( + "( SELECT DISTINCT comment_post_ID AS post_id FROM {$wpdb->comments} WHERE comment_post_ID > %d ) + UNION + ( SELECT ID AS post_id FROM {$wpdb->posts} WHERE comment_count <> 0 AND ID > %d ) + ORDER BY post_id + LIMIT %d", + $last_post_id, + $last_post_id, + $batch_size + ) + ); + + foreach ( $batch as $post_id ) { + if ( wp_update_comment_count_now( (int) $post_id ) ) { + ++$recalculated; + } + } + + $last_post_id = $batch ? (int) end( $batch ) : 0; + } while ( count( $batch ) === $batch_size ); + return $recalculated; } diff --git a/tests/phpunit/tests/comment/wpUpdateCommentCounts.php b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php index 6ca08be44125f..1aeac19620b14 100644 --- a/tests/phpunit/tests/comment/wpUpdateCommentCounts.php +++ b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php @@ -98,12 +98,77 @@ public function test_null_recalculates_all_posts_with_comments() { $this->set_stored_count( $post_a, 99 ); $this->set_stored_count( $post_b, 99 ); - wp_update_comment_counts(); + $recalculated = wp_update_comment_counts(); + $this->assertSame( 2, $recalculated ); $this->assertSame( '1', get_comments_number( $post_a ) ); $this->assertSame( '1', get_comments_number( $post_b ) ); } + /** + * Nonexistent, zero, and negative post IDs are skipped, not recounted. + * + * @ticket 65537 + */ + public function test_invalid_post_ids_are_skipped() { + $post_id = self::factory()->post->create(); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + $this->set_stored_count( $post_id, 99 ); + + // A negative ID must not be silently coerced into a valid one. + $this->assertSame( 0, wp_update_comment_counts( array( -$post_id, 0, PHP_INT_MAX ) ) ); + $this->assertSame( '99', get_comments_number( $post_id ), 'The negated ID should not recount the positive post.' ); + } + + /** + * An explicit ID for a post with no remaining comment rows forces a stale + * stored count back to zero. + * + * @ticket 65537 + */ + public function test_explicit_id_resets_stale_count_on_commentless_post() { + $post_id = self::factory()->post->create(); + $this->set_stored_count( $post_id, 5 ); + + $this->assertSame( 1, wp_update_comment_counts( $post_id ) ); + $this->assertSame( '0', get_comments_number( $post_id ) ); + } + + /** + * The null path also visits posts with a nonzero stored count but no + * remaining comment rows, which the comments table alone cannot reveal. + * + * @ticket 65537 + */ + public function test_null_path_resets_stale_count_on_commentless_post() { + $post_id = self::factory()->post->create(); + $this->set_stored_count( $post_id, 5 ); + + $recalculated = wp_update_comment_counts(); + + $this->assertSame( 1, $recalculated ); + $this->assertSame( '0', get_comments_number( $post_id ) ); + } + + /** + * Recounting bumps the comment last_changed key so cached query results + * from before the excluded set changed are invalidated. + * + * @ticket 65537 + */ + public function test_recount_invalidates_comment_query_cache() { + $before = wp_cache_get_last_changed( 'comment' ); + + wp_update_comment_counts( array() ); + + $this->assertNotSame( $before, wp_cache_get_last_changed( 'comment' ) ); + } + /** * The headline scenario: a type that joins the excluded set after comments * already exist must not keep inflating a previously stored count. From 491ecc31d4d7a9f4f7d6cf6c0ad756cf3b3f7512 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 16:43:46 -0700 Subject: [PATCH 3/5] Comments: Bound each arm of the recount batch query. The keyset loop was correct but its query was not batched in any useful sense. MySQL cannot push an outer ORDER BY ... LIMIT into a parenthesized UNION arm, so every iteration materialized all remaining rows of both arms into a temp table before taking its thousand - and since comment_count is unindexed, the posts arm scanned the whole remaining table each time. A recount of N commented posts cost roughly N squared over the batch size, which defeats the batching that is the whole point of the null path. Give each arm its own ORDER BY and LIMIT. Keyset semantics are unchanged: the first N rows of the union of two ascending sets are always among the first N of each, so the batch is the same batch, read with a bounded walk of each index. The batch size becomes filterable along the way, which also makes it testable across a boundary - including a post that has both comments and a stale count, so it turns up in both arms. See #65537. --- src/wp-includes/comment.php | 30 ++++++++++++-- .../tests/comment/wpUpdateCommentCounts.php | 39 +++++++++++++++++++ 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index f591beedee7e7..ba3efb90167cc 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -3391,24 +3391,48 @@ static function ( $post_id ) { return $recalculated; } + /** + * Filters how many posts wp_update_comment_counts() recalculates per batch. + * + * Lower it to spread a large recount over shorter queries, or raise it to trade + * memory for fewer round trips. + * + * @since 7.1.0 + * + * @param int $batch_size Number of posts to look up per query. Default 1000. + */ + $batch_size = (int) apply_filters( 'wp_update_comment_counts_batch_size', 1000 ); + + if ( $batch_size < 1 ) { + $batch_size = 1; + } + /* * Visit every post that has at least one comment row, plus every post with * a nonzero stored count (whose comment rows may all have been deleted), in * keyset batches so the full ID list is never materialized in memory. + * + * Each arm carries its own ORDER BY and LIMIT. MySQL cannot push the outer + * ones into a parenthesized UNION arm, so without them every iteration + * materializes all remaining rows into a temp table before taking a batch, + * and the posts arm scans the whole remaining table because comment_count is + * unindexed. Limiting the arms is safe: the first N rows of the union of two + * ascending sets are always among the first N of each. */ - $batch_size = 1000; $last_post_id = 0; do { $batch = $wpdb->get_col( $wpdb->prepare( - "( SELECT DISTINCT comment_post_ID AS post_id FROM {$wpdb->comments} WHERE comment_post_ID > %d ) + "( SELECT DISTINCT comment_post_ID AS post_id FROM {$wpdb->comments} WHERE comment_post_ID > %d ORDER BY comment_post_ID LIMIT %d ) UNION - ( SELECT ID AS post_id FROM {$wpdb->posts} WHERE comment_count <> 0 AND ID > %d ) + ( SELECT ID AS post_id FROM {$wpdb->posts} WHERE comment_count <> 0 AND ID > %d ORDER BY ID LIMIT %d ) ORDER BY post_id LIMIT %d", $last_post_id, + $batch_size, $last_post_id, + $batch_size, $batch_size ) ); diff --git a/tests/phpunit/tests/comment/wpUpdateCommentCounts.php b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php index 1aeac19620b14..c022e92f2bf34 100644 --- a/tests/phpunit/tests/comment/wpUpdateCommentCounts.php +++ b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php @@ -169,6 +169,45 @@ public function test_recount_invalidates_comment_query_cache() { $this->assertNotSame( $before, wp_cache_get_last_changed( 'comment' ) ); } + /** + * The keyset loop has to advance across batches, and a post that has both comments + * and a stale nonzero count appears in both arms of the union. + * + * @ticket 65537 + */ + public function test_recount_crosses_batch_boundaries() { + $post_ids = self::factory()->post->create_many( 5 ); + + foreach ( $post_ids as $post_id ) { + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + + // Stale count, so each post is returned by both union arms. + $this->set_stored_count( $post_id, 7 ); + } + + add_filter( + 'wp_update_comment_counts_batch_size', + static function () { + return 2; + } + ); + + $this->assertSame( + 5, + wp_update_comment_counts(), + 'Every post should be visited exactly once across the batches.' + ); + + foreach ( $post_ids as $post_id ) { + $this->assertSame( '1', get_comments_number( $post_id ) ); + } + } + /** * The headline scenario: a type that joins the excluded set after comments * already exist must not keep inflating a previously stored count. From 2f77f06fbbb69a7d1a552a476543412e0df31d9e Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 16:45:09 -0700 Subject: [PATCH 4/5] Comments: Stop no-op recounts from flushing every cached comment query. The comment last_changed key was bumped first thing, before the post IDs were even validated, so wp_update_comment_counts( array() ) or a list of nothing but invalid IDs invalidated every cached comment query on the site while recounting nothing at all. Bump it once there is at least one post to visit. The existing cache test relied on that no-op bump, so it now recounts a real post, and the no-op cases get their own test asserting the key is left alone. See #65537. --- src/wp-includes/comment.php | 14 ++++--- .../tests/comment/wpUpdateCommentCounts.php | 38 ++++++++++++++++++- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index ba3efb90167cc..5a7aaf2baf9bf 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -3363,12 +3363,6 @@ function wp_update_comment_count_now( $post_id ) { function wp_update_comment_counts( $post_ids = null ) { global $wpdb; - /* - * Invalidate cached comment query results: they may reflect the previous - * set of default-excluded comment types. - */ - wp_cache_set_last_changed( 'comment' ); - $recalculated = 0; if ( null !== $post_ids ) { @@ -3382,6 +3376,12 @@ static function ( $post_id ) { ) ); + if ( ! $post_ids ) { + return 0; + } + + wp_cache_set_last_changed( 'comment' ); + foreach ( $post_ids as $post_id ) { if ( wp_update_comment_count_now( $post_id ) ) { ++$recalculated; @@ -3391,6 +3391,8 @@ static function ( $post_id ) { return $recalculated; } + wp_cache_set_last_changed( 'comment' ); + /** * Filters how many posts wp_update_comment_counts() recalculates per batch. * diff --git a/tests/phpunit/tests/comment/wpUpdateCommentCounts.php b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php index c022e92f2bf34..5aad45e1cbbff 100644 --- a/tests/phpunit/tests/comment/wpUpdateCommentCounts.php +++ b/tests/phpunit/tests/comment/wpUpdateCommentCounts.php @@ -162,13 +162,49 @@ public function test_null_path_resets_stale_count_on_commentless_post() { * @ticket 65537 */ public function test_recount_invalidates_comment_query_cache() { + $post_id = self::factory()->post->create(); + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_approved' => 1, + ) + ); + $before = wp_cache_get_last_changed( 'comment' ); - wp_update_comment_counts( array() ); + wp_update_comment_counts( $post_id ); $this->assertNotSame( $before, wp_cache_get_last_changed( 'comment' ) ); } + /** + * A call with nothing to recount should not flush every cached comment query. + * + * @ticket 65537 + * + * @dataProvider data_no_op_recount_arguments + * + * @param int[] $post_ids Post IDs to pass to wp_update_comment_counts(). + */ + public function test_no_op_recount_leaves_the_comment_query_cache_alone( array $post_ids ) { + $before = wp_cache_get_last_changed( 'comment' ); + + $this->assertSame( 0, wp_update_comment_counts( $post_ids ) ); + $this->assertSame( $before, wp_cache_get_last_changed( 'comment' ) ); + } + + /** + * Data provider for test_no_op_recount_leaves_the_comment_query_cache_alone(). + * + * @return array> + */ + public function data_no_op_recount_arguments(): array { + return array( + 'empty list' => array( array() ), + 'only invalid IDs' => array( array( 0, -1 ) ), + ); + } + /** * The keyset loop has to advance across batches, and a post that has both comments * and a stale nonzero count appears in both arms of the union. From 5a87337bf297e2e5f163f251d31d03998ab91e84 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 12 Aug 2026 16:45:55 -0700 Subject: [PATCH 5/5] Comments: Say what wp_update_comment_counts() actually promises. The docblock recommended calling this from a plugin activation routine while also warning it can be expensive, which is contradictory advice: activation runs in a normal web request, and a full recount on a large site will hit max_execution_time partway through. Point at wp_schedule_single_event() or WP-CLI for that case, and say plainly that stopping partway is safe - posts visited before the stop are correct and the operation is idempotent. Three more things the function does that a caller could only learn by reading it: counts are written immediately rather than joining the wp_defer_comment_counting() queue, post IDs that do not exist are skipped and left out of the return value, and there is no capability check, so anything exposing this to a request has to add its own. See #65537. --- src/wp-includes/comment.php | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/src/wp-includes/comment.php b/src/wp-includes/comment.php index 5a7aaf2baf9bf..272b5d90cea36 100644 --- a/src/wp-includes/comment.php +++ b/src/wp-includes/comment.php @@ -3338,18 +3338,29 @@ function wp_update_comment_count_now( $post_id ) { * is only refreshed for a post when its comments change, so an existing count can * become stale after the set of excluded comment types changes (for example when * a plugin registers a type that opts out of default listings). A plugin that - * changes that set should call this once, typically from its activation routine, - * the same way rewrite rules are flushed with flush_rewrite_rules(). + * changes that set should call this once, the same way rewrite rules are flushed + * with flush_rewrite_rules(). * - * Cached comment query results are salted only by the comment `last_changed` - * key, so it is bumped here: on a persistent object cache, queries cached before - * the excluded set changed would otherwise keep serving the old results. + * Cached comment query results are bumped through the comment `last_changed` key + * when there is at least one post to visit, so that queries cached alongside the + * previous counts are not served afterwards. * * Recalculating every post is proportional to the number of posts that have * comments and can be expensive on large sites. Each recalculation also fires * the usual post-update hooks (`wp_update_comment_count`, `edit_post`), so cache - * purgers and search indexers run once per post. Pass a specific list of post - * IDs to limit the work, or run it from a maintenance context such as WP-CLI. + * purgers and search indexers run once per post. Pass a specific list of post IDs + * to limit the work. A full recount on a large site does not belong in a plugin + * activation routine, which runs in a normal web request and will hit + * `max_execution_time` partway through: schedule it with wp_schedule_single_event() + * or run it from WP-CLI instead. Stopping partway is safe - every post visited + * before the stop has a correct count, and the operation is idempotent, so a + * re-run simply redoes the earlier posts. + * + * Counts are written immediately: this does not participate in + * wp_defer_comment_counting(). + * + * There is no capability check, matching the rest of this family. Anything that + * exposes it to a request has to perform its own capability and nonce checks. * * @since 7.1.0 * @@ -3358,7 +3369,8 @@ function wp_update_comment_count_now( $post_id ) { * @param int[]|int|null $post_ids Optional. Post ID or array of post IDs to recalculate. * Default null, which recalculates every post that has * at least one comment or a nonzero stored count. - * @return int Number of posts whose comment count was recalculated. + * @return int Number of posts whose comment count was recalculated. Post IDs that do not + * exist are skipped and are not included in the count. */ function wp_update_comment_counts( $post_ids = null ) { global $wpdb;