Options, Meta APIs: Use more sensible default for autoloading options which allows WordPress core to make a decision.

An excessive amount of autoloaded options is a common cause for slow database responses, sometimes caused by very large individual autoloaded options. As it is not mandatory to provide an autoload value when adding an option to the database, it tends to be ignored, which in combination with a default value of "yes" and lack of documentation can lead to the aforementioned problem.

This changeset enhances the option autoloading behavior in several ways:
* Update the function documentation to encourage the use of boolean `true` or `false` to explicitly provide an autoload value for an option.
* Use new string values `on` and `off` for explicitly provided values stored in the database, to distinguish them from `yes` and `no`, since `yes` does not allow determining whether it was set intentionally by the developer or only as a default.
* Effectively deprecate the values `yes` and `no`. They are still supported for backward compatibility, but now discouraged.
* Use `null` as new default autoload value for `add_option()`. If the developer does not provide an explicit value, this will now trigger WordPress logic to determine an autoload value to use:
    * If WordPress determines that the option should not be autoloaded, it is stored in the database as `auto-off`. As part of this changeset, the single heuristic introduced for that is to check whether the option size is larger than a threshold of 150k bytes. This threshold is filterable via a new `wp_max_autoloaded_option_size` filter.
    * If WordPress determines that the option should be autoloaded, it is stored in the database as `auto-on`. No logic to make such a decision is introduced as part of this changeset, but a new filter `wp_default_autoload_value` can be used to define such heuristics, e.g. by optimization plugins.
    * If WordPress cannot determine whether or not to autoload the option, it is stored in the database as `auto`.
    * This effectively means that any option without an explicit autoload value provided by the developer will be stored with an autoload value of `auto`, unless the option's size exceeds the aforementioned threshold. Options with a value of `auto` are still autoloaded as of today, most importantly for backward compatibility. A new function `wp_autoload_values_to_autoload()` returns the list of autolaod values that dictate for an option to be autoloaded, and a new filter `wp_autoload_values_to_autoload` can be used to alter that list.

These behavioral changes encourage developers to be more mindful of autoloading, while providing WordPress core and optimization plugins with additional control over heuristics for autoloading options where no explicit autoload value was provided.

At the same time, the changes are fully backward compatible from a functionality perspective, with the only exception being that very large options will now no longer be autoloaded if the developer did not explicitly request for them to be autoloaded. Neither WordPress core nor plugins are able to override an explicitly provided value, which is intentional to continue giving developers full control over their own options.

Props pbearne, flixos90, joemcgill, azaozz, spacedmonkey, swissspidy, mukesh27, markjaquith.
Fixes #42441.

Built from https://develop.svn.wordpress.org/trunk@57920


git-svn-id: http://core.svn.wordpress.org/trunk@57421 1a063a9b-81f0-0310-95a4-ce76da25c4cd
This commit is contained in:
Felix Arntz 2024-04-03 21:31:15 +00:00
parent 725f302121
commit 6fe9681e77
3 changed files with 177 additions and 47 deletions

View File

