From 6662cb0bef6bdd3fd2aa02db92f5781a4b5db657 Mon Sep 17 00:00:00 2001 From: Jonny Harris Date: Tue, 21 May 2024 21:36:43 +0000 Subject: [PATCH] Options, Meta APIs: Introduce wp_prime_network_option_caches() to load multiple network options with a single database request. WordPress's `get_network_option` function generally makes individual database requests for each network option. While some options are preloaded in `wp_load_core_site_options`, many still require single database calls to the network options table. Building on the work done in [56445], [56990], and [57013], which introduced the `wp_prime_option_caches` function, this commit adds two new functions: `wp_prime_network_option_caches` and `wp_prime_site_option_caches`. These functions enable developers to pass an array of option names, allowing caches for these options to be primed in a single object cache or database request. If an option is not found, the notoptions cache key is refreshed, preventing unnecessary repeated requests. The function `wp_prime_site_option_caches` is similar to `get_site_option`, enabling developers to retrieve network options on the current network without needing to know the current network ID. If these functions are called in a non-multisite environment, they fall back to using wp_prime_option_caches. These functions have been implemented in `wp_load_core_site_options`, `get_site_transient`, and `set_site_transient`. Props to spacedmonkey, peterwilsoncc, mukesh27, joemcgill. Fixes #61053. git-svn-id: https://develop.svn.wordpress.org/trunk@58182 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/option.php | 149 +++++++-- .../option/wpPrimeNetworkOptionCaches.php | 302 ++++++++++++++++++ 2 files changed, 428 insertions(+), 23 deletions(-) create mode 100644 tests/phpunit/tests/option/wpPrimeNetworkOptionCaches.php diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php index 344dfee088cb0..83f282d5c059f 100644 --- a/src/wp-includes/option.php +++ b/src/wp-includes/option.php @@ -643,50 +643,150 @@ function wp_load_alloptions( $force_cache = false ) { } /** - * Loads and primes caches of certain often requested network options if is_multisite(). + * Primes specific network options for the current network into the cache with a single database query. * - * @since 3.0.0 - * @since 6.3.0 Also prime caches for network options when persistent object cache is enabled. + * Only network options that do not already exist in cache will be loaded. + * + * If site is not multisite, then call wp_prime_option_caches(). + * + * @since 6.6.0 + * + * @see wp_prime_network_option_caches() + * + * @param string[] $options An array of option names to be loaded. + */ +function wp_prime_site_option_caches( array $options ) { + wp_prime_network_option_caches( null, $options ); +} + +/** + * Primes specific network options into the cache with a single database query. + * + * Only network options that do not already exist in cache will be loaded. + * + * If site is not multisite, then call wp_prime_option_caches(). + * + * @since 6.6.0 * * @global wpdb $wpdb WordPress database abstraction object. * - * @param int $network_id Optional. Network ID of network for which to prime network options cache. Defaults to current network. + * @param int $network_id ID of the network. Can be null to default to the current network ID. + * @param string[] $options An array of option names to be loaded. */ -function wp_load_core_site_options( $network_id = null ) { +function wp_prime_network_option_caches( $network_id, array $options ) { global $wpdb; - if ( ! is_multisite() || wp_installing() ) { + if ( wp_installing() ) { + return; + } + + if ( ! is_multisite() ) { + wp_prime_option_caches( $options ); + return; + } + + if ( $network_id && ! is_numeric( $network_id ) ) { return; } - if ( empty( $network_id ) ) { + $network_id = (int) $network_id; + + // Fallback to the current network if a network ID is not specified. + if ( ! $network_id ) { $network_id = get_current_network_id(); } - $core_options = array( 'site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled', 'ms_files_rewriting' ); + $cache_keys = array(); + foreach ( $options as $option ) { + $cache_keys[ $option ] = "{$network_id}:{$option}"; + } + + $cache_group = 'site-options'; + $cached_options = wp_cache_get_multiple( array_values( $cache_keys ), $cache_group ); - if ( wp_using_ext_object_cache() ) { - $cache_keys = array(); - foreach ( $core_options as $option ) { - $cache_keys[] = "{$network_id}:{$option}"; + $notoptions_key = "$network_id:notoptions"; + $notoptions = wp_cache_get( $notoptions_key, $cache_group ); + + if ( ! is_array( $notoptions ) ) { + $notoptions = array(); + } + + // Filter options that are not in the cache. + $options_to_prime = array(); + foreach ( $cache_keys as $option => $cache_key ) { + if ( + ( ! isset( $cached_options[ $cache_key ] ) || false === $cached_options[ $cache_key ] ) + && ! isset( $notoptions[ $option ] ) + ) { + $options_to_prime[] = $option; } - wp_cache_get_multiple( $cache_keys, 'site-options' ); + } + // Bail early if there are no options to be loaded. + if ( empty( $options_to_prime ) ) { return; } - $core_options_in = "'" . implode( "', '", $core_options ) . "'"; - $options = $wpdb->get_results( $wpdb->prepare( "SELECT meta_key, meta_value FROM $wpdb->sitemeta WHERE meta_key IN ($core_options_in) AND site_id = %d", $network_id ) ); + $query_args = $options_to_prime; + $query_args[] = $network_id; + $results = $wpdb->get_results( + $wpdb->prepare( + sprintf( + "SELECT meta_key, meta_value FROM $wpdb->sitemeta WHERE meta_key IN (%s) AND site_id = %s", + implode( ',', array_fill( 0, count( $options_to_prime ), '%s' ) ), + '%d' + ), + $query_args + ) + ); - $data = array(); - foreach ( $options as $option ) { - $key = $option->meta_key; - $cache_key = "{$network_id}:$key"; - $option->meta_value = maybe_unserialize( $option->meta_value ); + $data = array(); + $options_found = array(); + foreach ( $results as $result ) { + $key = $result->meta_key; + $cache_key = $cache_keys[ $key ]; + $data[ $cache_key ] = maybe_unserialize( $result->meta_value ); + $options_found[] = $key; + } + wp_cache_set_multiple( $data, $cache_group ); + // If all options were found, no need to update `notoptions` cache. + if ( count( $options_found ) === count( $options_to_prime ) ) { + return; + } + + $options_not_found = array_diff( $options_to_prime, $options_found ); - $data[ $cache_key ] = $option->meta_value; + // Add the options that were not found to the cache. + $update_notoptions = false; + foreach ( $options_not_found as $option_name ) { + if ( ! isset( $notoptions[ $option_name ] ) ) { + $notoptions[ $option_name ] = true; + $update_notoptions = true; + } + } + + // Only update the cache if it was modified. + if ( $update_notoptions ) { + wp_cache_set( $notoptions_key, $notoptions, $cache_group ); + } +} + +/** + * Loads and primes caches of certain often requested network options if is_multisite(). + * + * @since 3.0.0 + * @since 6.3.0 Also prime caches for network options when persistent object cache is enabled. + * @since 6.6.0 Uses wp_prime_network_option_caches(). + * + * @param int $network_id Optional. Network ID of network for which to prime network options cache. Defaults to current network. + */ +function wp_load_core_site_options( $network_id = null ) { + if ( ! is_multisite() || wp_installing() ) { + return; } - wp_cache_set_multiple( $data, 'site-options' ); + $core_options = array( 'site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled', 'ms_files_rewriting', 'WPLANG' ); + + wp_prime_network_option_caches( $network_id, $core_options ); } /** @@ -2415,7 +2515,9 @@ function get_site_transient( $transient ) { $transient_option = '_site_transient_' . $transient; if ( ! in_array( $transient, $no_timeout, true ) ) { $transient_timeout = '_site_transient_timeout_' . $transient; - $timeout = get_site_option( $transient_timeout ); + wp_prime_site_option_caches( array( $transient_option, $transient_timeout ) ); + + $timeout = get_site_option( $transient_timeout ); if ( false !== $timeout && $timeout < time() ) { delete_site_option( $transient_option ); delete_site_option( $transient_timeout ); @@ -2493,6 +2595,7 @@ function set_site_transient( $transient, $value, $expiration = 0 ) { } else { $transient_timeout = '_site_transient_timeout_' . $transient; $option = '_site_transient_' . $transient; + wp_prime_site_option_caches( array( $option, $transient_timeout ) ); if ( false === get_site_option( $option ) ) { if ( $expiration ) { diff --git a/tests/phpunit/tests/option/wpPrimeNetworkOptionCaches.php b/tests/phpunit/tests/option/wpPrimeNetworkOptionCaches.php new file mode 100644 index 0000000000000..a18fc2e0c0202 --- /dev/null +++ b/tests/phpunit/tests/option/wpPrimeNetworkOptionCaches.php @@ -0,0 +1,302 @@ +network->create( + array( + 'domain' => 'wordpress.org', + 'path' => '/', + ) + ); + } + } + + /** + * Tests that wp_prime_network_option_caches() primes multiple options. + * + * @ticket 61053 + */ + public function test_wp_prime_network_option_caches() { + // Create some options to prime. + $network_id = get_current_network_id(); + if ( is_multisite() ) { + $cache_group = 'site-options'; + } else { + $cache_group = 'options'; + } + + // Create some options to prime. + $options_to_prime = array( + 'option1', + 'option2', + 'option3', + ); + + $cache_keys = array(); + foreach ( $options_to_prime as $option ) { + if ( is_multisite() ) { + $cache_key = "$network_id:$option"; + } else { + $cache_key = $option; + } + $cache_keys[ $option ] = $cache_key; + } + + /* + * Set values for the options, + * clear the cache for the options, + * check options are not in cache initially. + */ + foreach ( $cache_keys as $option => $cache_key ) { + update_network_option( $network_id, $option, "value_$option" ); + wp_cache_delete( $cache_key, $cache_group ); + $this->assertFalse( wp_cache_get( $cache_key, $cache_group ), "$option was not deleted from the cache." ); + } + + // Call the wp_prime_network_option_caches() function to prime the options. + wp_prime_network_option_caches( $network_id, $options_to_prime ); + + // Store the initial database query count. + $initial_query_count = get_num_queries(); + + // Check that options are only in the 'options' or 'site-options' cache group. + foreach ( $cache_keys as $option => $cache_key ) { + $this->assertSame( "value_$option", wp_cache_get( $cache_key, $cache_group ), "$option cache is not primed" ); + $this->assertSame( + "value_$option", + get_network_option( $network_id, $option ), + "$option has not been loaded" + ); + } + + // Ensure no additional database queries were made. + $this->assertSame( + $initial_query_count, + get_num_queries(), + 'Additional database queries were made.' + ); + } + + /** + * Tests that running wp_prime_network_option_caches() twice does not requery options. + * + * @ticket 61053 + */ + public function test_wp_prime_network_option_caches_run_twice() { + // Create some options to prime. + $network_id = get_current_network_id(); + if ( is_multisite() ) { + $cache_group = 'site-options'; + } else { + $cache_group = 'options'; + } + + // Create some options to prime. + $options_to_prime = array( + 'option1', + 'option2', + 'option3', + ); + + $cache_keys = array(); + foreach ( $options_to_prime as $option ) { + if ( is_multisite() ) { + $cache_key = "$network_id:$option"; + } else { + $cache_key = $option; + } + $cache_keys[ $option ] = $cache_key; + } + + /* + * Set values for the options, + * clear the cache for the options, + * check options are not in cache initially. + */ + foreach ( $cache_keys as $option => $cache_key ) { + update_network_option( $network_id, $option, "value_$option" ); + wp_cache_delete( $cache_key, $cache_group ); + $this->assertFalse( wp_cache_get( $cache_key, $cache_group ), "$option was not deleted from the cache." ); + } + + // Call the wp_prime_network_option_caches() function to prime the options. + wp_prime_network_option_caches( $network_id, $options_to_prime ); + + // Store the initial database query count. + $initial_query_count = get_num_queries(); + + // Call the wp_prime_network_option_caches() function second time + wp_prime_network_option_caches( $network_id, $options_to_prime ); + + // Ensure no additional database queries were made. + $this->assertSame( + $initial_query_count, + get_num_queries(), + 'Additional database queries were made.' + ); + } + + /** + * Tests that wp_prime_network_option_caches() handles a mix of primed and unprimed options. + * + * @ticket 61053 + */ + public function test_wp_prime_network_option_caches_handles_a_mix_of_primed_and_unprimed_options() { + // Create some options to prime. + $options_to_prime = array( + 'option1', + 'option2', + 'option3', + ); + + $network_id = get_current_network_id(); + if ( is_multisite() ) { + $cache_group = 'site-options'; + } else { + $cache_group = 'options'; + } + + $cache_keys = array(); + foreach ( $options_to_prime as $option ) { + if ( is_multisite() ) { + $cache_key = "$network_id:$option"; + } else { + $cache_key = $option; + } + $cache_keys[ $option ] = $cache_key; + } + + /* + * Set values for the options, + * clear the cache for the options, + * check options are not in cache initially. + */ + foreach ( $cache_keys as $option => $cache_key ) { + update_network_option( $network_id, $option, "value_$option" ); + wp_cache_delete( $cache_key, $cache_group ); + $this->assertFalse( wp_cache_get( $cache_key, $cache_group ), "$option was not deleted from the cache." ); + } + + // Add non-existent option to the options to prime. + $options_to_prime[] = 'option404notfound'; + + // Prime the first option with a non-existent option. + wp_prime_network_option_caches( $network_id, $options_to_prime ); + + array_pop( $options_to_prime ); + + // Store the initial database query count. + $initial_query_count = get_num_queries(); + + // Check that options are only in the 'options' or 'site-options' cache group. + foreach ( $cache_keys as $option => $cache_key ) { + $this->assertSame( "value_$option", wp_cache_get( $cache_key, $cache_group ), "$option cache is not primed" ); + $this->assertSame( + "value_$option", + get_network_option( $network_id, $option ), + "$option has not been loaded" + ); + } + + $this->assertFalse( get_network_option( $network_id, 'option404notfound' ), 'option404notfound should return false as option does not exist' ); + + // Ensure no additional database queries were made. + $this->assertSame( + $initial_query_count, + get_num_queries(), + 'Additional database queries were made.' + ); + } + + /** + * Test prime options on a different network. + * + * @group ms-required + * + * @ticket 61053 + */ + public function test_wp_prime_network_option_caches_no_exists_cache() { + $options_to_prime = array( + 'option1', + 'option2', + 'option3', + ); + + // Call the wp_prime_network_option_caches() function to prime the options. + wp_prime_network_option_caches( self::$different_network_id, $options_to_prime ); + + $notoptions_key = self::$different_network_id . ':notoptions'; + $expected = array_fill_keys( $options_to_prime, true ); + $this->assertSame( $expected, wp_cache_get( $notoptions_key, 'site-options' ) ); + } + + /** + * Test prime options on a different network. + * + * @group ms-required + * + * @ticket 61053 + */ + public function test_wp_prime_network_option_caches_multiple_networks() { + $network_id = get_current_network_id(); + $cache_group = 'site-options'; + + // Create some options to prime. + $options_to_prime = array( + 'option1', + 'option2', + 'option3', + ); + + $cache_keys = array(); + foreach ( $options_to_prime as $option ) { + $cache_key = "$network_id:$option"; + $cache_keys[ $option ] = $cache_key; + } + + /* + * Set values for the options, + * clear the cache for the options, + * check options are not in cache initially. + */ + foreach ( $cache_keys as $option => $cache_key ) { + update_network_option( $network_id, $option, "value_$option" ); + wp_cache_delete( $cache_key, $cache_group ); + $this->assertFalse( wp_cache_get( $cache_key, $cache_group ), "$option was not deleted from the cache." ); + } + + // Call the wp_prime_network_option_caches() function to prime the options. + wp_prime_network_option_caches( self::$different_network_id, $options_to_prime ); + + // Store the initial database query count. + $initial_query_count = get_num_queries(); + + foreach ( $cache_keys as $option => $cache_key ) { + $this->assertFalse( wp_cache_get( $cache_key, $cache_group ), "$option cache should be false" ); + $this->assertFalse( + get_network_option( self::$different_network_id, $option ), + "$option has not been loaded" + ); + } + + // Ensure no additional database queries were made. + $this->assertSame( + $initial_query_count, + get_num_queries(), + 'Additional database queries were made.' + ); + } +}