Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions src/wp-includes/collaboration/class-wp-sync-post-meta-storage.php
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,18 @@ public function add_update( string $room, $update ): bool {
return false;
}

$meta_id = add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $update, false );
// Suspend setting the posts last_changed cache key for this operation.
if ( ! isset( $GLOBALS['__suspend_posts_last_changed_update'] ) ) {
$GLOBALS['__suspend_posts_last_changed_update'] = 0;
}
++$GLOBALS['__suspend_posts_last_changed_update'];
try {
return (bool) add_post_meta( $post_id, self::SYNC_UPDATE_META_KEY, $update, false );
} finally {
--$GLOBALS['__suspend_posts_last_changed_update'];
}

return (bool) $meta_id;
return false;
}

/**
Expand Down Expand Up @@ -122,8 +131,19 @@ public function set_awareness_state( string $room, array $awareness ): bool {
return false;
}

// Suspend setting the posts last_changed cache key for this operation.
if ( ! isset( $GLOBALS['__suspend_posts_last_changed_update'] ) ) {
$GLOBALS['__suspend_posts_last_changed_update'] = 0;
}
++$GLOBALS['__suspend_posts_last_changed_update'];
try {
update_post_meta( $post_id, self::AWARENESS_META_KEY, wp_slash( $awareness ) );
} finally {
--$GLOBALS['__suspend_posts_last_changed_update'];
}

// update_post_meta returns false if the value is the same as the existing value.
update_post_meta( $post_id, wp_slash( self::AWARENESS_META_KEY ), wp_slash( $awareness ) );
// ignore the return value since awareness updates are not critical.
return true;
}

Expand Down
4 changes: 4 additions & 0 deletions src/wp-includes/post.php
Original file line number Diff line number Diff line change
Expand Up @@ -8443,6 +8443,10 @@ function wp_add_trashed_suffix_to_post_name_for_post( $post ) {
* @since 5.0.0
*/
function wp_cache_set_posts_last_changed() {
if ( ! empty( $GLOBALS['__suspend_posts_last_changed_update'] ) ) {
return;
}

wp_cache_set_last_changed( 'posts' );
}

Expand Down
259 changes: 259 additions & 0 deletions tests/phpunit/tests/post/wpCacheSetPostsLastChanged.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,259 @@
<?php

/**
* Tests for the wp_cache_set_posts_last_changed() function.
*
* @group post
* @group cache
*
* @covers ::wp_cache_set_posts_last_changed
*/
class Tests_Post_wpCacheSetPostsLastChanged extends WP_UnitTestCase {

protected static $post_id;

public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
self::$post_id = $factory->post->create();
}

public static function wpTearDownAfterClass() {
wp_delete_post( self::$post_id, true );
}

public function tear_down() {
unset( $GLOBALS['__suspend_posts_last_changed_update'] );
parent::tear_down();
}

/**
* Verifies that the function updates the 'posts' cache group.
*/
public function test_sets_last_changed_for_posts_group() {
wp_cache_delete( 'last_changed', 'posts' );

wp_cache_set_posts_last_changed();

$this->assertNotFalse( wp_cache_get( 'last_changed', 'posts' ) );
}

/**
* Verifies that the function is suspended when the global counter is set.
*/
public function test_suspended_when_global_is_set() {
wp_cache_delete( 'last_changed', 'posts' );

$GLOBALS['__suspend_posts_last_changed_update'] = 1;

wp_cache_set_posts_last_changed();

$this->assertFalse( wp_cache_get( 'last_changed', 'posts' ) );
}

/**
* Verifies that the function resumes after the global counter reaches zero.
*/
public function test_resumes_when_global_reaches_zero() {
wp_cache_delete( 'last_changed', 'posts' );

$GLOBALS['__suspend_posts_last_changed_update'] = 0;

wp_cache_set_posts_last_changed();

$this->assertNotFalse( wp_cache_get( 'last_changed', 'posts' ) );
}

/**
* Verifies that add_post_meta updates last_changed when not suspended.
*/
public function test_add_post_meta_updates_last_changed() {
wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// Ensure the microtime value changes.
usleep( 1 );

add_post_meta( self::$post_id, '_test_meta_add', 'value' );

$after = wp_cache_get( 'last_changed', 'posts' );
$this->assertNotSame( $before, $after, 'add_post_meta should update last_changed.' );
}

/**
* Verifies that add_post_meta does not update last_changed when suspended.
*/
public function test_add_post_meta_suspended() {
wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// Ensure the microtime value changes.
usleep( 1 );

$GLOBALS['__suspend_posts_last_changed_update'] = 1;
add_post_meta( self::$post_id, '_test_meta_add_suspended', 'value' );
$GLOBALS['__suspend_posts_last_changed_update'] = 0;

$after = wp_cache_get( 'last_changed', 'posts' );
$this->assertSame( $before, $after, 'add_post_meta should not update last_changed when suspended.' );
}

/**
* Verifies that update_post_meta updates last_changed when not suspended.
*/
public function test_update_post_meta_updates_last_changed() {
add_post_meta( self::$post_id, '_test_meta_update', 'old' );

wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// Ensure the microtime value changes.
usleep( 1 );

update_post_meta( self::$post_id, '_test_meta_update', 'new' );

$after = wp_cache_get( 'last_changed', 'posts' );
$this->assertNotSame( $before, $after, 'update_post_meta should update last_changed.' );
}

