[wp-trac] [WordPress Trac] #52798: delete_option() does not clear cache if option is missing in database.

WordPress Trac noreply at wordpress.org
Fri Mar 12 19:31:51 UTC 2021


#52798: delete_option() does not clear cache if option is missing in database.
--------------------------------+-----------------------------
 Reporter:  emrikol             |      Owner:  (none)
     Type:  defect (bug)        |     Status:  new
 Priority:  normal              |  Milestone:  Awaiting Review
Component:  Options, Meta APIs  |    Version:  trunk
 Severity:  minor               |   Keywords:  2nd-opinion
  Focuses:                      |
--------------------------------+-----------------------------
 It's an old one, but this is the mirror of #25015.

 If, for some reason, an object does not exist in the database, but does
 exist in persistent object cache, calling `delete_option()` will fail and
 the cached option will continue to exist:

 {{{
 wp> get_option( 'test-option' );
 => bool(false)
 wp> update_option( 'test-option', 'example', false );
 => bool(true)
 wp> get_option( 'test-option' );
 => string(7) "example"
 wp> global $wpdb;
 wp> $wpdb->get_results( 'SELECT * FROM wp_options WHERE option_name="test-
 option"' );
 => array(1) {
   [0]=>
   object(stdClass)#1977 (4) {
     ["option_id"]=>
     string(3) "121"
     ["option_name"]=>
     string(11) "test-option"
     ["option_value"]=>
     string(7) "example"
     ["autoload"]=>
     string(2) "no"
   }
 }
 wp> $wpdb->delete( $wpdb->options, array( 'option_name' => 'test-option' )
 );
 => int(1)
 wp> $wpdb->get_results( 'SELECT * FROM wp_options WHERE option_name="test-
 option"' );
 => array(0) {
 }
 wp> get_option( 'test-option' );
 => string(7) "example"
 wp> delete_option( 'test-option' );
 => bool(false)
 wp> get_option( 'test-option' );
 => string(7) "example"
 }}}

 I know what you're thinking, "Don't ever delete directly on the database."
 But recently when I ran into this, manual deletion wasn't the cause.  More
 than likely it was some sort of race condition between distributed
 database or cache servers, or other arcane edge cases.  The end result
 though was that the cache and database were out of sync and subsequent
 automated `delete_option()` calls were silently failing to clear the
 cache.

 The easiest solution would be to move the cache deletion before the
 database check:

 {{{
 diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php
 index 8692db7199..2f4a000ca1 100644
 --- a/src/wp-includes/option.php
 +++ b/src/wp-includes/option.php
 @@ -648,6 +648,18 @@ function delete_option( $option ) {

         wp_protect_special_option( $option );

 +       if ( ! wp_installing() ) {
 +               if ( 'yes' === $row->autoload ) {
 +                       $alloptions = wp_load_alloptions( true );
 +                       if ( is_array( $alloptions ) && isset(
 $alloptions[ $option ] ) ) {
 +                               unset( $alloptions[ $option ] );
 +                               wp_cache_set( 'alloptions', $alloptions,
 'options' );
 +                       }
 +               } else {
 +                       wp_cache_delete( $option, 'options' );
 +               }
 +       }
 +
         // Get the ID, if no ID then return.
         $row = $wpdb->get_row( $wpdb->prepare( "SELECT autoload FROM
 $wpdb->options WHERE option_name = %s", $option ) );
         if ( is_null( $row ) ) {
 @@ -665,18 +677,6 @@ function delete_option( $option ) {

         $result = $wpdb->delete( $wpdb->options, array( 'option_name' =>
 $option ) );

 -       if ( ! wp_installing() ) {
 -               if ( 'yes' === $row->autoload ) {
 -                       $alloptions = wp_load_alloptions( true );
 -                       if ( is_array( $alloptions ) && isset(
 $alloptions[ $option ] ) ) {
 -                               unset( $alloptions[ $option ] );
 -                               wp_cache_set( 'alloptions', $alloptions,
 'options' );
 -                       }
 -               } else {
 -                       wp_cache_delete( $option, 'options' );
 -               }
 -       }
 -
         if ( $result ) {

                 /**
 }}}

 but that's also before the `delete_option` hook.  Instead, the cache
 purging logic block could be duplicated right before returning `false`:

 {{{
 diff --git a/src/wp-includes/option.php b/src/wp-includes/option.php
 index 8692db7199..ab0a3f79f2 100644
 --- a/src/wp-includes/option.php
 +++ b/src/wp-includes/option.php
 @@ -651,6 +651,19 @@ function delete_option( $option ) {
         // Get the ID, if no ID then return.
         $row = $wpdb->get_row( $wpdb->prepare( "SELECT autoload FROM
 $wpdb->options WHERE option_name = %s", $option ) );
         if ( is_null( $row ) ) {
 +
 +               if ( ! wp_installing() ) {
 +                       if ( 'yes' === $row->autoload ) {
 +                               $alloptions = wp_load_alloptions( true );
 +                               if ( is_array( $alloptions ) && isset(
 $alloptions[ $option ] ) ) {
 +                                       unset( $alloptions[ $option ] );
 +                                       wp_cache_set( 'alloptions',
 $alloptions, 'options' );
 +                               }
 +                       } else {
 +                               wp_cache_delete( $option, 'options' );
 +                       }
 +               }
 +
                 return false;
         }
 }}}

 but that's not very clean, with that much duplicate code.

 I'm also open to suggestions if this is even a "core" bug, or if it should
 be the responsibility of the theme/plugin code to make sure the option
 cache is properly cleared?

-- 
Ticket URL: <https://core.trac.wordpress.org/ticket/52798>
WordPress Trac <https://core.trac.wordpress.org/>
WordPress publishing platform


More information about the wp-trac mailing list