From eb1c8b78f4d2e7c7c8d3496c15af0dc8a1659056 Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 17:12:14 +0800 Subject: [PATCH] perf(2meet-data-optimizer): flush the cold cache group without a server-side scan - TMDO_Cache_Layer::flush_group() bumps the post type's cold cache version (option wpdo_cold_cache_version) instead of wp_cache_flush_group(), which Redis Object Cache runs as one server-side scan that blocks Redis on a large group; entries under older keys expire by their TTL - TMDO_Zone_Cold::cache_key() builds every cold blob key; version 0 keeps the current keys, so upgrading invalidates nothing - Verify: CacheLayerTest (new keys, no scan, other post types untouched), unit 702, integration 583, phpcs, stan - PR: PR-110 --- includes/class-tmdo-cache-layer.php | 24 ++++++++++----------- includes/zones/class-tmdo-zone-cold.php | 28 ++++++++++++++++++++----- tests/unit/CacheLayerTest.php | 19 +++++++++++++++++ 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/includes/class-tmdo-cache-layer.php b/includes/class-tmdo-cache-layer.php index 0db82cc..2eabb4d 100644 --- a/includes/class-tmdo-cache-layer.php +++ b/includes/class-tmdo-cache-layer.php @@ -49,7 +49,7 @@ class TMDO_Cache_Layer { // Check which IDs are already cached. $uncached = array(); foreach ( $post_ids as $pid ) { - $cached = wp_cache_get( "cold_{$pid}", $group ); + $cached = wp_cache_get( TMDO_Zone_Cold::cache_key( (int) $pid, $post_type ), $group ); if ( false === $cached ) { $uncached[] = (int) $pid; } @@ -94,7 +94,7 @@ class TMDO_Cache_Layer { // Cache all results (including empty arrays for posts with no cold data). foreach ( $uncached as $pid ) { $data = $fetched[ $pid ] ?? array(); - wp_cache_set( "cold_{$pid}", $data, $group, $ttl ); + wp_cache_set( TMDO_Zone_Cold::cache_key( $pid, $post_type ), $data, $group, $ttl ); } } @@ -114,7 +114,7 @@ class TMDO_Cache_Layer { // Force a fresh read from DB (bypasses cache). $group = 'wpdo_cold_' . sanitize_key( $post_type ); - wp_cache_delete( "cold_{$post_id}", $group ); + wp_cache_delete( TMDO_Zone_Cold::cache_key( $post_id, $post_type ), $group ); // Re-read will populate cache. TMDO_Zone_Cold::get_blob( $post_id, $post_type ); @@ -136,18 +136,18 @@ class TMDO_Cache_Layer { /** * Flush all cold cache for a post type. * - * Useful after bulk imports or schema changes. + * Useful after bulk imports or schema changes. Bumps the post type's cache + * version (new keys from now on) instead of wp_cache_flush_group(), which + * Redis Object Cache runs as one server-side scan that blocks Redis on a + * large group; older entries expire by their TTL. * * @param string $post_type Post type to flush. */ public static function flush_group( string $post_type ): void { - $group = 'wpdo_cold_' . sanitize_key( $post_type ); - - if ( function_exists( 'wp_cache_flush_group' ) ) { - wp_cache_flush_group( $group ); - } - // If wp_cache_flush_group is not available (older WP or no object cache), - // individual entries will expire via TTL. + $versions = get_option( TMDO_Zone_Cold::CACHE_VERSION_OPTION, array() ); + $versions = is_array( $versions ) ? $versions : array(); + $versions[ $post_type ] = (int) ( $versions[ $post_type ] ?? 0 ) + 1; + update_option( TMDO_Zone_Cold::CACHE_VERSION_OPTION, $versions, true ); } /** @@ -183,7 +183,7 @@ class TMDO_Cache_Layer { return array( 'groups' => $groups, 'prefetch_support' => true, - 'flush_support' => function_exists( 'wp_cache_flush_group' ), + 'flush_support' => true, ); } diff --git a/includes/zones/class-tmdo-zone-cold.php b/includes/zones/class-tmdo-zone-cold.php index 0fcdfa0..2cfc1c0 100644 --- a/includes/zones/class-tmdo-zone-cold.php +++ b/includes/zones/class-tmdo-zone-cold.php @@ -25,6 +25,9 @@ if ( ! defined( 'ABSPATH' ) ) { */ class TMDO_Zone_Cold { + /** Option: post type → cold cache version (TMDO_Cache_Layer::flush_group()). */ + public const CACHE_VERSION_OPTION = 'wpdo_cold_cache_version'; + /** * Get the cold table name for a post type. * @@ -45,6 +48,21 @@ class TMDO_Zone_Cold { return 'wpdo_cold_' . sanitize_key( $post_type ); } + /** + * Object-cache key of a post's cold blob. It carries the post type's cache + * version once TMDO_Cache_Layer::flush_group() has bumped it (none before, + * so existing entries keep their keys). + * + * @param int $post_id Post ID. + * @param string $post_type Post type slug. + * @return string + */ + public static function cache_key( int $post_id, string $post_type ): string { + $versions = get_option( self::CACHE_VERSION_OPTION, array() ); + $version = is_array( $versions ) ? (int) ( $versions[ $post_type ] ?? 0 ) : 0; + return $version ? "cold_{$post_id}_v{$version}" : "cold_{$post_id}"; + } + /** * Ensure the cold table exists for a post type. * @@ -92,7 +110,7 @@ class TMDO_Zone_Cold { */ public static function get_blob( int $post_id, string $post_type ): array { $group = self::cache_group( $post_type ); - $cache_key = "cold_{$post_id}"; + $cache_key = self::cache_key( $post_id, $post_type ); $cached = wp_cache_get( $cache_key, $group ); if ( false !== $cached ) { @@ -202,7 +220,7 @@ class TMDO_Zone_Cold { ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) ); + wp_cache_delete( self::cache_key( $post_id, $post_type ), self::cache_group( $post_type ) ); } /** @@ -215,7 +233,7 @@ class TMDO_Zone_Cold { public static function delete( int $post_id, string $post_type ): void { global $wpdb; $wpdb->delete( self::table( $post_type ), array( 'post_id' => $post_id ), array( '%d' ) ); - wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) ); + wp_cache_delete( self::cache_key( $post_id, $post_type ), self::cache_group( $post_type ) ); } // ── Private helpers ─────────────────────────────────────────────────── @@ -264,7 +282,7 @@ class TMDO_Zone_Cold { ); // Invalidate object cache. - wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) ); + wp_cache_delete( self::cache_key( $post_id, $post_type ), self::cache_group( $post_type ) ); } /** @@ -305,6 +323,6 @@ class TMDO_Zone_Cold { ); // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching - wp_cache_delete( "cold_{$post_id}", self::cache_group( $post_type ) ); + wp_cache_delete( self::cache_key( $post_id, $post_type ), self::cache_group( $post_type ) ); } } diff --git a/tests/unit/CacheLayerTest.php b/tests/unit/CacheLayerTest.php index e15a618..d22cbd7 100644 --- a/tests/unit/CacheLayerTest.php +++ b/tests/unit/CacheLayerTest.php @@ -197,6 +197,25 @@ class CacheLayerTest extends TestCase { $this->assertNotSame( [ 'stale' => true ], $GLOBALS['_wp_cache'][ $group ][ $cache_key ] ?? null ); } + /** + * Flushing bumps the post type's cache version instead of asking the + * object cache to scan the group (a server-side script on Redis that + * blocks it on a large group); older entries expire by their TTL. + */ + public function test_flush_group_moves_to_new_keys_without_a_group_flush(): void { + $group = 'wpdo_cold_hp_listing'; + $old_key = WPDO_Zone_Cold::cache_key( 1, 'hp_listing' ); + $GLOBALS['_wp_cache'][ $group ][ $old_key ] = array( 'stale' => true ); + + WPDO_Cache_Layer::flush_group( 'hp_listing' ); + + $new_key = WPDO_Zone_Cold::cache_key( 1, 'hp_listing' ); + $this->assertNotSame( $old_key, $new_key ); + $this->assertFalse( wp_cache_get( $new_key, $group ) ); + $this->assertSame( 'cold_2', WPDO_Zone_Cold::cache_key( 2, 'other_type' ), 'other post types keep their keys' ); + $this->assertArrayHasKey( $old_key, $GLOBALS['_wp_cache'][ $group ], 'nothing scanned or deleted' ); + } + // ── get_stats() ────────────────────────────────────────────────────────── public function test_get_stats_includes_required_keys(): void {