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:
@@ -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 {
|
||||||
|
|||||||
@@ -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' );
|
||||||
|
|||||||
Reference in New Issue
Block a user