Make WordPress Core


Ignore:
Timestamp:
03/19/2026 01:13:06 PM (4 months ago)
Author:
ellatrix
Message:

Real-time collaboration: fix race condition in default polling provider.

See also: https://github.com/WordPress/wordpress-develop/pull/11067.
Developed in: https://github.com/WordPress/wordpress-develop/pull/11292.

Fixes #64887.
Props czarate, westonruter, mindctrl, peterwilsoncc, joefusco.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/collaboration/class-wp-sync-post-meta-storage.php

    r61788 r62064  
    8080                }
    8181
    82                 // Create an envelope and stamp each update to enable cursor-based filtering.
    83                 $envelope = array(
    84                         'timestamp' => $this->get_time_marker(),
    85                         'value'     => $update,
    86                 );
    87 
    88                 return (bool) add_post_meta( $post_id, wp_slash( self::SYNC_UPDATE_META_KEY ), wp_slash( $envelope ), false );
    89         }
    90 
    91         /**
    92          * Retrieves all sync updates for a given room.
    93          *
    94          * @since 7.0.0
    95          *
    96          * @param string $room Room identifier.
    97          * @return array<int, array{ timestamp: int, value: mixed }> Sync updates.
    98          */
    99         private function get_all_updates( string $room ): array {
    100                 $this->room_cursors[ $room ] = $this->get_time_marker() - 100; // Small buffer to ensure consistency.
    101 
    102                 $post_id = $this->get_storage_post_id( $room );
    103                 if ( null === $post_id ) {
    104                         return array();
    105                 }
    106 
    107                 $updates = get_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, false );
    108 
    109                 if ( ! is_array( $updates ) ) {
    110                         $updates = array();
    111                 }
    112 
    113                 // Filter out any updates that don't have the expected structure.
    114                 $updates = array_filter(
    115                         $updates,
    116                         static function ( $update ): bool {
    117                                 return is_array( $update ) && isset( $update['timestamp'], $update['value'] ) && is_int( $update['timestamp'] );
    118                         }
    119                 );
    120 
    121                 $this->room_update_counts[ $room ] = count( $updates );
    122 
    123                 return $updates;
     82                $meta_id = add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $update, false );
     83
     84                return (bool) $meta_id;
    12485        }
    12586
     
    171132         *
    172133         * The cursor is set during get_updates_after_cursor() and represents the
    173          * point in time just before the updates were retrieved, with a small buffer
    174          * to ensure consistency.
     134         * highest meta_id seen for the room's sync updates.
    175135         *
    176136         * @since 7.0.0
     
    237197
    238198        /**
    239          * Gets the current time in milliseconds as a comparable time marker.
    240          *
    241          * @since 7.0.0
    242          *
    243          * @return int Current time in milliseconds.
    244          */
    245         private function get_time_marker(): int {
    246                 return (int) floor( microtime( true ) * 1000 );
    247         }
    248 
    249         /**
    250199         * Gets the number of updates stored for a given room.
    251200         *
     
    260209
    261210        /**
    262          * Retrieves sync updates from a room for a given client and cursor. Updates
    263          * from the specified client should be excluded.
     211         * Retrieves sync updates from a room after the given cursor.
    264212         *
    265213         * @since 7.0.0
    266214         *
    267215         * @param string $room   Room identifier.
    268          * @param int    $cursor Return updates after this cursor.
     216         * @param int    $cursor Return updates after this cursor (meta_id).
    269217         * @return array<int, mixed> Sync updates.
    270218         */
    271219        public function get_updates_after_cursor( string $room, int $cursor ): array {
    272                 $all_updates = $this->get_all_updates( $room );
    273                 $updates     = array();
    274 
    275                 foreach ( $all_updates as $update ) {
    276                         if ( $update['timestamp'] > $cursor ) {
    277                                 $updates[] = $update;
    278                         }
    279                 }
    280 
    281                 // Sort by timestamp to ensure order.
    282                 usort(
    283                         $updates,
    284                         fn ( $a, $b ) => $a['timestamp'] <=> $b['timestamp']
    285                 );
    286 
    287                 return wp_list_pluck( $updates, 'value' );
     220                global $wpdb;
     221
     222                $post_id = $this->get_storage_post_id( $room );
     223                if ( null === $post_id ) {
     224                        $this->room_cursors[ $room ]       = 0;
     225                        $this->room_update_counts[ $room ] = 0;
     226                        return array();
     227                }
     228
     229                // Capture the current room state first so the returned cursor is race-safe.
     230                $stats = $wpdb->get_row(
     231                        $wpdb->prepare(
     232                                "SELECT COUNT(*) AS total_updates, COALESCE( MAX(meta_id), 0 ) AS max_meta_id FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s",
     233                                $post_id,
     234                                self::SYNC_UPDATE_META_KEY
     235                        )
     236                );
     237
     238                $total_updates = $stats ? (int) $stats->total_updates : 0;
     239                $max_meta_id   = $stats ? (int) $stats->max_meta_id : 0;
     240
     241                $this->room_update_counts[ $room ] = $total_updates;
     242                $this->room_cursors[ $room ]       = $max_meta_id;
     243
     244                if ( $max_meta_id <= $cursor ) {
     245                        return array();
     246                }
     247
     248                $rows = $wpdb->get_results(
     249                        $wpdb->prepare(
     250                                "SELECT meta_value FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s AND meta_id > %d AND meta_id <= %d ORDER BY meta_id ASC",
     251                                $post_id,
     252                                self::SYNC_UPDATE_META_KEY,
     253                                $cursor,
     254                                $max_meta_id
     255                        )
     256                );
     257
     258                if ( ! $rows ) {
     259                        return array();
     260                }
     261
     262                $updates = array();
     263                foreach ( $rows as $row ) {
     264                        $updates[] = maybe_unserialize( $row->meta_value );
     265                }
     266
     267                return $updates;
    288268        }
    289269
     
    294274         *
    295275         * @param string $room   Room identifier.
    296          * @param int    $cursor Remove updates with markers < this cursor.
     276         * @param int    $cursor Remove updates with meta_id < this cursor.
    297277         * @return bool True on success, false on failure.
    298278         */
    299279        public function remove_updates_before_cursor( string $room, int $cursor ): bool {
     280                global $wpdb;
     281
    300282                $post_id = $this->get_storage_post_id( $room );
    301283                if ( null === $post_id ) {
     
    303285                }
    304286
    305                 $all_updates = $this->get_all_updates( $room );
    306 
    307                 // Remove all updates for the room and re-store only those that are newer than the cursor.
    308                 if ( ! delete_post_meta( $post_id, wp_slash( self::SYNC_UPDATE_META_KEY ) ) ) {
     287                $deleted_rows = $wpdb->query(
     288                        $wpdb->prepare(
     289                                "DELETE FROM {$wpdb->postmeta} WHERE post_id = %d AND meta_key = %s AND meta_id < %d",
     290                                $post_id,
     291                                self::SYNC_UPDATE_META_KEY,
     292                                $cursor
     293                        )
     294                );
     295
     296                if ( false === $deleted_rows ) {
    309297                        return false;
    310298                }
    311299
    312                 // Re-store envelopes directly to avoid double-wrapping by add_update().
    313                 $add_result = true;
    314                 foreach ( $all_updates as $envelope ) {
    315                         if ( $add_result && $envelope['timestamp'] >= $cursor ) {
    316                                 $add_result = (bool) add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $envelope, false );
    317                         }
    318                 }
    319 
    320                 return $add_result;
     300                return true;
    321301        }
    322302}
Note: See TracChangeset for help on using the changeset viewer.