Options/Meta APIs: Optimize cache hits for non-existent options.
Some checks are pending
Coding Standards / PHP coding standards (push) Waiting to run
Coding Standards / JavaScript coding standards (push) Waiting to run
Coding Standards / Slack Notifications (push) Blocked by required conditions
Coding Standards / Failed workflow tasks (push) Blocked by required conditions
End-to-end Tests / Test with SCRIPT_DEBUG disabled (push) Waiting to run
End-to-end Tests / Test with SCRIPT_DEBUG enabled (push) Waiting to run
End-to-end Tests / Slack Notifications (push) Blocked by required conditions
End-to-end Tests / Failed workflow tasks (push) Blocked by required conditions
JavaScript Tests / QUnit Tests (push) Waiting to run
JavaScript Tests / Slack Notifications (push) Blocked by required conditions
JavaScript Tests / Failed workflow tasks (push) Blocked by required conditions
Performance Tests / Single site (push) Waiting to run
Performance Tests / Multisite (push) Waiting to run
Performance Tests / Slack Notifications (push) Blocked by required conditions
Performance Tests / Failed workflow tasks (push) Blocked by required conditions
PHP Compatibility / Check PHP compatibility (push) Waiting to run
PHP Compatibility / Slack Notifications (push) Blocked by required conditions
PHP Compatibility / Failed workflow tasks (push) Blocked by required conditions
PHPUnit Tests / PHP 7.2 (push) Waiting to run
PHPUnit Tests / PHP 7.3 (push) Waiting to run
PHPUnit Tests / PHP 7.4 (push) Waiting to run
PHPUnit Tests / PHP 8.0 (push) Waiting to run
PHPUnit Tests / PHP 8.1 (push) Waiting to run
PHPUnit Tests / PHP 8.2 (push) Waiting to run
PHPUnit Tests / PHP 8.3 (push) Waiting to run
PHPUnit Tests / PHP 8.4 (push) Waiting to run
PHPUnit Tests / html-api-html5lib-tests (push) Waiting to run
PHPUnit Tests / Slack Notifications (push) Blocked by required conditions
PHPUnit Tests / Failed workflow tasks (push) Blocked by required conditions
Test Build Processes / Core running from build (push) Waiting to run
Test Build Processes / Core running from src (push) Waiting to run
Test Build Processes / Gutenberg running from build (push) Waiting to run
Test Build Processes / Gutenberg running from src (push) Waiting to run
Test Build Processes / Slack Notifications (push) Blocked by required conditions
Test Build Processes / Failed workflow tasks (push) Blocked by required conditions

Optimize the order of checking the various options caches in `get_option()` to prevent hitting external caches each time it is called for a known non-existent option.

The caches are checked in the following order when getting an option:

1. Check the `alloptions` cache first to prioritize existing loaded options.
2. Check the `notoptions` cache before a cache lookup or DB hit.
3. Check the `options` cache prior to a DB hit.

Follow up to [56595].

Props adamsilverstein, flixos90, ivankristianto, joemcgill, rmccue, siliconforks, spacedmonkey.
Fixes #62692.
See #58277.


git-svn-id: https://develop.svn.wordpress.org/trunk@59631 602fd350-edb4-49c9-b593-d223f7449a82
This commit is contained in:
Peter Wilson 2025-01-15 22:11:15 +00:00
parent d2630e00cd
commit 5b01d24d8c
2 changed files with 122 additions and 48 deletions

View File

@ -162,37 +162,46 @@ function get_option( $option, $default_value = false ) {
if ( ! wp_installing() ) {
$alloptions = wp_load_alloptions();
/*
* When getting an option value, we check in the following order for performance:
*
* 1. Check the 'alloptions' cache first to prioritize existing loaded options.
* 2. Check the 'notoptions' cache before a cache lookup or DB hit.
* 3. Check the 'options' cache prior to a DB hit.
* 4. Check the DB for the option and cache it in either the 'options' or 'notoptions' cache.
*/
if ( isset( $alloptions[ $option ] ) ) {
$value = $alloptions[ $option ];
} else {
// Check for non-existent options first to avoid unnecessary object cache lookups and DB hits.
$notoptions = wp_cache_get( 'notoptions', 'options' );
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
wp_cache_set( 'notoptions', $notoptions, 'options' );
}
if ( isset( $notoptions[ $option ] ) ) {
/**
* Filters the default value for an option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.4.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value.
*
* @param mixed $default_value The default value to return if the option does not exist
* in the database.
* @param string $option Option name.
* @param bool $passed_default Was `get_option()` passed a default value?
*/
return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default );
}
$value = wp_cache_get( $option, 'options' );
if ( false === $value ) {
// Prevent non-existent options from triggering multiple queries.
$notoptions = wp_cache_get( 'notoptions', 'options' );
// Prevent non-existent `notoptions` key from triggering multiple key lookups.
if ( ! is_array( $notoptions ) ) {
$notoptions = array();
wp_cache_set( 'notoptions', $notoptions, 'options' );
} elseif ( isset( $notoptions[ $option ] ) ) {
/**
* Filters the default value for an option.
*
* The dynamic portion of the hook name, `$option`, refers to the option name.
*
* @since 3.4.0
* @since 4.4.0 The `$option` parameter was added.
* @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value.
*
* @param mixed $default_value The default value to return if the option does not exist
* in the database.
* @param string $option Option name.
* @param bool $passed_default Was `get_option()` passed a default value?
*/
return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default );
}
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );

