Make WordPress Core

Changeset 61772


Ignore:
Timestamp:
02/28/2026 12:12:59 AM (6 months ago)
Author:
johnjamesjacoby
Message:

Cache API: Handle numerically-keyed global_groups when switching sites.

This commit protects against another multisite cache edge-case where a persistent object-cache drop-in plugin (namely memcached) may use a numerically-keyed global_groups array instead of 'key' => true like the default object-cache class, and includes the following changes:

  • Use wp_is_numeric_array() inside of wp_cache_switch_to_blog_fallback() so that the global group names array are always properly formatted regardless of the caching back-end in use
  • Add private helper methods to Tests_Multisite_WpCacheSwitchToBlogFallback to properly format global group names, and tweak a few tests to make them more resilient to different caching back-ends

Follow up to r61760.

See #23290.

Location:
trunk
Files:
2 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/ms-blogs.php

    r61760 r61772  
    615615        $non_persistent_groups = false;
    616616
    617         if ( is_object( $wp_object_cache ) && isset( $wp_object_cache->global_groups ) ) {
    618                 $global_groups         = array_keys( $wp_object_cache->global_groups );
    619                 $all_groups            = array_fill_keys( array_keys( $wp_object_cache->cache ), true );
    620                 $non_persistent_groups = array_keys( array_diff_key( $all_groups, $wp_object_cache->global_groups ) );
     617        if ( is_object( $wp_object_cache ) && isset( $wp_object_cache->global_groups ) && is_array( $wp_object_cache->global_groups ) ) {
     618
     619                // Get the global groups as they are.
     620                $group_names = $wp_object_cache->global_groups;
     621
     622                // Get global group keys if non-numeric array.
     623                if ( ! wp_is_numeric_array( $group_names ) ) {
     624                        $group_names = array_keys( $group_names );
     625                }
     626
     627                $global_groups = $group_names;
     628
     629                /*
     630                 * Non-persistent groups: Check for no_mc_groups first (memcached drop-in).
     631                 * Fall back to cache structure (default cache).
     632                 */
     633                if ( isset( $wp_object_cache->no_mc_groups ) && is_array( $wp_object_cache->no_mc_groups ) && ! empty( $wp_object_cache->no_mc_groups ) ) {
     634                        $non_persistent_groups = $wp_object_cache->no_mc_groups;
     635                } elseif ( isset( $wp_object_cache->cache ) && is_array( $wp_object_cache->cache ) ) {
     636                        $all_groups            = array_keys( $wp_object_cache->cache );
     637                        $non_persistent_groups = array_values( array_diff( $all_groups, $global_groups ) );
     638                }
    621639        }
    622640
  • trunk/tests/phpunit/tests/multisite/wpCacheSwitchToBlogFallback.php

    r61760 r61772  
    7373
    7474        /**
     75         * Normalizes global group names for both indexed and associative arrays.
     76         *
     77         * @param mixed $groups Group collection from the cache object.
     78         * @return string[]
     79         */
     80        private function get_global_group_names( $groups ) {
     81
     82                // Skip if empty groups.
     83                if ( ! is_array( $groups ) ) {
     84                        return array();
     85                }
     86
     87                // Use array values if numeric.
     88                if ( wp_is_numeric_array( $groups ) ) {
     89                        return array_values( $groups );
     90                }
     91
     92                // Default to array keys.
     93                return array_keys( $groups );
     94        }
     95
     96        /**
     97         * Asserts that a global group exists regardless of internal storage shape.
     98         *
     99         * @param string $group Group name.
     100         */
     101        private function assert_global_group_exists( $group ) {
     102                global $wp_object_cache;
     103
     104                $this->assertContains( $group, $this->get_global_group_names( $wp_object_cache->global_groups ) );
     105        }
     106
     107        /**
    75108         * Test that wp_cache_switch_to_blog() is always available.
    76109         *
     
    94127
    95128        /**
    96          * Test that wp_cache_switch_to_blog_fallback() reinitializes the cache.
    97          *
    98          * @ticket 23290
    99          * @covers ::wp_cache_switch_to_blog_fallback
    100          */
    101         public function test_reinitializes_cache() {
     129         * Test that cache remains functional after fallback reinitialization.
     130         *
     131         * The fallback reinitializes the cache object. This test verifies the cache
     132         * continues to work after reinitialization.
     133         *
     134         * @ticket 23290
     135         * @covers ::wp_cache_switch_to_blog_fallback
     136         */
     137        public function test_cache_remains_functional_after_fallback() {
    102138
    103139                // Set some cache data before switching.
     
    108144                $this->call_cache_switch_fallback();
    109145
    110                 // Verify the cache has been reinitialized (non-global groups should be cleared).
    111                 $this->assertFalse( wp_cache_get( 'test_key', 'test_group' ) );
     146                // Verify cache is reinitialized and remains functional.
     147                // Set new data in a non-global group after fallback.
     148                wp_cache_set( 'test_key', 'test_value_after_fallback', 'test_group' );
     149                $this->assertSame( 'test_value_after_fallback', wp_cache_get( 'test_key', 'test_group' ) );
    112150        }
    113151
     
    138176
    139177                // Verify key global groups are still present.
    140                 $this->assertArrayHasKey( 'users', $wp_object_cache->global_groups );
    141                 $this->assertArrayHasKey( 'user_meta', $wp_object_cache->global_groups );
    142                 $this->assertArrayHasKey( 'site-options', $wp_object_cache->global_groups );
     178                $this->assert_global_group_exists( 'users' );
     179                $this->assert_global_group_exists( 'user_meta' );
     180                $this->assert_global_group_exists( 'site-options' );
    143181        }
    144182
     
    197235                );
    198236
     237                $global_groups = $this->get_global_group_names( $wp_object_cache->global_groups );
     238
    199239                foreach ( $expected_groups as $group ) {
    200                         $this->assertContains( $group, array_keys( $wp_object_cache->global_groups ) );
     240                        $this->assertContains( $group, $global_groups );
    201241                }
    202242        }
    203243
    204244        /**
    205          * Test that wp_cache_switch_to_blog_fallback() preserves non-persistent groups from cache.
    206          *
    207          * When the fallback is called, it analyzes which groups exist in the cache->cache array
    208          * (excluding global groups) to determine non-persistent groups that should be preserved.
    209          * This test verifies that groups identified from the cache structure can still be used
    210          * after the fallback reinitializes.
    211          *
    212          * @ticket 23290
    213          * @covers ::wp_cache_switch_to_blog_fallback
    214          */
    215         public function test_preserves_non_persistent_groups_from_cache_object() {
    216                 global $wp_object_cache;
    217 
    218                 // Verify we have access to the cache structure.
    219                 $this->assertObjectHasProperty( 'cache', $wp_object_cache );
     245         * Test that non-persistent groups configuration is preserved after fallback.
     246         *
     247         * When the fallback is called, it attempts to preserve non-persistent group
     248         * configuration from the existing cache object, either via no_mc_groups for
     249         * memcached or by analyzing cache structure for default cache.
     250         *
     251         * This test verifies that groups remain usable after fallback.
     252         *
     253         * @ticket 23290
     254         * @covers ::wp_cache_switch_to_blog_fallback
     255         */
     256        public function test_preserves_non_persistent_groups_configuration() {
     257                global $wp_object_cache;
     258
     259                // Verify we have the global_groups property.
    220260                $this->assertObjectHasProperty( 'global_groups', $wp_object_cache );
    221261
     
    224264                wp_cache_set( 'plugin_key', 'plugin_data', 'plugins' );
    225265
    226                 // Verify they're in the cache structure before fallback.
    227                 $this->assertArrayHasKey( 'counts', $wp_object_cache->cache );
    228                 $this->assertArrayHasKey( 'plugins', $wp_object_cache->cache );
    229 
    230266                /*
    231                  * Call the fallback function, which should identify these groups
    232                  * from the cache structure before reinitializing.
     267                 * Call the fallback function, which should identify non-persistent groups
     268                 * and reinitialize the cache while preserving group configuration.
    233269                 */
    234270                $this->call_cache_switch_fallback();
     
    238274
    239275                /*
    240                  * The data is cleared (cache is reinitialized), but the groups should be re-addable.
     276                 * The groups should be preserved and re-addable after fallback.
    241277                 * Set new data in those same non-persistent groups to verify they're preserved.
    242278                 */
     
    345381
    346382        /**
    347          * Test that multiple calls to wp_cache_switch_to_blog_fallback() work correctly.
    348 
    349         /**
    350          * Test that non-global cache data is properly cleared during fallback.
    351          *
    352          * @ticket 23290
    353          */
    354         public function test_non_global_cache_data_is_cleared() {
     383         * Test that non-global cache groups remain writable after fallback.
     384         *
     385         * The fallback reinitializes the cache, which clears non-persistent group data.
     386         * This test verifies that non-global groups are still usable after reinitialization.
     387         *
     388         * @ticket 23290
     389         * @covers ::wp_cache_switch_to_blog_fallback
     390         */
     391        public function test_non_global_groups_remain_writable_after_fallback() {
    355392
    356393                // Set cache data in various non-global groups.
     
    369406                $this->call_cache_switch_fallback();
    370407
    371                 // Verify all non-global cache data is cleared.
    372                 $this->assertFalse( wp_cache_get( 'post_key', 'posts' ) );
    373                 $this->assertFalse( wp_cache_get( 'term_key', 'terms' ) );
    374                 $this->assertFalse( wp_cache_get( 'option_key', 'options' ) );
    375                 $this->assertFalse( wp_cache_get( 'default_key', 'default' ) );
     408                /*
     409                 * After fallback, verify non-global groups remain writable by setting
     410                 * new data. This tests the cache is functional after reinitialization.
     411                 */
     412                wp_cache_set( 'post_key', 'post_value_after_fallback', 'posts' );
     413                wp_cache_set( 'term_key', 'term_value_after_fallback', 'terms' );
     414                wp_cache_set( 'option_key', 'option_value_after_fallback', 'options' );
     415                wp_cache_set( 'default_key', 'default_value_after_fallback', 'default' );
     416
     417                $this->assertSame( 'post_value_after_fallback', wp_cache_get( 'post_key', 'posts' ) );
     418                $this->assertSame( 'term_value_after_fallback', wp_cache_get( 'term_key', 'terms' ) );
     419                $this->assertSame( 'option_value_after_fallback', wp_cache_get( 'option_key', 'options' ) );
     420                $this->assertSame( 'default_value_after_fallback', wp_cache_get( 'default_key', 'default' ) );
    376421        }
    377422
     
    385430         */
    386431        public function test_preserves_many_custom_global_groups() {
    387                 global $wp_object_cache;
    388432
    389433                // Add multiple custom global groups.
     
    400444                // Verify they were added.
    401445                foreach ( $custom_groups as $group ) {
    402                         $this->assertArrayHasKey( $group, $wp_object_cache->global_groups );
     446                        $this->assert_global_group_exists( $group );
    403447                }
    404448
     
    408452                // Verify all custom global groups are still configured.
    409453                foreach ( $custom_groups as $group ) {
    410                         $this->assertArrayHasKey( $group, $wp_object_cache->global_groups, "Custom group '{$group}' should be preserved." );
     454                        $this->assert_global_group_exists( $group );
    411455                }
    412456
     
    436480                // Should fall back to default global groups.
    437481                $this->assertNotEmpty( $wp_object_cache->global_groups );
    438                 $this->assertArrayHasKey( 'users', $wp_object_cache->global_groups );
     482                $this->assert_global_group_exists( 'users' );
    439483        }
    440484
     
    449493         */
    450494        public function test_preserves_both_global_and_non_persistent_groups() {
    451                 global $wp_object_cache;
    452495
    453496                // Add a custom global group.
     
    464507
    465508                // Global group configuration should be preserved.
    466                 $this->assertArrayHasKey( 'my_global', $wp_object_cache->global_groups );
     509                $this->assert_global_group_exists( 'my_global' );
    467510
    468511                // All groups should still be functional after fallback.
     
    512555
    513556                // Custom global groups should still be configured after switching.
    514                 global $wp_object_cache;
    515                 $this->assertArrayHasKey( 'my_plugin_users', $wp_object_cache->global_groups );
    516                 $this->assertArrayHasKey( 'my_plugin_settings', $wp_object_cache->global_groups );
     557                $this->assert_global_group_exists( 'my_plugin_users' );
     558                $this->assert_global_group_exists( 'my_plugin_settings' );
    517559        }
    518560
     
    534576                $this->call_cache_switch_fallback();
    535577
    536                 // After fallback, the cache is reinitialized, so data is cleared.
    537                 $this->assertFalse( wp_cache_get( 'my_key', 'posts' ) );
    538 
    539                 // But we can still set blog-specific data.
     578                // Cache should remain usable for blog-specific data after fallback.
    540579                wp_cache_set( 'my_key', 'new_value', 'posts' );
    541580                $this->assertSame( 'new_value', wp_cache_get( 'my_key', 'posts' ) );
     
    547586
    548587        /**
    549          * Test fallback correctly identifies non-persistent groups from cache structure.
    550          *
    551          * The fallback analyzes the existing cache structure to determine which groups
    552          * are non-persistent (not in global_groups). This test verifies that analysis works.
    553          *
    554          * @ticket 23290
    555          * @covers ::wp_cache_switch_to_blog_fallback
    556          */
    557         public function test_identifies_non_persistent_groups_from_cache() {
     588         * Test that previously-used non-persistent groups remain available after fallback.
     589         *
     590         * The fallback attempts to restore non-persistent group configuration.
     591         * This test verifies that groups which had data before fallback can be used again afterward.
     592         *
     593         * @ticket 23290
     594         * @covers ::wp_cache_switch_to_blog_fallback
     595         */
     596        public function test_previously_used_groups_remain_available_after_fallback() {
    558597                global $wp_object_cache;
    559598
     
    599638         */
    600639        public function test_ticket_23290_non_persistent_groups_are_maintained() {
    601                 global $wp_object_cache;
    602640
    603641                // Simulate a plugin adding a custom non-persistent group by populating the cache.
     
    605643                wp_cache_set( 'plugin_cache_2', 'data2', 'another_plugin' );
    606644
    607                 // These groups now exist in the cache structure.
    608                 $this->assertArrayHasKey( 'my_plugin_cache', $wp_object_cache->cache );
    609                 $this->assertArrayHasKey( 'another_plugin', $wp_object_cache->cache );
    610 
    611645                // Before fix: calling the fallback would lose these groups.
    612646                // After fix: they should be re-added to the cache configuration.
     
    632666         */
    633667        public function test_restore_current_blog_with_fallback() {
    634                 global $wp_object_cache;
    635668
    636669                $original_blog_id = get_current_blog_id();
     
    641674
    642675                // Store initial state.
    643                 $this->assertArrayHasKey( 'custom_for_restore', $wp_object_cache->global_groups );
     676                $this->assert_global_group_exists( 'custom_for_restore' );
    644677
    645678                // Switch to another blog.
     
    648681
    649682                // Verify custom global group is still there after switch.
    650                 $this->assertArrayHasKey( 'custom_for_restore', $wp_object_cache->global_groups );
     683                $this->assert_global_group_exists( 'custom_for_restore' );
    651684
    652685                // Restore to original blog.
     
    657690
    658691                // Custom global group configuration should still be present after restore.
    659                 $this->assertArrayHasKey( 'custom_for_restore', $wp_object_cache->global_groups );
     692                $this->assert_global_group_exists( 'custom_for_restore' );
    660693        }
    661694
     
    667700         */
    668701        public function test_nested_blog_switches_with_fallback() {
    669                 global $wp_object_cache;
    670702
    671703                $original_blog_id = get_current_blog_id();
     
    679711                switch_to_blog( $blog_id_1 );
    680712                $this->assertSame( $blog_id_1, get_current_blog_id() );
    681                 $this->assertArrayHasKey( 'nested_test_group', $wp_object_cache->global_groups );
     713                $this->assert_global_group_exists( 'nested_test_group' );
    682714
    683715                // Level 2: Switch to blog 2 from blog 1.
    684716                switch_to_blog( $blog_id_2 );
    685717                $this->assertSame( $blog_id_2, get_current_blog_id() );
    686                 $this->assertArrayHasKey( 'nested_test_group', $wp_object_cache->global_groups );
     718                $this->assert_global_group_exists( 'nested_test_group' );
    687719
    688720                // Restore from level 2 to level 1.
    689721                restore_current_blog();
    690722                $this->assertSame( $blog_id_1, get_current_blog_id() );
    691                 $this->assertArrayHasKey( 'nested_test_group', $wp_object_cache->global_groups );
     723                $this->assert_global_group_exists( 'nested_test_group' );
    692724
    693725                // Restore from level 1 to original.
    694726                restore_current_blog();
    695727                $this->assertSame( $original_blog_id, get_current_blog_id() );
    696                 $this->assertArrayHasKey( 'nested_test_group', $wp_object_cache->global_groups );
     728                $this->assert_global_group_exists( 'nested_test_group' );
    697729        }
    698730
     
    718750
    719751                        // Verify the group is still there after each call.
    720                         $this->assertArrayHasKey( 'rapid_test', $wp_object_cache->global_groups );
     752                        $this->assert_global_group_exists( 'rapid_test' );
    721753
    722754                        // The number of global groups should remain consistent.
     
    726758
    727759        /**
    728          * Test fallback behavior when wp_cache_add_global_groups() doesn't exist.
    729          *
    730          * Some cache drop-ins might not have the wp_cache_add_global_groups() function.
    731          * The fallback should handle this gracefully without errors.
    732          *
    733          * @ticket 23290
    734          * @covers ::wp_cache_switch_to_blog_fallback
    735          */
    736         public function test_handles_missing_wp_cache_add_global_groups_function() {
     760         * Test that fallback gracefully handles wp_cache_add_global_groups() being missing.
     761         *
     762         * The fallback uses function_exists() checks before calling wp_cache_add_global_groups().
     763         * This test verifies the fallback doesn't error out (in case a drop-in lacks this function).
     764         *
     765         * @ticket 23290
     766         * @covers ::wp_cache_switch_to_blog_fallback
     767         */
     768        public function test_fallback_works_when_wp_cache_add_global_groups_may_not_exist() {
    737769                global $wp_object_cache;
    738770
     
    762794
    763795        /**
    764          * Test fallback behavior when wp_cache_add_non_persistent_groups() doesn't exist.
    765          *
    766          * Some cache drop-ins might not have the wp_cache_add_non_persistent_groups() function.
    767          * The fallback should handle this gracefully without errors.
    768          *
    769          * @ticket 23290
    770          * @covers ::wp_cache_switch_to_blog_fallback
    771          */
    772         public function test_handles_missing_wp_cache_add_non_persistent_groups_function() {
     796         * Test that fallback gracefully handles wp_cache_add_non_persistent_groups() being missing.
     797         *
     798         * The fallback uses function_exists() checks before calling wp_cache_add_non_persistent_groups().
     799         * This test verifies the fallback doesn't error out (in case a drop-in lacks this function).
     800         *
     801         * @ticket 23290
     802         * @covers ::wp_cache_switch_to_blog_fallback
     803         */
     804        public function test_fallback_works_when_wp_cache_add_non_persistent_groups_may_not_exist() {
    773805                global $wp_object_cache;
    774806
     
    918950                $this->call_cache_switch_fallback();
    919951
    920                 // Regular non-global cache data should be cleared.
    921                 $this->assertFalse( wp_cache_get( 'regular_cache_key', 'custom_group' ) );
     952                // Regular non-global cache groups should remain usable.
     953                wp_cache_set( 'regular_cache_key', 'regular_cache_value_after_fallback', 'custom_group' );
     954                $this->assertSame( 'regular_cache_value_after_fallback', wp_cache_get( 'regular_cache_key', 'custom_group' ) );
    922955
    923956                // Site transients should persist (they're in the global 'site-transient' group).
Note: See TracChangeset for help on using the changeset viewer.