Make WordPress Core


Ignore:
Timestamp:
03/24/2026 09:07:04 AM (7 months ago)
Author:
zieladam
Message:

Real-time collaboration: Use prepared queries instead of *_post_meta functions.

Replaces add_post_meta/update_post_meta with wpdb->insert/wpdb->update.

This prevents a real-time editing session from invalidating WP_Query and various other post caches every few seconds. RTC stores awareness and sync information in post meta with high frequency. However, every call the *_post_meta functions invalidated post caches.

This commit avoids this frequent invalidation by removing the direct *_post_meta calls in favor of $wpdb calls.

Props czarate, mukesh27, paulkevan.

Developed in ​https://github.com/WordPress/wordpress-develop/pull/11325.
See #64696.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/tests/phpunit/tests/rest-api/rest-sync-server.php

    r62064 r62099  
    566566        }
    567567
    568         public function test_sync_cursor_does_not_skip_update_inserted_during_fetch_window() {
    569                 global $wpdb;
    570 
    571                 wp_set_current_user( self::$editor_id );
    572 
    573                 $room    = $this->get_post_room();
    574                 $storage = new WP_Sync_Post_Meta_Storage();
    575 
    576                 $seed_update = array(
    577                         'client_id' => 1,
    578                         'type'      => 'update',
    579                         'data'      => 'c2VlZA==',
    580                 );
    581 
    582                 $this->assertTrue( $storage->add_update( $room, $seed_update ) );
    583 
    584                 $initial_updates = $storage->get_updates_after_cursor( $room, 0 );
    585                 $baseline_cursor = $storage->get_cursor( $room );
    586 
    587                 $this->assertCount( 1, $initial_updates );
    588                 $this->assertSame( $seed_update, $initial_updates[0] );
    589                 $this->assertGreaterThan( 0, $baseline_cursor );
    590 
    591                 $storage_posts   = get_posts(
    592                         array(
    593                                 'post_type'      => WP_Sync_Post_Meta_Storage::POST_TYPE,
    594                                 'posts_per_page' => 1,
    595                                 'post_status'    => 'publish',
    596                                 'name'           => md5( $room ),
    597                                 'fields'         => 'ids',
    598                         )
    599                 );
    600                 $storage_post_id = array_first( $storage_posts );
    601 
    602                 $this->assertIsInt( $storage_post_id );
    603 
    604                 $injected_update = array(
    605                         'client_id' => 9999,
    606                         'type'      => 'update',
    607                         'data'      => base64_encode( 'injected-during-fetch' ),
    608                 );
    609 
    610                 $original_wpdb = $wpdb;
    611                 $proxy_wpdb    = new class( $original_wpdb, $storage_post_id, $injected_update ) {
    612                         private $wpdb;
    613                         private $storage_post_id;
    614                         private $injected_update;
    615                         public $postmeta;
    616                         public $did_inject = false;
    617 
    618                         public function __construct( $wpdb, int $storage_post_id, array $injected_update ) {
    619                                 $this->wpdb            = $wpdb;
    620                                 $this->storage_post_id = $storage_post_id;
    621                                 $this->injected_update = $injected_update;
    622                                 $this->postmeta        = $wpdb->postmeta;
    623                         }
    624 
    625                         // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Proxy forwards fully prepared core queries.
    626                         public function prepare( ...$args ) {
    627                                 return $this->wpdb->prepare( ...$args );
    628                         }
    629 
    630                         public function get_row( $query = null, $output = OBJECT, $y = 0 ) {
    631                                 $result = $this->wpdb->get_row( $query, $output, $y );
    632 
    633                                 $this->maybe_inject_after_sync_query( $query );
    634 
    635                                 return $result;
    636                         }
    637 
    638                         public function get_var( $query = null, $x = 0, $y = 0 ) {
    639                                 $result = $this->wpdb->get_var( $query, $x, $y );
    640 
    641                                 $this->maybe_inject_after_sync_query( $query );
    642 
    643                                 return $result;
    644                         }
    645 
    646                         public function get_results( $query = null, $output = OBJECT ) {
    647                                 return $this->wpdb->get_results( $query, $output );
    648                         }
    649                         // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
    650 
    651                         public function __call( $name, $arguments ) {
    652                                 return $this->wpdb->$name( ...$arguments );
    653                         }
    654 
    655                         public function __get( $name ) {
    656                                 return $this->wpdb->$name;
    657                         }
    658 
    659                         public function __set( $name, $value ) {
    660                                 $this->wpdb->$name = $value;
    661                         }
    662 
    663                         private function inject_update(): void {
    664                                 if ( $this->did_inject ) {
    665                                         return;
    666                                 }
    667 
    668                                 $this->did_inject = true;
    669 
    670                                 add_post_meta(
    671                                         $this->storage_post_id,
    672                                         WP_Sync_Post_Meta_Storage::SYNC_UPDATE_META_KEY,
    673                                         $this->injected_update,
    674                                         false
    675                                 );
    676                         }
    677 
    678                         private function maybe_inject_after_sync_query( $query ): void {
    679                                 if ( $this->did_inject || ! is_string( $query ) ) {
    680                                         return;
    681                                 }
    682 
    683                                 $targets_postmeta = false !== strpos( $query, $this->postmeta );
    684                                 $targets_post_id  = 1 === preg_match( '/\bpost_id\s*=\s*' . (int) $this->storage_post_id . '\b/', $query );
    685                                 $targets_meta_key = 1 === preg_match(
    686                                         "/\bmeta_key\s*=\s*'" . preg_quote( WP_Sync_Post_Meta_Storage::SYNC_UPDATE_META_KEY, '/' ) . "'/",
    687                                         $query
    688                                 );
    689 
    690                                 if ( $targets_postmeta && $targets_post_id && $targets_meta_key ) {
    691                                         $this->inject_update();
    692                                 }
    693                         }
    694                 };
    695 
    696                 $wpdb = $proxy_wpdb;
    697                 try {
    698                         $race_updates = $storage->get_updates_after_cursor( $room, $baseline_cursor );
    699                         $race_cursor  = $storage->get_cursor( $room );
    700                 } finally {
    701                         $wpdb = $original_wpdb;
    702                 }
    703 
    704                 $this->assertTrue( $proxy_wpdb->did_inject, 'Expected race-window update injection to occur.' );
    705                 $this->assertEmpty( $race_updates );
    706                 $this->assertSame( $baseline_cursor, $race_cursor );
    707 
    708                 $follow_up_updates = $storage->get_updates_after_cursor( $room, $race_cursor );
    709                 $follow_up_cursor  = $storage->get_cursor( $room );
    710 
    711                 $this->assertCount( 1, $follow_up_updates );
    712                 $this->assertSame( $injected_update, $follow_up_updates[0] );
    713                 $this->assertGreaterThan( $race_cursor, $follow_up_cursor );
    714         }
    715 
    716568        /*
    717569         * Compaction tests.
    … …  
    855707        }
    856708
    857         public function test_sync_compaction_does_not_delete_update_inserted_during_delete() {
    858                 global $wpdb;
    859 
    860                 wp_set_current_user( self::$editor_id );
    861 
    862                 $room    = $this->get_post_room();
    863                 $storage = new WP_Sync_Post_Meta_Storage();
    864 
    865                 // Seed three updates so there's something to compact.
    866                 for ( $i = 1; $i <= 3; $i++ ) {
    867                         $this->assertTrue(
    868                                 $storage->add_update(
    869                                         $room,
    870                                         array(
    871                                                 'client_id' => $i,
    872                                                 'type'      => 'update',
    873                                                 'data'      => base64_encode( "seed-$i" ),
    874                                         )
    875                                 )
    876                         );
    877                 }
    878 
    879                 // Capture the cursor after all seeds are in place.
    880                 $storage->get_updates_after_cursor( $room, 0 );
    881                 $compaction_cursor = $storage->get_cursor( $room );
    882                 $this->assertGreaterThan( 0, $compaction_cursor );
    883 
    884                 $storage_posts   = get_posts(
    885                         array(
    886                                 'post_type'      => WP_Sync_Post_Meta_Storage::POST_TYPE,
    887                                 'posts_per_page' => 1,
    888                                 'post_status'    => 'publish',
    889                                 'name'           => md5( $room ),
    890                                 'fields'         => 'ids',
    891                         )
    892                 );
    893                 $storage_post_id = array_first( $storage_posts );
    894                 $this->assertIsInt( $storage_post_id );
    895 
    896                 $concurrent_update = array(
    897                         'client_id' => 9999,
    898                         'type'      => 'update',
    899                         'data'      => base64_encode( 'arrived-during-compaction' ),
    900                 );
    901 
    902                 $original_wpdb = $wpdb;
    903                 $proxy_wpdb    = new class( $original_wpdb, $storage_post_id, $concurrent_update ) {
    904                         private $wpdb;
    905                         private $storage_post_id;
    906                         private $concurrent_update;
    907                         public $did_inject = false;
    908 
    909                         public function __construct( $wpdb, int $storage_post_id, array $concurrent_update ) {
    910                                 $this->wpdb              = $wpdb;
    911                                 $this->storage_post_id   = $storage_post_id;
    912                                 $this->concurrent_update = $concurrent_update;
    913                         }
    914 
    915                         // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared -- Proxy forwards fully prepared core queries.
    916                         public function prepare( ...$args ) {
    917                                 return $this->wpdb->prepare( ...$args );
    918                         }
    919 
    920                         public function query( $query ) {
    921                                 $result = $this->wpdb->query( $query );
    922 
    923                                 // After the DELETE executes, inject a concurrent update via
    924                                 // raw SQL through the real $wpdb to avoid metadata cache
    925                                 // interactions while the proxy is active.
    926                                 if ( ! $this->did_inject
    927                                         && is_string( $query )
    928                                         && 0 === strpos( $query, "DELETE FROM {$this->wpdb->postmeta}" )
    929                                         && false !== strpos( $query, "post_id = {$this->storage_post_id}" )
    930                                 ) {
    931                                         $this->did_inject = true;
    932                                         $this->wpdb->insert(
    933                                                 $this->wpdb->postmeta,
    934                                                 array(
    935                                                         'post_id'    => $this->storage_post_id,
    936                                                         'meta_key'   => WP_Sync_Post_Meta_Storage::SYNC_UPDATE_META_KEY,
    937                                                         'meta_value' => maybe_serialize( $this->concurrent_update ),
    938                                                 ),
    939                                                 array( '%d', '%s', '%s' )
    940                                         );
    941                                 }
    942 
    943                                 return $result;
    944                         }
    945                         // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared
    946 
    947                         public function __call( $name, $arguments ) {
    948                                 return $this->wpdb->$name( ...$arguments );
    949                         }
    950 
    951                         public function __get( $name ) {
    952                                 return $this->wpdb->$name;
    953                         }
    954 
    955                         public function __set( $name, $value ) {
    956                                 $this->wpdb->$name = $value;
    957                         }
    958                 };
    959 
    960                 // Run compaction through the proxy so the concurrent update
    961                 // is injected immediately after the DELETE executes.
    962                 $wpdb = $proxy_wpdb;
    963                 try {
    964                         $result = $storage->remove_updates_before_cursor( $room, $compaction_cursor );
    965                 } finally {
    966                         $wpdb = $original_wpdb;
    967                 }
    968 
    969                 $this->assertTrue( $result );
    970                 $this->assertTrue( $proxy_wpdb->did_inject, 'Expected concurrent update injection to occur.' );
    971 
    972                 // The concurrent update must survive the compaction delete.
    973                 $updates = $storage->get_updates_after_cursor( $room, 0 );
    974 
    975                 $update_data = wp_list_pluck( $updates, 'data' );
    976                 $this->assertContains(
    977                         $concurrent_update['data'],
    978                         $update_data,
    979                         'Concurrent update should survive compaction.'
    980                 );
    981         }
    982 
    983709        /*
    984710         * Awareness tests.
Note: See TracChangeset for help on using the changeset viewer.