fix(2meet-data-optimizer-hivepress-addon): keep HivePress transients in the object cache when there is one

- With a persistent object cache (wp_using_ext_object_cache(), Redis on the HivePress sites) the transient filter stores HivePress's per-post transient pairs in the tmdo_hp_transient cache group instead of wp_options, where they piled up (1.15 M rows on dev36). Reads fall back to wp_options for values written before; deletes clear both. Without an object cache nothing changes.
- Verify: HivepressTransientFilterTest object-cache cases (RED before); dev36 with Redis: update/get/delete_post_meta on an HP transient key leaves 0 rows in wp_options and postmeta.
- PR: PR-081
This commit is contained in:
2026-09-27 09:00:03 +08:00
parent fda9e43087
commit b515b8441e
3 changed files with 72 additions and 7 deletions
@@ -77,6 +77,9 @@ final class TMDO_Hivepress_Transient_Filter {
/** Namespace prefix for translated wp_options entries. */ /** Namespace prefix for translated wp_options entries. */
public const TRANSLATED_NAMESPACE = 'wpdo_hp_pm_'; public const TRANSLATED_NAMESPACE = 'wpdo_hp_pm_';
/** Object cache group used instead of wp_options when a persistent cache is present. */
public const CACHE_GROUP = 'tmdo_hp_transient';
/** /**
* Register the four metadata filters on `init`. Idempotent — safe to call * Register the four metadata filters on `init`. Idempotent — safe to call
* multiple times. * multiple times.
@@ -164,7 +167,11 @@ final class TMDO_Hivepress_Transient_Filter {
// HivePress cache layer reads the timeout *first*, then decides whether // HivePress cache layer reads the timeout *first*, then decides whether
// to read the value. We must preserve raw stored values until HivePress // to read the value. We must preserve raw stored values until HivePress
// itself orders deletion. // itself orders deletion.
$value = get_option( $option_name, null ); $value = wp_using_ext_object_cache() ? wp_cache_get( $option_name, self::CACHE_GROUP ) : false;
if ( false === $value ) {
// wp_options: where values went before an object cache was used.
$value = get_option( $option_name, null );
}
if ( null === $value ) { if ( null === $value ) {
// Cache miss in our store → fall through to normal postmeta path // Cache miss in our store → fall through to normal postmeta path
@@ -239,16 +246,23 @@ final class TMDO_Hivepress_Transient_Filter {
$translated = self::translate_key( (int) $object_id, (string) $meta_key ); $translated = self::translate_key( (int) $object_id, (string) $meta_key );
$option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated; $option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated;
if ( wp_using_ext_object_cache() ) {
wp_cache_delete( $option_name, self::CACHE_GROUP );
}
delete_option( $option_name ); delete_option( $option_name );
return true; return true;
} }
/** /**
* Internal: persist value or timeout to wp_options. * Internal: persist value or timeout to the object cache when a persistent
* one is in use (Redis: a TTL cache belongs there, not in wp_options), else
* to wp_options.
* *
* Both halves of the transient pair are written with `autoload=no` (they * In wp_options both halves of the transient pair are written with
* are read on-demand only — never via wp_load_alloptions() — so eagerly * `autoload=no` (they are read on-demand only — never via
* loading them on every page would defeat the point of the move). * wp_load_alloptions() — so eagerly loading them on every page would defeat
* the point of the move). HivePress checks the timeout itself, so the
* cache entries get no TTL of their own.
* *
* @param int $post_id Post ID. * @param int $post_id Post ID.
* @param string $meta_key Original `_transient_*` meta_key. * @param string $meta_key Original `_transient_*` meta_key.
@@ -260,6 +274,10 @@ final class TMDO_Hivepress_Transient_Filter {
$translated = self::translate_key( $post_id, $meta_key ); $translated = self::translate_key( $post_id, $meta_key );
$option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated; $option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated;
if ( wp_using_ext_object_cache() ) {
wp_cache_set( $option_name, $meta_value, self::CACHE_GROUP );
return;
}
update_option( $option_name, $meta_value, false ); update_option( $option_name, $meta_value, false );
} }
@@ -44,8 +44,10 @@ class HivepressTransientFilterTest extends TestCase {
global $wpdb; global $wpdb;
$wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' ); $wpdb->query( 'TRUNCATE TABLE `' . self::POSTMETA . '`' );
// Reset wp_options state for filter tests // Reset wp_options / object cache state for filter tests
$GLOBALS['_wp_options'] = array(); $GLOBALS['_wp_options'] = array();
$GLOBALS['_wp_cache'] = array();
$GLOBALS['_wp_using_ext_object_cache'] = false;
// Default the toggle to enabled // Default the toggle to enabled
update_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED, '1' ); update_option( WPDO_Hivepress_Transient_Filter::OPT_ENABLED, '1' );
@@ -195,6 +197,44 @@ class HivepressTransientFilterTest extends TestCase {
$this->assertSame( array( array( 'serialized', 'data' ) ), $value ); $this->assertSame( array( array( 'serialized', 'data' ) ), $value );
} }
// ── External object cache (2.2 PR-081) ──────────────────────────────────
public function test_with_an_object_cache_writes_skip_wp_options(): void {
$GLOBALS['_wp_using_ext_object_cache'] = true;
$key = '_transient_hp_models/cat/v1';
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
$this->assertTrue( WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' ) );
WPDO_Hivepress_Transient_Filter::on_update( null, 100, '_transient_timeout_hp_models/cat/v1', '99999999', '' );
$this->assertFalse( get_option( '_transient_' . $translated ), 'no wp_options row' );
$this->assertFalse( get_option( '_transient_timeout_' . $translated ), 'no wp_options row' );
$this->assertSame( array( 'val' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) );
$this->assertSame( array( '99999999' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_transient_timeout_hp_models/cat/v1', true ) );
}
public function test_with_an_object_cache_a_value_left_in_wp_options_still_reads(): void {
$key = '_transient_hp_models/cat/v1';
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
update_option( '_transient_' . $translated, 'from-before' );
$GLOBALS['_wp_using_ext_object_cache'] = true;
$this->assertSame( array( 'from-before' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) );
}
public function test_with_an_object_cache_delete_clears_both_stores(): void {
$key = '_transient_hp_models/cat/v1';
$translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key );
update_option( '_transient_' . $translated, 'from-before' );
$GLOBALS['_wp_using_ext_object_cache'] = true;
WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' );
WPDO_Hivepress_Transient_Filter::on_delete( null, 100, $key, '', false );
$this->assertNull( WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) );
$this->assertFalse( get_option( '_transient_' . $translated ) );
}
// ── count/purge legacy ────────────────────────────────────────────────── // ── count/purge legacy ──────────────────────────────────────────────────
public function test_count_legacy_postmeta_rows_returns_zero_when_clean(): void { public function test_count_legacy_postmeta_rows_returns_zero_when_clean(): void {
+7
View File
@@ -23,6 +23,13 @@ if ( ! file_exists( $tmdo_core_bootstrap ) ) {
} }
require_once $tmdo_core_bootstrap; require_once $tmdo_core_bootstrap;
// External object cache switch for the transient filter tests (Redis on real sites).
if ( ! function_exists( 'wp_using_ext_object_cache' ) ) {
function wp_using_ext_object_cache( $using = null ): bool { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.Found
return ! empty( $GLOBALS['_wp_using_ext_object_cache'] );
}
}
define( 'TMDO_HIVEPRESS_PATH', dirname( __DIR__, 2 ) . '/' ); define( 'TMDO_HIVEPRESS_PATH', dirname( __DIR__, 2 ) . '/' );
define( 'TMDO_HIVEPRESS_URL', 'http://localhost/wp-content/plugins/2meet-data-optimizer-hivepress-addon/' ); define( 'TMDO_HIVEPRESS_URL', 'http://localhost/wp-content/plugins/2meet-data-optimizer-hivepress-addon/' );
define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivepress-addon.php' ); define( 'TMDO_HIVEPRESS_FILE', TMDO_HIVEPRESS_PATH . '2meet-data-optimizer-hivepress-addon.php' );