diff --git a/src/wp-admin/includes/class-wp-upgrader.php b/src/wp-admin/includes/class-wp-upgrader.php index 695ce50bf0d7e..6299c66a5faef 100644 --- a/src/wp-admin/includes/class-wp-upgrader.php +++ b/src/wp-admin/includes/class-wp-upgrader.php @@ -1064,6 +1064,22 @@ public static function create_lock( $lock_name, $release_timeout = null ) { // Try to lock. $lock_result = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'off') /* LOCK */", $lock_option, time() ) ); + /* + * Invalidate the notoptions cache for the lock option. + * + * The lock is created with a direct INSERT IGNORE query, bypassing the + * Options API. This means the notoptions cache may still contain a stale + * entry for this option (added when the option previously did not exist), + * causing get_option() to return false without querying the database. + * + * @see https://core.trac.wordpress.org/ticket/64080 + */ + $notoptions = wp_cache_get( 'notoptions', 'options' ); + if ( is_array( $notoptions ) && isset( $notoptions[ $lock_option ] ) ) { + unset( $notoptions[ $lock_option ] ); + wp_cache_set( 'notoptions', $notoptions, 'options' ); + } + if ( ! $lock_result ) { $lock_result = get_option( $lock_option ); diff --git a/tests/phpunit/tests/admin/wpUpgrader.php b/tests/phpunit/tests/admin/wpUpgrader.php index ffea594ae87bc..1df8416c63dbf 100644 --- a/tests/phpunit/tests/admin/wpUpgrader.php +++ b/tests/phpunit/tests/admin/wpUpgrader.php @@ -1598,6 +1598,27 @@ public function test_release_lock_should_remove_lock_option() { $this->assertNotSame( 'content', get_option( 'lock.lock' ) ); } + /** + * Tests that `WP_Upgrader::create_lock()` invalidates stale notoptions cache + * so that expired locks can be detected and re-created. + * + * @ticket 64080 + * + * @covers WP_Upgrader::create_lock + */ + public function test_create_lock_should_invalidate_stale_notoptions_cache() { + // Prime notoptions cache by requesting the non-existent lock option. + get_option( 'test.lock' ); + + $this->assertTrue( WP_Upgrader::create_lock( 'test' ), 'create_lock() should succeed despite stale notoptions cache.' ); + $this->assertNotFalse( get_option( 'test.lock' ), 'get_option() should return the lock timestamp after create_lock().' ); + + update_option( 'test.lock', time() - 10 ); + $this->assertTrue( WP_Upgrader::create_lock( 'test', 5 ), 'Expired lock should be released and re-created.' ); + + WP_Upgrader::release_lock( 'test' ); + } + /** * Tests that `WP_Upgrader::download_package()` returns early when * the 'upgrader_pre_download' filter returns a non-false value.