View File

@ -112,8 +112,8 @@ class Tests_Option_Option extends WP_UnitTestCase {
wp_cache_set( 'notoptions', $notoptions, 'options' );
$before = get_num_queries();
$value = get_option( 'invalid' );
$after = get_num_queries();
get_option( 'invalid' );
$after = get_num_queries();
$this->assertSame( 0, $after - $before );
}
@ -127,8 +127,8 @@ class Tests_Option_Option extends WP_UnitTestCase {
get_option( 'invalid' );
$before = get_num_queries();
$value = get_option( 'invalid' );
$after = get_num_queries();
get_option( 'invalid' );
$after = get_num_queries();
$notoptions = wp_cache_get( 'notoptions', 'options' );
@ -137,25 +137,6 @@ class Tests_Option_Option extends WP_UnitTestCase {
$this->assertArrayHasKey( 'invalid', $notoptions, 'The "invalid" option should be in the notoptions cache.' );
}
/**
* @ticket 58277
*
* @covers ::get_option
*/
public function test_get_option_notoptions_do_not_load_cache() {
add_option( 'foo', 'bar', '', false );
wp_cache_delete( 'notoptions', 'options' );
$before = get_num_queries();
$value = get_option( 'foo' );
$after = get_num_queries();
$notoptions = wp_cache_get( 'notoptions', 'options' );
$this->assertSame( 0, $after - $before, 'The options cache was not hit on the second call to `get_option()`.' );
$this->assertFalse( $notoptions, 'The notoptions cache should not be set.' );
}
/**
* @covers ::get_option
* @covers ::add_option
@ -548,4 +529,88 @@ class Tests_Option_Option extends WP_UnitTestCase {
$updated_notoptions = wp_cache_get( 'notoptions', 'options' );
$this->assertArrayNotHasKey( $option_name, $updated_notoptions, 'The "foobar" option should not be in the notoptions cache after adding it.' );
}
/**
* Test that get_option() does not hit the external cache multiple times for the same option.
*
* @ticket 62692
*
* @covers ::get_option
*
* @dataProvider data_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option
*
* @param int $expected_connections Expected number of connections to the memcached server.
* @param bool $option_exists Whether the option should be set. Default true.
* @param string $autoload Whether the option should be auto loaded. Default true.
*/
public function test_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option( $expected_connections, $option_exists = true, $autoload = true ) {
if ( ! wp_using_ext_object_cache() ) {
$this->markTestSkipped( 'This test requires an external object cache.' );
}
if ( false === $this->helper_object_cache_stats_cmd_get() ) {
$this->markTestSkipped( 'This test requires access to the number of get requests to the external object cache.' );
}
if ( $option_exists ) {
add_option( 'ticket-62692', 'value', '', $autoload );
}
wp_cache_delete_multiple( array( 'ticket-62692', 'notoptions', 'alloptions' ), 'options' );
$connections_start = $this->helper_object_cache_stats_cmd_get();
$call_getter = 10;
while ( $call_getter-- ) {
get_option( 'ticket-62692' );
}
$connections_end = $this->helper_object_cache_stats_cmd_get();
$this->assertSame( $expected_connections, $connections_end - $connections_start );
}
/**
* Data provider.
*
* @return array[]
*/
public function data_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option() {
return array(
'exists, autoload' => array( 1, true, true ),
'exists, not autoloaded' => array( 3, true, false ),
'does not exist' => array( 3, false ),
);
}
/**
* Helper function to get the number of get commands from the external object cache.
*
* @return int|false Number of get command calls, false if unavailable.
*/
public function helper_object_cache_stats_cmd_get() {
if ( ! wp_using_ext_object_cache() || ! function_exists( 'wp_cache_get_stats' ) ) {
return false;
}
$stats = wp_cache_get_stats();
// Check the shape of the stats.
if ( ! is_array( $stats ) ) {
return false;
}
// Get the first server's stats.
$stats = array_shift( $stats );
if ( ! is_array( $stats ) ) {
return false;
}
if ( ! array_key_exists( 'cmd_get', $stats ) ) {
return false;
}
return $stats['cmd_get'];
}
}