hardening 2.3: 2meet-data-optimizer 1.3.0 (flat-table DDL outside web requests, routed count cache, audit fixes) #7
@@ -49,7 +49,7 @@ class TMDO_Cache_Layer {
|
|||||||
// Check which IDs are already cached.
|
// Check which IDs are already cached.
|
||||||
$uncached = array();
|
$uncached = array();
|
||||||
foreach ( $post_ids as $pid ) {
|
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 ) {
|
if ( false === $cached ) {
|
||||||
$uncached[] = (int) $pid;
|
$uncached[] = (int) $pid;
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ class TMDO_Cache_Layer {
|
|||||||
// Cache all results (including empty arrays for posts with no cold data).
|
// Cache all results (including empty arrays for posts with no cold data).
|
||||||
foreach ( $uncached as $pid ) {
|
foreach ( $uncached as $pid ) {
|
||||||
$data = $fetched[ $pid ] ?? array();
|
$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).
|
// Force a fresh read from DB (bypasses cache).
|
||||||
$group = 'wpdo_cold_' . sanitize_key( $post_type );
|
$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.
|
// Re-read will populate cache.
|
||||||
TMDO_Zone_Cold::get_blob( $post_id, $post_type );
|
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.
|
* 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.
|
* @param string $post_type Post type to flush.
|
||||||
*/
|
*/
|
||||||
public static function flush_group( string $post_type ): void {
|
public static function flush_group( string $post_type ): void {
|
||||||
$group = 'wpdo_cold_' . sanitize_key( $post_type );
|
$versions = get_option( TMDO_Zone_Cold::CACHE_VERSION_OPTION, array() );
|
||||||
|
$versions = is_array( $versions ) ? $versions : array();
|
||||||
if ( function_exists( 'wp_cache_flush_group' ) ) {
|
$versions[ $post_type ] = (int) ( $versions[ $post_type ] ?? 0 ) + 1;
|
||||||
wp_cache_flush_group( $group );
|
update_option( TMDO_Zone_Cold::CACHE_VERSION_OPTION, $versions, true );
|
||||||
}
|
|
||||||
// If wp_cache_flush_group is not available (older WP or no object cache),
|
|
||||||
// individual entries will expire via TTL.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -183,7 +183,7 @@ class TMDO_Cache_Layer {
|
|||||||
return array(
|
return array(
|
||||||
'groups' => $groups,
|
'groups' => $groups,
|
||||||
'prefetch_support' => true,
|
'prefetch_support' => true,
|
||||||
'flush_support' => function_exists( 'wp_cache_flush_group' ),
|
'flush_support' => true,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,9 @@ if ( ! defined( 'ABSPATH' ) ) {
|
|||||||
*/
|
*/
|
||||||
class TMDO_Zone_Cold {
|
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.
|
* Get the cold table name for a post type.
|
||||||
*
|
*
|
||||||
@@ -45,6 +48,21 @@ class TMDO_Zone_Cold {
|
|||||||
return 'wpdo_cold_' . sanitize_key( $post_type );
|
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.
|
* 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 {
|
public static function get_blob( int $post_id, string $post_type ): array {
|
||||||
$group = self::cache_group( $post_type );
|
$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 );
|
$cached = wp_cache_get( $cache_key, $group );
|
||||||
if ( false !== $cached ) {
|
if ( false !== $cached ) {
|
||||||
@@ -202,7 +220,7 @@ class TMDO_Zone_Cold {
|
|||||||
);
|
);
|
||||||
// phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
// 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 {
|
public static function delete( int $post_id, string $post_type ): void {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$wpdb->delete( self::table( $post_type ), array( 'post_id' => $post_id ), array( '%d' ) );
|
$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 ───────────────────────────────────────────────────
|
// ── Private helpers ───────────────────────────────────────────────────
|
||||||
@@ -264,7 +282,7 @@ class TMDO_Zone_Cold {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Invalidate object cache.
|
// 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
|
// 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 ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,6 +197,25 @@ class CacheLayerTest extends TestCase {
|
|||||||
$this->assertNotSame( [ 'stale' => true ], $GLOBALS['_wp_cache'][ $group ][ $cache_key ] ?? null );
|
$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() ──────────────────────────────────────────────────────────
|
// ── get_stats() ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
public function test_get_stats_includes_required_keys(): void {
|
public function test_get_stats_includes_required_keys(): void {
|
||||||
|
|||||||
Reference in New Issue
Block a user