Skip to content

Commit 91ed455

Browse files
committed
Real-time collaboration: fix race condition in default polling provider.
See also: #11067. Developed in: #11292. Fixes #64887. Props czarate, westonruter, mindctrl, peterwilsoncc, joefusco. git-svn-id: https://develop.svn.wordpress.org/trunk@62064 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 3678df3 commit 91ed455

2 files changed

Lines changed: 336 additions & 82 deletions

File tree

src/wp-includes/collaboration/class-wp-sync-post-meta-storage.php

Lines changed: 61 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -79,48 +79,9 @@ public function add_update( string $room, $update ): bool {
7979
return false;
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-
);
82+
$meta_id = add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $update, false );
8783

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;
84+
return (bool) $meta_id;
12485
}
12586

12687
/**
@@ -170,8 +131,7 @@ public function set_awareness_state( string $room, array $awareness ): bool {
170131
* Gets the current cursor for a given room.
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
177137
*
@@ -235,17 +195,6 @@ private function get_storage_post_id( string $room ): ?int {
235195
return null;
236196
}
237197

238-
/**
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-
249198
/**
250199
* Gets the number of updates stored for a given room.
251200
*
@@ -259,32 +208,63 @@ public function get_update_count( string $room ): int {
259208
}
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();
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+
}
274228

275-
foreach ( $all_updates as $update ) {
276-
if ( $update['timestamp'] > $cursor ) {
277-
$updates[] = $update;
278-
}
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();
279246
}
280247

281-
// Sort by timestamp to ensure order.
282-
usort(
283-
$updates,
284-
fn ( $a, $b ) => $a['timestamp'] <=> $b['timestamp']
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+
)
285256
);
286257

287-
return wp_list_pluck( $updates, 'value' );
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

290270
/**
@@ -293,30 +273,30 @@ public function get_updates_after_cursor( string $room, int $cursor ): array {
293273
* @since 7.0.0
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 ) {
302284
return false;
303285
}
304286

305-
$all_updates = $this->get_all_updates( $room );
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+
);
306295

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 ) ) ) {
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
}

0 commit comments

Comments
 (0)