-
Notifications
You must be signed in to change notification settings - Fork 3.4k
RTC: Ensure a single canonical update log for collaborative edits. #11660
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,7 +86,7 @@ public function add_update( string $room, $update ): bool { | |
| // Use direct database operation to avoid cache invalidation performed by | ||
| // post meta functions (`wp_cache_set_posts_last_changed()` and direct | ||
| // `wp_cache_delete()` calls). | ||
| return (bool) $wpdb->insert( | ||
| $result = $wpdb->insert( | ||
| $wpdb->postmeta, | ||
| array( | ||
| 'post_id' => $post_id, | ||
|
|
@@ -95,6 +95,13 @@ public function add_update( string $room, $update ): bool { | |
| ), | ||
| array( '%d', '%s', '%s' ) | ||
| ); | ||
|
|
||
| if ( $result ) { | ||
| $room_hash = md5( $room ); | ||
| self::$storage_post_ids[ $room_hash ] = $this->merge_duplicate_storage_posts( $room_hash, $post_id ); | ||
| } | ||
|
|
||
| return (bool) $result; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -153,7 +160,8 @@ public function get_awareness_state( string $room ): array { | |
| public function set_awareness_state( string $room, array $awareness ): bool { | ||
| global $wpdb; | ||
|
|
||
| $post_id = $this->get_storage_post_id( $room ); | ||
| $room_hash = md5( $room ); | ||
| $post_id = $this->get_storage_post_id( $room ); | ||
| if ( null === $post_id ) { | ||
| return false; | ||
| } | ||
|
|
@@ -174,16 +182,22 @@ public function set_awareness_state( string $room, array $awareness ): bool { | |
| ); | ||
|
|
||
| if ( $meta_id ) { | ||
| return (bool) $wpdb->update( | ||
| $result = $wpdb->update( | ||
| $wpdb->postmeta, | ||
| array( 'meta_value' => wp_json_encode( $awareness ) ), | ||
| array( 'meta_id' => $meta_id ), | ||
| array( '%s' ), | ||
| array( '%d' ) | ||
| ); | ||
|
|
||
| if ( false !== $result ) { | ||
| self::$storage_post_ids[ $room_hash ] = $this->merge_duplicate_storage_posts( $room_hash, $post_id ); | ||
| } | ||
|
Comment on lines
+193
to
+195
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you actually did what i suggested above, but why the negative check instead of the positive test? |
||
|
|
||
| return false !== $result; | ||
| } | ||
|
|
||
| return (bool) $wpdb->insert( | ||
| $result = $wpdb->insert( | ||
| $wpdb->postmeta, | ||
| array( | ||
| 'post_id' => $post_id, | ||
|
|
@@ -192,6 +206,12 @@ public function set_awareness_state( string $room, array $awareness ): bool { | |
| ), | ||
| array( '%d', '%s', '%s' ) | ||
| ); | ||
|
|
||
| if ( $result ) { | ||
| self::$storage_post_ids[ $room_hash ] = $this->merge_duplicate_storage_posts( $room_hash, $post_id ); | ||
| } | ||
|
Comment on lines
+210
to
+212
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See above, same argument/case |
||
|
|
||
| return (bool) $result; | ||
| } | ||
|
|
||
| /** | ||
|
|
@@ -256,14 +276,185 @@ private function get_storage_post_id( string $room ): ?int { | |
| ) | ||
| ); | ||
|
|
||
| if ( is_int( $post_id ) ) { | ||
| self::$storage_post_ids[ $room_hash ] = $post_id; | ||
| return $post_id; | ||
| if ( is_int( $post_id ) && $post_id > 0 ) { | ||
| $canonical_post_id = $this->resolve_canonical_storage_post_id_after_insert( $room_hash, $post_id ); | ||
| if ( null === $canonical_post_id ) { | ||
| return null; | ||
| } | ||
|
|
||
| self::$storage_post_ids[ $room_hash ] = $canonical_post_id; | ||
| return $canonical_post_id; | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolves the canonical room storage post after inserting a new post. | ||
| * | ||
| * Two concurrent first writers can both miss the lookup above and create | ||
| * storage posts for the same room hash. Depending on the exact interleaving, | ||
| * WordPress may create either a duplicate exact slug or a suffixed slug. | ||
| * When that happens, merge everything back into one canonical lineage. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param string $room_hash MD5 hash of the room identifier. | ||
| * @param int $inserted_post_id Post ID returned by wp_insert_post(). | ||
| * @return int|null Canonical storage post ID. | ||
| */ | ||
| private function resolve_canonical_storage_post_id_after_insert( string $room_hash, int $inserted_post_id ): ?int { | ||
| $canonical_post_id = $this->find_canonical_storage_post_id( $room_hash ); | ||
| if ( null === $canonical_post_id ) { | ||
| $canonical_post_id = $this->promote_storage_post_to_canonical_slug( $room_hash, $inserted_post_id ); | ||
| } | ||
|
|
||
| if ( null === $canonical_post_id ) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would still consider checking for |
||
| wp_delete_post( $inserted_post_id, true ); | ||
| return null; | ||
| } | ||
|
|
||
| return $this->merge_duplicate_storage_posts( $room_hash, $canonical_post_id ); | ||
| } | ||
|
|
||
| /** | ||
| * Merges duplicate storage posts created by a first-access race. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param string $room_hash MD5 hash of the room identifier. | ||
| * @param int $canonical_post_id Preferred post ID that should own the room. | ||
| * @return int Canonical storage post ID. | ||
| */ | ||
| private function merge_duplicate_storage_posts( string $room_hash, int $canonical_post_id ): int { | ||
| global $wpdb; | ||
|
|
||
| $storage_post_ids = $this->get_storage_post_ids_for_room_hash( $room_hash ); | ||
| if ( empty( $storage_post_ids ) ) { | ||
| return $canonical_post_id; | ||
| } | ||
|
|
||
| $exact_post_id = $this->find_canonical_storage_post_id( $room_hash ); | ||
| if ( null === $exact_post_id ) { | ||
| $canonical_post_id = in_array( $canonical_post_id, $storage_post_ids, true ) ? $canonical_post_id : (int) $storage_post_ids[0]; | ||
| $promoted_post_id = $this->promote_storage_post_to_canonical_slug( $room_hash, $canonical_post_id ); | ||
| if ( null === $promoted_post_id ) { | ||
| return $canonical_post_id; | ||
| } | ||
|
|
||
| $canonical_post_id = $promoted_post_id; | ||
| $storage_post_ids = $this->get_storage_post_ids_for_room_hash( $room_hash ); | ||
| } else { | ||
| $canonical_post_id = $exact_post_id; | ||
| } | ||
|
|
||
| foreach ( $storage_post_ids as $duplicate_id ) { | ||
| if ( $canonical_post_id === $duplicate_id ) { | ||
| continue; | ||
| } | ||
|
|
||
| $move_result = $wpdb->update( | ||
| $wpdb->postmeta, | ||
| array( 'post_id' => $canonical_post_id ), | ||
| array( 'post_id' => $duplicate_id ), | ||
| array( '%d' ), | ||
| array( '%d' ) | ||
| ); | ||
|
|
||
| if ( false === $move_result ) { | ||
| continue; | ||
| } | ||
|
|
||
| wp_delete_post( $duplicate_id, true ); | ||
| } | ||
|
|
||
| return $canonical_post_id; | ||
| } | ||
|
|
||
| /** | ||
| * Finds the canonical storage post for a room hash. | ||
| * | ||
| * The canonical post is the oldest published storage post with the exact | ||
| * room hash slug. Suffixed slugs are repair candidates, not canonical. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param string $room_hash MD5 hash of the room identifier. | ||
| * @return int|null Canonical storage post ID. | ||
| */ | ||
| private function find_canonical_storage_post_id( string $room_hash ): ?int { | ||
| global $wpdb; | ||
|
|
||
| $post_id = $wpdb->get_var( | ||
| $wpdb->prepare( | ||
| "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s AND post_status = 'publish' AND post_name = %s ORDER BY ID ASC LIMIT 1", | ||
| self::POST_TYPE, | ||
| $room_hash | ||
| ) | ||
| ); | ||
|
|
||
| return is_numeric( $post_id ) ? (int) $post_id : null; | ||
| } | ||
|
|
||
| /** | ||
| * Promotes a storage post to the canonical room slug. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param string $room_hash MD5 hash of the room identifier. | ||
| * @param int $post_id Post ID to promote. | ||
| * @return int|null Promoted post ID on success. | ||
| */ | ||
| private function promote_storage_post_to_canonical_slug( string $room_hash, int $post_id ): ?int { | ||
| global $wpdb; | ||
|
|
||
| $result = $wpdb->update( | ||
| $wpdb->posts, | ||
| array( 'post_name' => $room_hash ), | ||
| array( | ||
| 'ID' => $post_id, | ||
| 'post_type' => self::POST_TYPE, | ||
| 'post_status' => 'publish', | ||
| ), | ||
| array( '%s' ), | ||
| array( '%d', '%s', '%s' ) | ||
| ); | ||
|
|
||
| if ( false === $result ) { | ||
| return null; | ||
| } | ||
|
|
||
| clean_post_cache( $post_id ); | ||
| return $post_id; | ||
| } | ||
|
|
||
| /** | ||
| * Lists storage posts belonging to a room hash, including suffixed duplicates. | ||
| * | ||
| * @since 7.0.0 | ||
| * | ||
| * @param string $room_hash MD5 hash of the room identifier. | ||
| * @return array<int> Storage post IDs. | ||
| */ | ||
| private function get_storage_post_ids_for_room_hash( string $room_hash ): array { | ||
| global $wpdb; | ||
|
|
||
| $post_ids = $wpdb->get_col( | ||
| $wpdb->prepare( | ||
| "SELECT ID FROM {$wpdb->posts} | ||
| WHERE post_type = %s | ||
| AND post_status = 'publish' | ||
| AND ( post_name = %s OR post_name LIKE %s ) | ||
| ORDER BY ID ASC", | ||
| self::POST_TYPE, | ||
| $room_hash, | ||
| $wpdb->esc_like( $room_hash . '-' ) . '%' | ||
| ) | ||
| ); | ||
|
|
||
| return array_map( 'intval', $post_ids ); | ||
| } | ||
|
|
||
| /** | ||
| * Gets the number of updates stored for a given room. | ||
| * | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
While technically fine, how would you feel about a strict comparison?
insert()returnint|false.What about