/**
* Verifies that update_post_meta does not update last_changed when suspended.
*/
public function test_update_post_meta_suspended() {
add_post_meta( self::$post_id, '_test_meta_update_suspended', 'old' );

wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// Ensure the microtime value changes.
usleep( 1 );

$GLOBALS['__suspend_posts_last_changed_update'] = 1;
update_post_meta( self::$post_id, '_test_meta_update_suspended', 'new' );
$GLOBALS['__suspend_posts_last_changed_update'] = 0;

$after = wp_cache_get( 'last_changed', 'posts' );
$this->assertSame( $before, $after, 'update_post_meta should not update last_changed when suspended.' );
}

/**
* Verifies that delete_post_meta updates last_changed when not suspended.
*/
public function test_delete_post_meta_updates_last_changed() {
add_post_meta( self::$post_id, '_test_meta_delete', 'value' );

wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// Ensure the microtime value changes.
usleep( 1 );

delete_post_meta( self::$post_id, '_test_meta_delete' );

$after = wp_cache_get( 'last_changed', 'posts' );
$this->assertNotSame( $before, $after, 'delete_post_meta should update last_changed.' );
}

/**
* Verifies that delete_post_meta does not update last_changed when suspended.
*/
public function test_delete_post_meta_suspended() {
add_post_meta( self::$post_id, '_test_meta_delete_suspended', 'value' );

wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// Ensure the microtime value changes.
usleep( 1 );

$GLOBALS['__suspend_posts_last_changed_update'] = 1;
delete_post_meta( self::$post_id, '_test_meta_delete_suspended' );
$GLOBALS['__suspend_posts_last_changed_update'] = 0;

$after = wp_cache_get( 'last_changed', 'posts' );
$this->assertSame( $before, $after, 'delete_post_meta should not update last_changed when suspended.' );
}

/**
* Verifies that an embedded post meta operation triggered by a hook during
* a suspended call does not prematurely unsuspend the counter.
*
* Simulates: outer add_post_meta (suspended) triggers a hook that calls
* update_post_meta internally. The inner operation should not reset the
* counter, and last_changed should remain unchanged after both complete.
*/
public function test_nested_post_meta_via_hook_respects_suspension_counter() {
// When the outer add_post_meta fires 'added_post_meta', this hook
// performs its own update_post_meta while the counter is still active.
$nested_callback = function ( $meta_id, $post_id, $meta_key ) {
if ( '_test_outer_meta' !== $meta_key ) {
return;
}

// Increment the counter to simulate a nested suspended operation.
++$GLOBALS['__suspend_posts_last_changed_update'];
try {
update_post_meta( $post_id, '_test_inner_meta', 'inner_value' );
} finally {
--$GLOBALS['__suspend_posts_last_changed_update'];
}
};

add_action( 'added_post_meta', $nested_callback, 10, 3 );

wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// Ensure the microtime value changes.
usleep( 1 );

// Outer suspended operation.
$GLOBALS['__suspend_posts_last_changed_update'] = 1;
add_post_meta( self::$post_id, '_test_outer_meta', 'outer_value' );
$GLOBALS['__suspend_posts_last_changed_update'] = 0;

$after = wp_cache_get( 'last_changed', 'posts' );

remove_action( 'added_post_meta', $nested_callback, 10 );

$this->assertSame( $before, $after, 'Nested post meta via hook should not update last_changed while suspended.' );
$this->assertSame( 'inner_value', get_post_meta( self::$post_id, '_test_inner_meta', true ), 'Inner meta should still be written.' );
}

/**
* Verifies that last_changed is updated normally after suspension ends,
* even when a nested operation occurred during the suspended window.
*/
public function test_last_changed_updates_after_nested_suspension_ends() {
$nested_callback = function ( $mid, $post_id, $meta_key ) {
if ( '_test_outer_meta_2' !== $meta_key ) {
return;
}
++$GLOBALS['__suspend_posts_last_changed_update'];
try {
update_post_meta( $post_id, '_test_inner_meta_2', 'inner' );
} finally {
--$GLOBALS['__suspend_posts_last_changed_update'];
}
};

add_action( 'added_post_meta', $nested_callback, 10, 3 );

// Suspended operation with nested hook.
$GLOBALS['__suspend_posts_last_changed_update'] = 1;
add_post_meta( self::$post_id, '_test_outer_meta_2', 'outer' );
$GLOBALS['__suspend_posts_last_changed_update'] = 0;

remove_action( 'added_post_meta', $nested_callback, 10 );

// Record the cache value after the suspended window.
wp_cache_set_posts_last_changed();
$before = wp_cache_get( 'last_changed', 'posts' );

// A normal (unsuspended) meta operation should update last_changed.
usleep( 1 );

add_post_meta( self::$post_id, '_test_after_nested', 'value' );

$after = wp_cache_get( 'last_changed', 'posts' );
$this->assertNotSame( $before, $after, 'last_changed should update normally after suspension ends.' );
}
}
Loading