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/tests/phpunit/testcases/core/abstraction.php

    r5898 r7467  
    3232
    3333        /**
     34         * @covers ::bbp_bump_count_meta
     35         */
     36        public function test_bbp_bump_count_meta_handles_missing_and_non_negative_values() {
     37                $post_id = self::factory()->post->create();
     38
     39                $this->assertTrue( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', 2, 3 ) );
     40                $this->assertSame( 5, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     41                $this->assertTrue( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -10 ) );
     42                $this->assertSame( 0, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     43                $this->assertFalse( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 ) );
     44        }
     45
     46        /**
     47         * @covers ::bbp_bump_count_meta
     48         */
     49        public function test_bbp_bump_count_meta_validates_required_values_before_filters() {
     50                $filtered = 0;
     51                $callback = function( $check ) use ( &$filtered ) {
     52                        $filtered++;
     53                        return $check;
     54                };
     55
     56                add_filter( 'bbp_pre_bump_count_meta', $callback );
     57                $this->assertFalse( bbp_bump_count_meta( 'post', 0, '_bbp_test_count' ) );
     58                $this->assertFalse( bbp_bump_count_meta( 'post', 1, '' ) );
     59                $this->assertFalse( bbp_bump_count_meta( 'post', 1, '_bbp_test_count', 0 ) );
     60                remove_filter( 'bbp_pre_bump_count_meta', $callback, 10 );
     61
     62                $this->assertSame( 0, $filtered );
     63        }
     64
     65        /**
     66         * @covers ::bbp_bump_count_meta
     67         */
     68        public function test_bbp_bump_count_meta_can_be_short_circuited_before_type_validation() {
     69                $meta_types_filtered = 0;
     70                $received            = array();
     71                $pre_callback        = function( $check, $meta_type, $object_id, $meta_key, $difference, $default ) use ( &$received ) {
     72                        $received = func_get_args();
     73                        return false;
     74                };
     75                $types_callback = function( $meta_types ) use ( &$meta_types_filtered ) {
     76                        $meta_types_filtered++;
     77                        return $meta_types;
     78                };
     79
     80                add_filter( 'bbp_pre_bump_count_meta', $pre_callback, 10, 6 );
     81                add_filter( 'bbp_bump_count_meta_types', $types_callback );
     82                $result = bbp_bump_count_meta( 'invalid', 1, '_bbp_test_count' );
     83                remove_filter( 'bbp_bump_count_meta_types', $types_callback, 10 );
     84                remove_filter( 'bbp_pre_bump_count_meta', $pre_callback, 10 );
     85
     86                $this->assertFalse( $result );
     87                $this->assertSame( array( null, 'invalid', 1, '_bbp_test_count', 1, 0 ), $received );
     88                $this->assertSame( 0, $meta_types_filtered );
     89
     90                $pre_callback = function() {
     91                        return true;
     92                };
     93
     94                add_filter( 'bbp_pre_bump_count_meta', $pre_callback );
     95                $result = bbp_bump_count_meta( 'invalid', 1, '_bbp_test_count' );
     96                remove_filter( 'bbp_pre_bump_count_meta', $pre_callback, 10 );
     97
     98                $this->assertTrue( $result );
     99        }
     100
     101        /**
     102         * @covers ::bbp_bump_count_meta
     103         */
     104        public function test_bbp_bump_count_meta_supports_user_term_and_comment_metadata() {
     105                $post_id    = self::factory()->post->create();
     106                $user_id    = self::factory()->user->create();
     107                $term_id    = self::factory()->term->create();
     108                $comment_id = self::factory()->comment->create( array( 'comment_post_ID' => $post_id ) );
     109
     110                $this->assertTrue( bbp_bump_count_meta( 'user', $user_id, '_bbp_test_count', 1 ) );
     111                $this->assertSame( 1, (int) get_user_meta( $user_id, '_bbp_test_count', true ) );
     112                $this->assertTrue( bbp_bump_count_meta( 'term', $term_id, '_bbp_test_count', 2 ) );
     113                $this->assertSame( 2, (int) get_term_meta( $term_id, '_bbp_test_count', true ) );
     114                $this->assertTrue( bbp_bump_count_meta( 'comment', $comment_id, '_bbp_test_count', 3 ) );
     115                $this->assertSame( 3, (int) get_comment_meta( $comment_id, '_bbp_test_count', true ) );
     116        }
     117
     118        /**
     119         * @covers ::bbp_bump_count_meta
     120         */
     121        public function test_bbp_bump_count_meta_types_are_filterable() {
     122                $term_id  = self::factory()->term->create();
     123                $callback = function() {
     124                        return array( 'post' );
     125                };
     126
     127                add_filter( 'bbp_bump_count_meta_types', $callback );
     128                $result = bbp_bump_count_meta( 'term', $term_id, '_bbp_test_count' );
     129                remove_filter( 'bbp_bump_count_meta_types', $callback, 10 );
     130
     131                $this->assertFalse( $result );
     132                $this->assertSame( '', get_term_meta( $term_id, '_bbp_test_count', true ) );
     133        }
     134
     135        /**
     136         * @covers ::bbp_bump_count_meta
     137         */
     138        public function test_bbp_bump_count_meta_does_not_retry_a_filtered_update() {
     139                $post_id = self::factory()->post->create();
     140                $updates = 0;
     141                $callback = function() use ( &$updates ) {
     142                        $updates++;
     143                        return false;
     144                };
     145
     146                update_post_meta( $post_id, '_bbp_test_count', 5 );
     147                add_filter( 'update_post_metadata', $callback );
     148                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     149                remove_filter( 'update_post_metadata', $callback, 10 );
     150
     151                $this->assertFalse( $result );
     152                $this->assertSame( 1, $updates );
     153                $this->assertSame( 5, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     154        }
     155
     156        /**
     157         * @covers ::bbp_bump_count_meta
     158         */
     159        public function test_bbp_bump_count_meta_filters_an_update_before_adding_metadata() {
     160                $post_id  = self::factory()->post->create();
     161                $previous = null;
     162                $callback = function( $check, $filtered_post_id, $meta_key, $meta_value, $prev_value ) use ( &$previous ) {
     163                        $previous = $prev_value;
     164                        return false;
     165                };
     166
     167                add_filter( 'update_post_metadata', $callback, 10, 5 );
     168                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     169                remove_filter( 'update_post_metadata', $callback, 10 );
     170
     171                $this->assertFalse( $result );
     172                $this->assertSame( '', $previous );
     173                $this->assertSame( '', get_post_meta( $post_id, '_bbp_test_count', true ) );
     174        }
     175
     176        /**
     177         * @covers ::bbp_bump_count_meta
     178         * @ticket BBP3678
     179         */
     180        public function test_bbp_bump_count_meta_uses_the_database_after_a_filtered_read() {
     181                $post_id = self::factory()->post->create();
     182                $callback = function( $value, $filtered_post_id, $meta_key ) use ( $post_id ) {
     183                        return ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) )
     184                                ? 100
     185                                : $value;
     186                };
     187
     188                update_post_meta( $post_id, '_bbp_test_count', 5 );
     189                add_filter( 'get_post_metadata', $callback, 10, 3 );
     190                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     191                remove_filter( 'get_post_metadata', $callback, 10 );
     192
     193                $this->assertTrue( $result );
     194                $this->assertSame( 6, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     195        }
     196
     197        /**
     198         * @covers ::bbp_bump_count_meta
     199         */
     200        public function test_bbp_bump_count_meta_returns_false_when_adding_metadata_fails() {
     201                $post_id = self::factory()->post->create();
     202                $callback = function() {
     203                        return false;
     204                };
     205
     206                add_filter( 'add_post_metadata', $callback );
     207                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     208                remove_filter( 'add_post_metadata', $callback, 10 );
     209
     210                $this->assertFalse( $result );
     211                $this->assertSame( '', get_post_meta( $post_id, '_bbp_test_count', true ) );
     212        }
     213
     214        /**
     215         * @covers ::bbp_bump_count_meta
     216         * @ticket BBP3678
     217         */
     218        public function test_bbp_bump_count_meta_preserves_an_interleaved_update_from_zero() {
     219                $post_id     = self::factory()->post->create();
     220                $interleaved = false;
     221                $callback    = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved ) {
     222                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     223                                $interleaved = true;
     224                                bbp_bump_count_meta( 'post', $post_id, $meta_key );
     225                        }
     226
     227                        return $check;
     228                };
     229
     230                update_post_meta( $post_id, '_bbp_test_count', 0 );
     231                add_filter( 'update_post_metadata', $callback, 10, 3 );
     232                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     233                remove_filter( 'update_post_metadata', $callback, 10 );
     234
     235                $this->assertSame( 2, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     236        }
     237
     238        /**
     239         * @covers ::bbp_bump_count_meta
     240         * @ticket BBP3678
     241         */
     242        public function test_bbp_bump_count_meta_preserves_interleaved_decrements() {
     243                $post_id      = self::factory()->post->create();
     244                $interleaved  = false;
     245                $filter_calls = 0;
     246                $callback     = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved, &$filter_calls ) {
     247                        $filter_calls++;
     248
     249                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     250                                $interleaved = true;
     251                                bbp_bump_count_meta( 'post', $post_id, $meta_key, -1 );
     252                        }
     253
     254                        return $check;
     255                };
     256
     257                update_post_meta( $post_id, '_bbp_test_count', 3 );
     258                add_filter( 'update_post_metadata', $callback, 10, 3 );
     259                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 );
     260                remove_filter( 'update_post_metadata', $callback, 10 );
     261
     262                $this->assertSame( 1, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     263                $this->assertSame( 2, $filter_calls );
     264        }
     265
     266        /**
     267         * @covers ::bbp_bump_count_meta
     268         * @ticket BBP3678
     269         */
     270        public function test_bbp_bump_count_meta_refreshes_a_stale_zero_before_decrementing() {
     271                global $wpdb;
     272
     273                $post_id = self::factory()->post->create();
     274
     275                update_post_meta( $post_id, '_bbp_test_count', 0 );
     276                get_post_meta( $post_id, '_bbp_test_count', true );
     277
     278                $wpdb->update(
     279                        $wpdb->postmeta,
     280                        array( 'meta_value' => 1 ),
     281                        array(
     282                                'post_id'  => $post_id,
     283                                'meta_key' => '_bbp_test_count'
     284                        ),
     285                        array( '%d' ),
     286                        array( '%d', '%s' )
     287                );
     288
     289                $this->assertTrue( bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 ) );
     290                $this->assertSame( 0, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     291        }
     292
     293        /**
     294         * @covers ::bbp_bump_count_meta
     295         * @ticket BBP3678
     296         */
     297        public function test_bbp_bump_count_meta_max_attempts_is_filterable() {
     298                $post_id     = self::factory()->post->create();
     299                $interleaved = false;
     300                $callback    = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved ) {
     301                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     302                                $interleaved = true;
     303                                bbp_bump_count_meta( 'post', $post_id, $meta_key, -1 );
     304                        }
     305
     306                        return $check;
     307                };
     308                $max_attempts = function() {
     309                        return 1;
     310                };
     311
     312                update_post_meta( $post_id, '_bbp_test_count', 3 );
     313                add_filter( 'update_post_metadata', $callback, 10, 3 );
     314                add_filter( 'bbp_bump_count_meta_max_attempts', $max_attempts );
     315                $result = bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count', -1 );
     316                remove_filter( 'bbp_bump_count_meta_max_attempts', $max_attempts, 10 );
     317                remove_filter( 'update_post_metadata', $callback, 10 );
     318
     319                $this->assertFalse( $result );
     320                $this->assertSame( 2, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     321        }
     322
     323        /**
     324         * @covers ::bbp_bump_count_meta
     325         * @ticket BBP3678
     326         */
     327        public function test_bbp_bump_count_meta_preserves_an_interleaved_add() {
     328                $post_id     = self::factory()->post->create();
     329                $interleaved = false;
     330                $callback    = function( $check, $filtered_post_id, $meta_key ) use ( $post_id, &$interleaved ) {
     331                        if ( ( $post_id === $filtered_post_id ) && ( '_bbp_test_count' === $meta_key ) && ! $interleaved ) {
     332                                $interleaved = true;
     333                                bbp_bump_count_meta( 'post', $post_id, $meta_key );
     334                        }
     335
     336                        return $check;
     337                };
     338
     339                add_filter( 'add_post_metadata', $callback, 10, 3 );
     340                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     341                remove_filter( 'add_post_metadata', $callback, 10 );
     342
     343                $this->assertSame( 2, (int) get_post_meta( $post_id, '_bbp_test_count', true ) );
     344        }
     345
     346        /**
     347         * @covers ::bbp_bump_count_meta
     348         */
     349        public function test_bbp_bump_count_meta_runs_standard_metadata_actions() {
     350                $post_id        = self::factory()->post->create();
     351                $updated        = 0;
     352                $legacy_updated = 0;
     353                $callback = function( $meta_id, $updated_post_id, $meta_key, $meta_value ) use ( $post_id, &$updated ) {
     354                        if ( ( $post_id === $updated_post_id ) && ( '_bbp_test_count' === $meta_key ) && ( 2 === $meta_value ) ) {
     355                                $updated++;
     356                        }
     357                };
     358                $legacy_callback = function( $meta_id, $updated_post_id, $meta_key, $meta_value ) use ( $post_id, &$legacy_updated ) {
     359                        if ( ( $post_id === $updated_post_id ) && ( '_bbp_test_count' === $meta_key ) && ( 2 === $meta_value ) ) {
     360                                $legacy_updated++;
     361                        }
     362                };
     363
     364                update_post_meta( $post_id, '_bbp_test_count', 1 );
     365                add_action( 'updated_post_meta', $callback, 10, 4 );
     366                add_action( 'updated_postmeta', $legacy_callback, 10, 4 );
     367                bbp_bump_count_meta( 'post', $post_id, '_bbp_test_count' );
     368                remove_action( 'updated_post_meta', $callback, 10 );
     369                remove_action( 'updated_postmeta', $legacy_callback, 10 );
     370
     371                $this->assertSame( 1, $updated );
     372                $this->assertSame( 1, $legacy_updated );
     373        }
     374
     375        /**
    34376         * @covers ::bbp_rewrite
    35377         * @todo   Implement test_bbp_rewrite().
Note: See TracChangeset for help on using the changeset viewer.