@ -286,6 +286,7 @@ foreach (
} }
// Misc filters. // Misc filters.
add_filter( 'wp_default_autoload_value', 'wp_filter_default_autoload_value_via_option_size', 10, 4 );
add_filter( 'option_ping_sites', 'privacy_ping_filter' ); add_filter( 'option_ping_sites', 'privacy_ping_filter' );
add_filter( 'option_blog_charset', '_wp_specialchars' ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop. add_filter( 'option_blog_charset', '_wp_specialchars' ); // IMPORTANT: This must not be wp_specialchars() or esc_html() or it'll cause an infinite loop.
add_filter( 'option_blog_charset', '_canonical_charset' ); add_filter( 'option_blog_charset', '_canonical_charset' );

View File

@ -392,16 +392,16 @@ function wp_set_option_autoload_values( array $options ) {
} }
$grouped_options = array( $grouped_options = array(
'yes' => array(), 'on' => array(),
'no' => array(), 'off' => array(),
); );
$results = array(); $results = array();
foreach ( $options as $option => $autoload ) { foreach ( $options as $option => $autoload ) {
wp_protect_special_option( $option ); // Ensure only valid options can be passed. wp_protect_special_option( $option ); // Ensure only valid options can be passed.
if ( 'no' === $autoload || false === $autoload ) { // Sanitize autoload value and categorize accordingly. if ( 'off' === $autoload || 'no' === $autoload || false === $autoload ) { // Sanitize autoload value and categorize accordingly.
$grouped_options['no'][] = $option; $grouped_options['off'][] = $option;
} else { } else {
$grouped_options['yes'][] = $option; $grouped_options['on'][] = $option;
} }
$results[ $option ] = false; // Initialize result value. $results[ $option ] = false; // Initialize result value.
} }
@ -465,19 +465,19 @@ function wp_set_option_autoload_values( array $options ) {
} }
/* /*
* If any options were changed to 'yes', delete their individual caches, and delete 'alloptions' cache so that it * If any options were changed to 'on', delete their individual caches, and delete 'alloptions' cache so that it
* is refreshed as needed. * is refreshed as needed.
* If no options were changed to 'yes' but any options were changed to 'no', delete them from the 'alloptions' * If no options were changed to 'on' but any options were changed to 'no', delete them from the 'alloptions'
* cache. This is not necessary when options were changed to 'yes', since in that situation the entire cache is * cache. This is not necessary when options were changed to 'on', since in that situation the entire cache is
* deleted anyway. * deleted anyway.
*/ */
if ( $grouped_options['yes'] ) { if ( $grouped_options['on'] ) {
wp_cache_delete_multiple( $grouped_options['yes'], 'options' ); wp_cache_delete_multiple( $grouped_options['on'], 'options' );
wp_cache_delete( 'alloptions', 'options' ); wp_cache_delete( 'alloptions', 'options' );
} elseif ( $grouped_options['no'] ) { } elseif ( $grouped_options['off'] ) {
$alloptions = wp_load_alloptions( true ); $alloptions = wp_load_alloptions( true );
foreach ( $grouped_options['no'] as $option ) { foreach ( $grouped_options['off'] as $option ) {
if ( isset( $alloptions[ $option ] ) ) { if ( isset( $alloptions[ $option ] ) ) {
unset( $alloptions[ $option ] ); unset( $alloptions[ $option ] );
} }
@ -606,7 +606,8 @@ function wp_load_alloptions( $force_cache = false ) {
if ( ! $alloptions ) { if ( ! $alloptions ) {
$suppress = $wpdb->suppress_errors(); $suppress = $wpdb->suppress_errors();
$alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload = 'yes'" ); $alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload IN ( '" . implode( "', '", wp_autoload_values_to_autoload() ) . "' )" );
if ( ! $alloptions_db ) { if ( ! $alloptions_db ) {
$alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options" ); $alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options" );
} }
@ -705,17 +706,21 @@ function wp_load_core_site_options( $network_id = null ) {
* *
* @global wpdb $wpdb WordPress database abstraction object. * @global wpdb $wpdb WordPress database abstraction object.
* *
* @param string $option Name of the option to update. Expected to not be SQL-escaped. * @param string $option Name of the option to update. Expected to not be SQL-escaped.
* @param mixed $value Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped. * @param mixed $value Option value. Must be serializable if non-scalar. Expected to not be SQL-escaped.
* @param string|bool $autoload Optional. Whether to load the option when WordPress starts up. For existing options, * @param bool|null $autoload Optional. Whether to load the option when WordPress starts up.
* `$autoload` can only be updated using `update_option()` if `$value` is also changed. * Accepts a boolean, or `null` to stick with the initial value or, if no initial value is set,
* Accepts 'yes'|true to enable or 'no'|false to disable. * to leave the decision up to default heuristics in WordPress.
* Autoloading too many options can lead to performance problems, especially if the * For existing options,
* options are not frequently used. For options which are accessed across several places * `$autoload` can only be updated using `update_option()` if `$value` is also changed.
* in the frontend, it is recommended to autoload them, by using 'yes'|true. * For backward compatibility 'yes' and 'no' are also accepted.
* For options which are accessed only on few specific URLs, it is recommended * Autoloading too many options can lead to performance problems, especially if the
* to not autoload them, by using 'no'|false. For non-existent options, the default value * options are not frequently used. For options which are accessed across several places
* is 'yes'. Default null. * in the frontend, it is recommended to autoload them, by using true.
* For options which are accessed only on few specific URLs, it is recommended
* to not autoload them, by using false.
* For non-existent options, the default is null, which means WordPress will determine
* the autoload value.
* @return bool True if the value was updated, false otherwise. * @return bool True if the value was updated, false otherwise.
*/ */
function update_option( $option, $value, $autoload = null ) { function update_option( $option, $value, $autoload = null ) {
@ -801,11 +806,6 @@ function update_option( $option, $value, $autoload = null ) {
/** This filter is documented in wp-includes/option.php */ /** This filter is documented in wp-includes/option.php */
if ( apply_filters( "default_option_{$option}", false, $option, false ) === $old_value ) { if ( apply_filters( "default_option_{$option}", false, $option, false ) === $old_value ) {
// Default setting for new options is 'yes'.
if ( null === $autoload ) {
$autoload = 'yes';
}
return add_option( $option, $value, '', $autoload ); return add_option( $option, $value, '', $autoload );
} }
@ -827,7 +827,17 @@ function update_option( $option, $value, $autoload = null ) {
); );
if ( null !== $autoload ) { if ( null !== $autoload ) {
$update_args['autoload'] = ( 'no' === $autoload || false === $autoload ) ? 'no' : 'yes'; $update_args['autoload'] = wp_determine_option_autoload_value( $option, $value, $serialized_value, $autoload );
} else {
// Retrieve the current autoload value to reevaluate it in case it was set automatically.
$raw_autoload = $wpdb->get_var( $wpdb->prepare( "SELECT autoload FROM $wpdb->options WHERE option_name = %s LIMIT 1", $option ) );
$allow_values = array( 'auto-on', 'auto-off', 'auto' );
if ( in_array( $raw_autoload, $allow_values, true ) ) {
$autoload = wp_determine_option_autoload_value( $option, $value, $serialized_value, $autoload );
if ( $autoload !== $raw_autoload ) {
$update_args['autoload'] = $autoload;
}
}
} }
$result = $wpdb->update( $wpdb->options, $update_args, array( 'option_name' => $option ) ); $result = $wpdb->update( $wpdb->options, $update_args, array( 'option_name' => $option ) );
@ -853,7 +863,7 @@ function update_option( $option, $value, $autoload = null ) {
} else { } else {
wp_cache_set( $option, $serialized_value, 'options' ); wp_cache_set( $option, $serialized_value, 'options' );
} }
} elseif ( 'yes' === $update_args['autoload'] ) { } elseif ( in_array( $update_args['autoload'], wp_autoload_values_to_autoload(), true ) ) {
// Delete the individual cache, then set in alloptions cache. // Delete the individual cache, then set in alloptions cache.
wp_cache_delete( $option, 'options' ); wp_cache_delete( $option, 'options' );
@ -915,23 +925,26 @@ function update_option( $option, $value, $autoload = null ) {
* options the same as the ones which are protected. * options the same as the ones which are protected.
* *
* @since 1.0.0 * @since 1.0.0
* @since 6.6.0 The $autoload parameter's default value was changed to null.
* *
* @global wpdb $wpdb WordPress database abstraction object. * @global wpdb $wpdb WordPress database abstraction object.
* *
* @param string $option Name of the option to add. Expected to not be SQL-escaped. * @param string $option Name of the option to add. Expected to not be SQL-escaped.
* @param mixed $value Optional. Option value. Must be serializable if non-scalar. * @param mixed $value Optional. Option value. Must be serializable if non-scalar.
* Expected to not be SQL-escaped. * Expected to not be SQL-escaped.
* @param string $deprecated Optional. Description. Not used anymore. * @param string $deprecated Optional. Description. Not used anymore.
* @param string|bool $autoload Optional. Whether to load the option when WordPress starts up. * @param bool|null $autoload Optional. Whether to load the option when WordPress starts up.
* Accepts 'yes'|true to enable or 'no'|false to disable. * Accepts a boolean, or `null` to leave the decision up to default heuristics in WordPress.
* Autoloading too many options can lead to performance problems, especially if the * For backward compatibility 'yes' and 'no' are also accepted.
* options are not frequently used. For options which are accessed across several places * Autoloading too many options can lead to performance problems, especially if the
* in the frontend, it is recommended to autoload them, by using 'yes'|true. * options are not frequently used. For options which are accessed across several places
* For options which are accessed only on few specific URLs, it is recommended * in the frontend, it is recommended to autoload them, by using 'yes'|true.
* to not autoload them, by using 'no'|false. Default 'yes'. * For options which are accessed only on few specific URLs, it is recommended
* to not autoload them, by using false.
* Default is null, which means WordPress will determine the autoload value.
* @return bool True if the option was added, false otherwise. * @return bool True if the option was added, false otherwise.
*/ */
function add_option( $option, $value = '', $deprecated = '', $autoload = 'yes' ) { function add_option( $option, $value = '', $deprecated = '', $autoload = null ) {
global $wpdb; global $wpdb;
if ( ! empty( $deprecated ) ) { if ( ! empty( $deprecated ) ) {
@ -991,7 +1004,8 @@ function add_option( $option, $value = '', $deprecated = '', $autoload = 'yes' )
} }
$serialized_value = maybe_serialize( $value ); $serialized_value = maybe_serialize( $value );
$autoload = ( 'no' === $autoload || false === $autoload ) ? 'no' : 'yes';
$autoload = wp_determine_option_autoload_value( $option, $value, $serialized_value, $autoload );
/** /**
* Fires before an option is added. * Fires before an option is added.
@ -1009,7 +1023,7 @@ function add_option( $option, $value = '', $deprecated = '', $autoload = 'yes' )
} }
if ( ! wp_installing() ) { if ( ! wp_installing() ) {
if ( 'yes' === $autoload ) { if ( in_array( $autoload, wp_autoload_values_to_autoload(), true ) ) {
$alloptions = wp_load_alloptions( true ); $alloptions = wp_load_alloptions( true );
$alloptions[ $option ] = $serialized_value; $alloptions[ $option ] = $serialized_value;
wp_cache_set( 'alloptions', $alloptions, 'options' ); wp_cache_set( 'alloptions', $alloptions, 'options' );
@ -1093,7 +1107,7 @@ function delete_option( $option ) {
$result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option ) ); $result = $wpdb->delete( $wpdb->options, array( 'option_name' => $option ) );
if ( ! wp_installing() ) { if ( ! wp_installing() ) {
if ( 'yes' === $row->autoload ) { if ( in_array( $row->autoload, wp_autoload_values_to_autoload(), true ) ) {
$alloptions = wp_load_alloptions( true ); $alloptions = wp_load_alloptions( true );
if ( is_array( $alloptions ) && isset( $alloptions[ $option ] ) ) { if ( is_array( $alloptions ) && isset( $alloptions[ $option ] ) ) {
@ -1133,6 +1147,96 @@ function delete_option( $option ) {
return false; return false;
} }
/**
* Determines the appropriate autoload value for an option based on input.
*
* This function checks the provided autoload value and returns a standardized value
* ('on', 'off', 'auto-on', 'auto-off', or 'auto') based on specific conditions.
*
* If no explicit autoload value is provided, the function will check for certain heuristics around the given option.
* It will return `auto-on` to indicate autoloading, `auto-off` to indicate not autoloading, or `auto` if no clear
* decision could be made.
*
* @since 6.6.0
* @access private
*
* @param string $option The name of the option.
* @param mixed $value The value of the option to check its autoload value.
* @param mixed $serialized_value The serialized value of the option to check its autoload value.
* @param bool|null $autoload The autoload value to check.
* Accepts 'on'|true to enable or 'off'|false to disable, or
* 'auto-on', 'auto-off', or 'auto' for internal purposes.
* Any other autoload value will be forced to either 'auto-on',
* 'auto-off', or 'auto'.
* 'yes' and 'no' are supported for backward compatibility.
* @return string Returns the original $autoload value if explicit, or 'auto-on', 'auto-off',
* or 'auto' depending on default heuristics.
*/
function wp_determine_option_autoload_value( $option, $value, $serialized_value, $autoload ) {
// Check if autoload is a boolean.
if ( is_bool( $autoload ) ) {
return $autoload ? 'on' : 'off';
}
switch ( $autoload ) {
case 'on':
case 'yes':
return 'on';
case 'off':
case 'no':
return 'off';
}
/**
* Allows to determine the default autoload value for an option where no explicit value is passed.
*
* @since 6.6.0
*
* @param bool|null $autoload The default autoload value to set. Returning true will be set as 'auto-on' in the
* database, false will be set as 'auto-off', and null will be set as 'auto'.
* @param string $option The passed option name.
* @param mixed $value The passed option value to be saved.
*/
$autoload = apply_filters( 'wp_default_autoload_value', null, $option, $value, $serialized_value );
if ( is_bool( $autoload ) ) {
return $autoload ? 'auto-on' : 'auto-off';
}
return 'auto';
}
/**
* Filters the default autoload value to disable autoloading if the option value is too large.
*
* @since 6.6.0
* @access private
*
* @param bool|null $autoload The default autoload value to set.
* @param string $option The passed option name.
* @param mixed $value The passed option value to be saved.
* @param mixed $serialized_value The passed option value to be saved, in serialized form.
* @return bool|null Potentially modified $default.
*/
function wp_filter_default_autoload_value_via_option_size( $autoload, $option, $value, $serialized_value ) {
/**
* Filters the maximum size of option value in bytes.
*
* @since 6.6.0
*
* @param int $max_option_size The option-size threshold, in bytes. Default 150000.
* @param string $option The name of the option.
*/
$max_option_size = (int) apply_filters( 'wp_max_autoloaded_option_size', 150000, $option );
$size = ! empty( $serialized_value ) ? strlen( $serialized_value ) : 0;
if ( $size > $max_option_size ) {
return false;
}
return $autoload;
}
/** /**
* Deletes a transient. * Deletes a transient.
* *
@ -2924,3 +3028,28 @@ function filter_default_option( $default_value, $option, $passed_default ) {
return $registered[ $option ]['default']; return $registered[ $option ]['default'];
} }
/**
* Returns the values that trigger autoloading from the options table.
*
* @since 6.6.0
*
* @return array The values that trigger autoloading.
*/
function wp_autoload_values_to_autoload() {
$autoload_values = array( 'yes', 'on', 'auto-on', 'auto' );
/**
* Filters the autoload values that should be considered for autoloading from the options table.
*
* The filter can only be used to remove autoload values from the default list.
*
* @since 6.6.0
*
* @param array $autoload_values Autoload values used to autoload option.
* Default list contains 'yes', 'on', 'auto-on', and 'auto'.
*/
$filtered_values = apply_filters( 'wp_autoload_values_to_autoload', $autoload_values );
return array_intersect( $filtered_values, $autoload_values );
}

View File

@ -16,7 +16,7 @@
* *
* @global string $wp_version * @global string $wp_version
*/ */
$wp_version = '6.6-alpha-57919'; $wp_version = '6.6-alpha-57920';
/** /**
* Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema. * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.