Skip to:
Content

bbPress.org


Ignore:
Timestamp:
09/10/2026 12:35:43 AM (2 weeks ago)
Author:
johnjamesjacoby
Message:

Counts: synchronize metadata across concurrent writes.

Introduce bbp_bump_count_meta() to update existing numeric metadata with bounded compare-and-swap retries while preserving WordPress metadata filters, actions, sanitization, and cache invalidation. Apply atomic differences to forum, topic, reply, ancestor, and user contribution counts.

Reconcile counts, engagements, and voices across status transitions, permanent deletion, author changes and reassignment, moderator move/merge/split operations, and repair recounts. Correct recursive forum totals and reply visibility, and preserve term-backed favorites and subscriptions during engagement rebuilds.

Document the count-hook compatibility changes and new helper arguments, and add regression coverage for single-site and multisite workflows.

In trunk, for 2.7.

Props alex-ye.
Fixes #2233.
Fixes #3678.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/includes/users/functions.php

    r7452 r7467  
    720720                : bbp_get_user_topic_count( $user_id, true );
    721721
    722         $user_topic_count = bbp_number_not_negative( $count + $difference );
     722        $user_topic_count = (int) bbp_number_not_negative( $count + $difference );
    723723
    724724        // Add them up and filter them
    725725        $new_count = (int) apply_filters( 'bbp_bump_user_topic_count', $user_topic_count, $user_id, $difference, $count );
    726726
    727         return bbp_update_user_topic_count( $user_id, $new_count );
     727        // Preserve absolute count filters before using the atomic difference
     728        $difference = ( $new_count === $user_topic_count )
     729                ? $new_count - $count
     730                : false;
     731
     732        return bbp_update_user_topic_count( $user_id, $new_count, $difference );
    728733}
    729734
    … …  
    756761                : bbp_get_user_reply_count( $user_id, true );
    757762
    758         $user_reply_count = bbp_number_not_negative( $count + $difference );
     763        $user_reply_count = (int) bbp_number_not_negative( $count + $difference );
    759764
    760765        // Add them up and filter them
    761766        $new_count = (int) apply_filters( 'bbp_bump_user_reply_count', $user_reply_count, $user_id, $difference, $count );
    762767
    763         return bbp_update_user_reply_count( $user_id, $new_count );
     768        // Preserve absolute count filters before using the atomic difference
     769        $difference = ( $new_count === $user_reply_count )
     770                ? $new_count - $count
     771                : false;
     772
     773        return bbp_update_user_reply_count( $user_id, $new_count, $difference );
     774}
     775
     776/**
     777 * Update user counts when a topic or reply changes authors.
     778 *
     779 * @since 2.6.16
     780 *
     781 * @param int     $post_id     Post ID.
     782 * @param WP_Post $post_after  Post object following the update.
     783 * @param WP_Post $post_before Post object before the update.
     784 */
     785function bbp_update_counts_on_post_author_change( $post_id = 0, $post_after = false, $post_before = false ) {
     786
     787        // Bail if the author or post type did not change as expected
     788        if ( ( $post_after->post_author === $post_before->post_author ) || ( $post_after->post_type !== $post_before->post_type ) ) {
     789                return;
     790        }
     791
     792        // Set topic public membership
     793        if ( bbp_get_topic_post_type() === $post_after->post_type ) {
     794                $public_statuses = bbp_get_public_topic_statuses();
     795                $was_public      = in_array( $post_before->post_status, $public_statuses, true );
     796                $is_public       = in_array( $post_after->post_status,  $public_statuses, true );
     797                $is_topic        = true;
     798
     799        // Set reply public membership
     800        } elseif ( bbp_get_reply_post_type() === $post_after->post_type ) {
     801                $public_statuses = bbp_get_public_reply_statuses();
     802                $was_public      = in_array( $post_before->post_status, $public_statuses, true );
     803                $is_public       = in_array( $post_after->post_status,  $public_statuses, true );
     804                $is_topic        = false;
     805
     806        // Bail if this is not a topic or reply
     807        } else {
     808                return;
     809        }
     810
     811        // The transition callback already handles posts that were not public
     812        if ( ! $was_public ) {
     813                return;
     814        }
     815
     816        // Transfer a public contribution between authors
     817        if ( $is_public ) {
     818                if ( $is_topic ) {
     819                        bbp_bump_user_topic_count( $post_before->post_author, -1 );
     820                        bbp_bump_user_topic_count( $post_after->post_author,   1 );
     821                } else {
     822                        bbp_bump_user_reply_count( $post_before->post_author, -1 );
     823                        bbp_bump_user_reply_count( $post_after->post_author,   1 );
     824                }
     825
     826        // Repair both authors after the transition callback targeted the new author
     827        } else {
     828                foreach ( bbp_get_unique_array_values( array( $post_before->post_author, $post_after->post_author ) ) as $user_id ) {
     829                        if ( $is_topic ) {
     830                                bbp_update_user_topic_count( $user_id, bbp_get_user_topic_count_raw( $user_id ) );
     831                        } else {
     832                                bbp_update_user_reply_count( $user_id, bbp_get_user_reply_count_raw( $user_id ) );
     833                        }
     834                }
     835        }
     836}
     837
     838/**
     839 * Update topic engagements when a topic or reply changes authors.
     840 *
     841 * @since 2.6.16
     842 *
     843 * @param int     $post_id     Post ID.
     844 * @param WP_Post $post_after  Post object following the update.
     845 * @param WP_Post $post_before Post object before the update.
     846 */
     847function bbp_recalculate_engagements_on_post_author_change( $post_id = 0, $post_after = false, $post_before = false ) {
     848
     849        // Bail if the author did not change
     850        if ( $post_after->post_author === $post_before->post_author ) {
     851                return;
     852        }
     853
     854        // Get the topic ID from a topic or reply
     855        if ( bbp_get_topic_post_type() === $post_after->post_type ) {
     856                $topic_id = $post_id;
     857        } elseif ( bbp_get_reply_post_type() === $post_after->post_type ) {
     858                $topic_id = bbp_get_reply_topic_id( $post_id );
     859        } else {
     860                return;
     861        }
     862
     863        // Recalculate engagements and their count
     864        bbp_recalculate_topic_engagements( $topic_id );
     865        bbp_update_topic_voice_count( $topic_id );
     866}
     867
     868/**
     869 * Update counts and engagements when a deleted user's posts are reassigned.
     870 *
     871 * WordPress reassigns post authors directly in the database, bypassing the
     872 * normal post update actions. Record affected topics before that write, then
     873 * repair the replacement user's counts and those topics after it completes.
     874 *
     875 * @since 2.6.16
     876 *
     877 * @param int      $user_id  ID of the user being deleted.
     878 * @param int|null $reassign ID of the user receiving the posts.
     879 */
     880function bbp_update_counts_on_user_reassignment( $user_id = 0, $reassign = null ) {
     881        static $topic_ids = array();
     882
     883        $user_id = (int) $user_id;
     884        $reassign = (int) $reassign;
     885
     886        // Bail if posts are not being reassigned to another user
     887        if ( empty( $user_id ) || empty( $reassign ) || ( $user_id === $reassign ) ) {
     888                return;
     889        }
     890
     891        $key = get_current_blog_id() . ':' . $user_id . ':' . $reassign;
     892
     893        // Record affected topics before WordPress changes their authors directly
     894        if ( 'delete_user' === current_filter() ) {
     895                $bbp_db     = bbp_db();
     896                $topic_type = bbp_get_topic_post_type();
     897                $reply_type = bbp_get_reply_post_type();
     898                $query      = $bbp_db->prepare(
     899                        "SELECT DISTINCT CASE WHEN post_type = %s THEN ID ELSE post_parent END FROM {$bbp_db->posts} WHERE post_author = %d AND post_type IN ( %s, %s )",
     900                        $topic_type,
     901                        $user_id,
     902                        $topic_type,
     903                        $reply_type
     904                );
     905
     906                $topic_ids[ $key ] = wp_parse_id_list( array_filter( $bbp_db->get_col( $query ) ) );
     907                return;
     908        }
     909
     910        // Bail unless WordPress completed the reassignment recorded above
     911        if ( ( 'deleted_user' !== current_filter() ) || ! isset( $topic_ids[ $key ] ) ) {
     912                return;
     913        }
     914
     915        $affected_topic_ids = $topic_ids[ $key ];
     916        unset( $topic_ids[ $key ] );
     917
     918        // Bail if the deleted user did not author any topics or replies
     919        if ( empty( $affected_topic_ids ) ) {
     920                return;
     921        }
     922
     923        // Recount contributions for the replacement user
     924        bbp_update_user_topic_count( $reassign, bbp_get_user_topic_count_raw( $reassign ) );
     925        bbp_update_user_reply_count( $reassign, bbp_get_user_reply_count_raw( $reassign ) );
     926
     927        // Rebuild engagements and voices for each affected topic
     928        foreach ( $affected_topic_ids as $topic_id ) {
     929                bbp_recalculate_topic_engagements( $topic_id, true );
     930                bbp_update_topic_voice_count( $topic_id );
     931        }
    764932}
    765933
Note: See TracChangeset for help on using the changeset viewer.