Make WordPress Core

Changeset 61772

Timestamp:
02/28/2026 12:12:59 AM (5 days 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
     76
     77
     78
     79
     80
     81
     82
     83
     84
     85
     86
     87
     88
     89
     90
     91
     92
     93
     94
     95
     96
     97
     98
     99
     100
     101
     102
     103
     104
     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 );
     179        $this->assert );
     180        $this->assert );
    143181    }
    144182
     
    197235        );
    198236
     237
     238
    199239        foreach ( $expected_groups as $group ) {
    200             $this->assertContains( $group, array_keys( $wp_object_cache->global_groups ) );
     240            $this->assertContains( $group, );
    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 .
     246     *
     247     * When the fallback is called, it a
     248     *
     249     *
     250     *
     251     *
     252     *
     253     * @
     254     *
     255   
     256   
     257        global $wp_object_cache;
     258
     259       
    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 groups
     268         * .
    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 .
    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 );
    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 );
    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 );
    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 );
    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 .
     589     *
     590     * The fallback a
     591     * .
     592     *
     593     * @ticket 23290
     594     * @covers ::wp_cache_switch_to_blog_fallback
     595     */
     596    public function test_() {
    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 );
    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 );
    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 );
    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 );
    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 );
    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 );
    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 );
    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 );
    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 .
     761     *
     762     * .
     763     * Th.
     764     *
     765     * @ticket 23290
     766     * @covers ::wp_cache_switch_to_blog_fallback
     767     */
     768    public function test_() {
    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 .
     797     *
     798     * .
     799     * Th.
     800     *
     801     * @ticket 23290
     802     * @covers ::wp_cache_switch_to_blog_fallback
     803     */
     804    public function test_() {
    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.