Skip to content

Commit 5b01d24

Browse files
committed
Options/Meta APIs: Optimize cache hits for non-existent options.
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
1 parent d2630e0 commit 5b01d24

File tree

2 files changed

+122
-48
lines changed

2 files changed

+122
-48
lines changed

src/wp-includes/option.php

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -162,37 +162,46 @@ function get_option( $option, $default_value = false ) {
162162

163163
if ( ! wp_installing() ) {
164164
$alloptions = wp_load_alloptions();
165-
165+
/*
166+
* When getting an option value, we check in the following order for performance:
167+
*
168+
* 1. Check the 'alloptions' cache first to prioritize existing loaded options.
169+
* 2. Check the 'notoptions' cache before a cache lookup or DB hit.
170+
* 3. Check the 'options' cache prior to a DB hit.
171+
* 4. Check the DB for the option and cache it in either the 'options' or 'notoptions' cache.
172+
*/
166173
if ( isset( $alloptions[ $option ] ) ) {
167174
$value = $alloptions[ $option ];
168175
} else {
176+
// Check for non-existent options first to avoid unnecessary object cache lookups and DB hits.
177+
$notoptions = wp_cache_get( 'notoptions', 'options' );
178+
179+
if ( ! is_array( $notoptions ) ) {
180+
$notoptions = array();
181+
wp_cache_set( 'notoptions', $notoptions, 'options' );
182+
}
183+
184+
if ( isset( $notoptions[ $option ] ) ) {
185+
/**
186+
* Filters the default value for an option.
187+
*
188+
* The dynamic portion of the hook name, `$option`, refers to the option name.
189+
*
190+
* @since 3.4.0
191+
* @since 4.4.0 The `$option` parameter was added.
192+
* @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value.
193+
*
194+
* @param mixed $default_value The default value to return if the option does not exist
195+
* in the database.
196+
* @param string $option Option name.
197+
* @param bool $passed_default Was `get_option()` passed a default value?
198+
*/
199+
return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default );
200+
}
201+
169202
$value = wp_cache_get( $option, 'options' );
170203

171204
if ( false === $value ) {
172-
// Prevent non-existent options from triggering multiple queries.
173-
$notoptions = wp_cache_get( 'notoptions', 'options' );
174-
175-
// Prevent non-existent `notoptions` key from triggering multiple key lookups.
176-
if ( ! is_array( $notoptions ) ) {
177-
$notoptions = array();
178-
wp_cache_set( 'notoptions', $notoptions, 'options' );
179-
} elseif ( isset( $notoptions[ $option ] ) ) {
180-
/**
181-
* Filters the default value for an option.
182-
*
183-
* The dynamic portion of the hook name, `$option`, refers to the option name.
184-
*
185-
* @since 3.4.0
186-
* @since 4.4.0 The `$option` parameter was added.
187-
* @since 4.7.0 The `$passed_default` parameter was added to distinguish between a `false` value and the default parameter value.
188-
*
189-
* @param mixed $default_value The default value to return if the option does not exist
190-
* in the database.
191-
* @param string $option Option name.
192-
* @param bool $passed_default Was `get_option()` passed a default value?
193-
*/
194-
return apply_filters( "default_option_{$option}", $default_value, $option, $passed_default );
195-
}
196205

197206
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
198207

tests/phpunit/tests/option/option.php

Lines changed: 88 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ public function test_get_option_notoptions_cache() {
112112
wp_cache_set( 'notoptions', $notoptions, 'options' );
113113

114114
$before = get_num_queries();
115-
$value = get_option( 'invalid' );
116-
$after = get_num_queries();
115+
get_option( 'invalid' );
116+
$after = get_num_queries();
117117

118118
$this->assertSame( 0, $after - $before );
119119
}
@@ -127,8 +127,8 @@ public function test_get_option_notoptions_set_cache() {
127127
get_option( 'invalid' );
128128

129129
$before = get_num_queries();
130-
$value = get_option( 'invalid' );
131-
$after = get_num_queries();
130+
get_option( 'invalid' );
131+
$after = get_num_queries();
132132

133133
$notoptions = wp_cache_get( 'notoptions', 'options' );
134134

@@ -137,25 +137,6 @@ public function test_get_option_notoptions_set_cache() {
137137
$this->assertArrayHasKey( 'invalid', $notoptions, 'The "invalid" option should be in the notoptions cache.' );
138138
}
139139

140-
/**
141-
* @ticket 58277
142-
*
143-
* @covers ::get_option
144-
*/
145-
public function test_get_option_notoptions_do_not_load_cache() {
146-
add_option( 'foo', 'bar', '', false );
147-
wp_cache_delete( 'notoptions', 'options' );
148-
149-
$before = get_num_queries();
150-
$value = get_option( 'foo' );
151-
$after = get_num_queries();
152-
153-
$notoptions = wp_cache_get( 'notoptions', 'options' );
154-
155-
$this->assertSame( 0, $after - $before, 'The options cache was not hit on the second call to `get_option()`.' );
156-
$this->assertFalse( $notoptions, 'The notoptions cache should not be set.' );
157-
}
158-
159140
/**
160141
* @covers ::get_option
161142
* @covers ::add_option
@@ -548,4 +529,88 @@ public function test_add_option_clears_the_notoptions_cache() {
548529
$updated_notoptions = wp_cache_get( 'notoptions', 'options' );
549530
$this->assertArrayNotHasKey( $option_name, $updated_notoptions, 'The "foobar" option should not be in the notoptions cache after adding it.' );
550531
}
532+
533+
/**
534+
* Test that get_option() does not hit the external cache multiple times for the same option.
535+
*
536+
* @ticket 62692
537+
*
538+
* @covers ::get_option
539+
*
540+
* @dataProvider data_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option
541+
*
542+
* @param int $expected_connections Expected number of connections to the memcached server.
543+
* @param bool $option_exists Whether the option should be set. Default true.
544+
* @param string $autoload Whether the option should be auto loaded. Default true.
545+
*/
546+
public function test_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option( $expected_connections, $option_exists = true, $autoload = true ) {
547+
if ( ! wp_using_ext_object_cache() ) {
548+
$this->markTestSkipped( 'This test requires an external object cache.' );
549+
}
550+
551+
if ( false === $this->helper_object_cache_stats_cmd_get() ) {
552+
$this->markTestSkipped( 'This test requires access to the number of get requests to the external object cache.' );
553+
}
554+
555+
if ( $option_exists ) {
556+
add_option( 'ticket-62692', 'value', '', $autoload );
557+
}
558+
559+
wp_cache_delete_multiple( array( 'ticket-62692', 'notoptions', 'alloptions' ), 'options' );
560+
561+
$connections_start = $this->helper_object_cache_stats_cmd_get();
562+
563+
$call_getter = 10;
564+
while ( $call_getter-- ) {
565+
get_option( 'ticket-62692' );
566+
}
567+
568+
$connections_end = $this->helper_object_cache_stats_cmd_get();
569+
570+
$this->assertSame( $expected_connections, $connections_end - $connections_start );
571+
}
572+
573+
/**
574+
* Data provider.
575+
*
576+
* @return array[]
577+
*/
578+
public function data_get_option_does_not_hit_the_external_cache_multiple_times_for_the_same_option() {
579+
return array(
580+
'exists, autoload' => array( 1, true, true ),
581+
'exists, not autoloaded' => array( 3, true, false ),
582+
'does not exist' => array( 3, false ),
583+
);
584+
}
585+
586+
/**
587+
* Helper function to get the number of get commands from the external object cache.
588+
*
589+
* @return int|false Number of get command calls, false if unavailable.
590+
*/
591+
public function helper_object_cache_stats_cmd_get() {
592+
if ( ! wp_using_ext_object_cache() || ! function_exists( 'wp_cache_get_stats' ) ) {
593+
return false;
594+
}
595+
596+
$stats = wp_cache_get_stats();
597+
598+
// Check the shape of the stats.
599+
if ( ! is_array( $stats ) ) {
600+
return false;
601+
}
602+
603+
// Get the first server's stats.
604+
$stats = array_shift( $stats );
605+
606+
if ( ! is_array( $stats ) ) {
607+
return false;
608+
}
609+
610+
if ( ! array_key_exists( 'cmd_get', $stats ) ) {
611+
return false;
612+
}
613+
614+
return $stats['cmd_get'];
615+
}
551616
}

0 commit comments

Comments
 (0)