Changeset 62099
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/collaboration/class-wp-sync-post-meta-storage.php
r62064 r62099 31 31 * @var string 32 32 */ 33 const AWARENESS_META_KEY = 'wp_sync_awareness ';33 const AWARENESS_META_KEY = 'wp_sync_awareness_state'; 34 34 35 35 /** … … 39 39 * @var string 40 40 */ 41 const SYNC_UPDATE_META_KEY = 'wp_sync_update ';41 const SYNC_UPDATE_META_KEY = 'wp_sync_update_data'; 42 42 43 43 /** … … 69 69 * 70 70 * @since 7.0.0 71 * 72 * @global wpdb $wpdb WordPress database abstraction object. 71 73 * 72 74 * @param string $room Room identifier. … … 75 77 */ 76 78 public function add_update( string $room, $update ): bool { 79 global $wpdb; 80 77 81 $post_id = $this->get_storage_post_id( $room ); 78 82 if ( null === $post_id ) { … … 80 84 } 81 85 82 $meta_id = add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $update, false ); 83 84 return (bool) $meta_id; 86 // Use direct database operation to avoid cache invalidation performed by 87 // post meta functions (`wp_cache_set_posts_last_changed()` and direct 88 // `wp_cache_delete()` calls). 89 return (bool) $wpdb->insert( 90 $wpdb->postmeta, 91 array( 92 'post_id' => $post_id, 93 'meta_key' => self::SYNC_UPDATE_META_KEY, 94 'meta_value' => wp_json_encode( $update ), 95 ), 96 array( '%d', '%s', '%s' ) 97 ); 85 98 } 86 99 … … 89 102 * 90 103 * @since 7.0.0 104 * 105 * @global wpdb $wpdb WordPress database abstraction object. 91 106 * 92 107 * @param string $room Room identifier. … … 94 109 */ 95 110 public function get_awareness_state( string $room ): array { 96 $post_id = $this->get_storage_post_id( $room ); 97 if ( null === $post_id ) { 98 return array(); 99 } 100 101 $awareness = get_post_meta( $post_id, self::AWARENESS_META_KEY, true ); 111 global $wpdb; 112 113 $post_id = $this->get_storage_post_id( $room ); 114 if ( null === $post_id ) { 115 return array(); 116 } 117 118 // Use direct database operation to avoid updating the post meta cache. 119 // ORDER BY meta_id DESC ensures the latest row wins if duplicates exist 120 // from a past race condition in set_awareness_state(). 121 $meta_value = $wpdb->get_var( 122 $wpdb->prepare( 123 "SELECT meta_value FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = %s ORDER BY meta_id DESC LIMIT 1", 124 $post_id, 125 self::AWARENESS_META_KEY 126 ) 127 ); 128 129 if ( null === $meta_value ) { 130 return array(); 131 } 132 133 $awareness = json_decode( $meta_value, true ); 102 134 103 135 if ( ! is_array( $awareness ) ) { … … 112 144 * 113 145 * @since 7.0.0 146 * 147 * @global wpdb $wpdb WordPress database abstraction object. 114 148 * 115 149 * @param string $room Room identifier. … … 118 152 */ 119 153 public function set_awareness_state( string $room, array $awareness ): bool { 154 global $wpdb; 155 120 156 $post_id = $this->get_storage_post_id( $room ); 121 157 if ( null === $post_id ) { … … 123 159 } 124 160 125 // update_post_meta returns false if the value is the same as the existing value. 126 update_post_meta( $post_id, wp_slash( self::AWARENESS_META_KEY ), wp_slash( $awareness ) ); 127 return true; 161 // Use direct database operation to avoid cache invalidation performed by 162 // post meta functions (`wp_cache_set_posts_last_changed()` and direct 163 // `wp_cache_delete()` calls). 164 // 165 // If two concurrent requests both see no row and both INSERT, the 166 // duplicate is harmless: get_awareness_state() reads the latest row 167 // (ORDER BY meta_id DESC). 168 $meta_id = $wpdb->get_var( 169 $wpdb->prepare( 170 "SELECT meta_id FROM $wpdb->postmeta WHERE post_id = %d AND meta_key = %s ORDER BY meta_id DESC LIMIT 1", 171 $post_id, 172 self::AWARENESS_META_KEY 173 ) 174 ); 175 176 if ( $meta_id ) { 177 return (bool) $wpdb->update( 178 $wpdb->postmeta, 179 array( 'meta_value' => wp_json_encode( $awareness ) ), 180 array( 'meta_id' => $meta_id ), 181 array( '%s' ), 182 array( '%d' ) 183 ); 184 } 185 186 return (bool) $wpdb->insert( 187 $wpdb->postmeta, 188 array( 189 'post_id' => $post_id, 190 'meta_key' => self::AWARENESS_META_KEY, 191 'meta_value' => wp_json_encode( $awareness ), 192 ), 193 array( '%d', '%s', '%s' ) 194 ); 128 195 } 129 196 … … 169 236 'name' => $room_hash, 170 237 'fields' => 'ids', 238 'orderby' => 'ID', 239 'order' => 'ASC', 171 240 ) 172 241 ); … … 213 282 * @since 7.0.0 214 283 * 284 * @global wpdb $wpdb WordPress database abstraction object. 285 * 215 286 * @param string $room Room identifier. 216 287 * @param int $cursor Return updates after this cursor (meta_id). … … 262 333 $updates = array(); 263 334 foreach ( $rows as $row ) { 264 $updates[] = maybe_unserialize( $row->meta_value ); 335 $decoded = json_decode( $row->meta_value, true ); 336 if ( null !== $decoded ) { 337 $updates[] = $decoded; 338 } 265 339 } 266 340 … … 272 346 * 273 347 * @since 7.0.0 348 * 349 * @global wpdb $wpdb WordPress database abstraction object. 274 350 * 275 351 * @param string $room Room identifier. -
trunk/tests/phpunit/tests/rest-api/rest-sync-server.php
r62064 r62099 566 566 } 567 567 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.NotPrepared650 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 false675 );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 $query688 );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 716 568 /* 717 569 * Compaction tests. … … 855 707 } 856 708 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 via924 // raw SQL through the real $wpdb to avoid metadata cache925 // interactions while the proxy is active.926 if ( ! $this->did_inject927 && 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.NotPrepared946 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 update961 // 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 983 709 /* 984 710 * Awareness tests.
Note: See TracChangeset
for help on using the changeset viewer.