From 788ddccfb32ec71660fcbc10dbb48832ead741cb Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 10:32:12 +0800 Subject: [PATCH] fix(2meet-data-optimizer-hivepress-addon): with an object cache, keep HivePress transients out of wp_options entirely MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - On a cache miss the filter fell back to get_option(): with the values now written to the cache, the option is absent, so WordPress added its name to `notoptions` for good (dev36: +1,000 entries for 1,000 posts, one array loaded on every miss); a hit kept the old row in the options cache group with no expiry, which slowed the selective Redis flush (8.9 s, BUSY). The cache is now the only store for reads and deletes; old rows wait for cleanup-hp-transients --options. Without an object cache nothing changes. - Verify: HivepressTransientFilterTest (2 RED before); dev36 notoptions 210 → 210 over 1,000 posts (was 128 → 1,128) - PR: PR-089c --- CHANGELOG.md | 10 ++++++---- includes/class-tmdo-hivepress-transient-filter.php | 12 +++++++++--- tests/integration/HivepressTransientFilterTest.php | 10 ++++++---- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index acf5429..f57af6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,10 +20,12 @@ Released with 2meet-data-optimizer 1.2.0 (hardening 2.2). - With a persistent object cache (`wp_using_ext_object_cache()`), the transient filter keeps HivePress's per-post transient pairs in the `tmdo_hp_transient` cache group instead of `wp_options`, where they piled - up (1.15 M pairs on dev36). Reads fall back to `wp_options` for values - written before; deletes clear both. Without an object cache nothing changes. - After upgrading, remove the rows left behind with core's - `wp tmdo cleanup-hp-transients --options --confirm`. + up (1.15 M pairs on dev36). The cache is then the only store: rows written + before are neither read nor deleted (HivePress rebuilds those caches once), + because a `get_option()` miss adds the name to WordPress's `notoptions` for + good and a hit keeps the old row in the `options` cache group with no expiry. + Without an object cache nothing changes. After upgrading, remove the rows + left behind with core's `wp tmdo cleanup-hp-transients --options --confirm`. ### Fixed diff --git a/includes/class-tmdo-hivepress-transient-filter.php b/includes/class-tmdo-hivepress-transient-filter.php index 1deadff..8adb2d5 100644 --- a/includes/class-tmdo-hivepress-transient-filter.php +++ b/includes/class-tmdo-hivepress-transient-filter.php @@ -167,9 +167,14 @@ final class TMDO_Hivepress_Transient_Filter { // HivePress cache layer reads the timeout *first*, then decides whether // to read the value. We must preserve raw stored values until HivePress // itself orders deletion. - $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. + if ( wp_using_ext_object_cache() ) { + // The object cache is the only store. Rows left in wp_options from + // before are not read (`wp tmdo cleanup-hp-transients --options` + // removes them): a get_option() miss would add the name to + // `notoptions` for good, as later writes never reach wp_options. + $value = wp_cache_get( $option_name, self::CACHE_GROUP ); + $value = false === $value ? null : $value; + } else { $value = get_option( $option_name, null ); } @@ -248,6 +253,7 @@ final class TMDO_Hivepress_Transient_Filter { if ( wp_using_ext_object_cache() ) { wp_cache_delete( $option_name, self::CACHE_GROUP ); + return true; } delete_option( $option_name ); return true; diff --git a/tests/integration/HivepressTransientFilterTest.php b/tests/integration/HivepressTransientFilterTest.php index 41e2f9a..3547f20 100644 --- a/tests/integration/HivepressTransientFilterTest.php +++ b/tests/integration/HivepressTransientFilterTest.php @@ -213,16 +213,18 @@ class HivepressTransientFilterTest extends TestCase { $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 { + public function test_with_an_object_cache_a_value_left_in_wp_options_is_not_read(): void { + // A miss must not fall back to get_option(): the option is absent after + // cleanup and WordPress would add its name to `notoptions` for good. $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 ) ); + $this->assertNull( WPDO_Hivepress_Transient_Filter::on_read( null, 100, $key, true ) ); } - public function test_with_an_object_cache_delete_clears_both_stores(): void { + public function test_with_an_object_cache_delete_leaves_wp_options_alone(): void { $key = '_transient_hp_models/cat/v1'; $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); update_option( '_transient_' . $translated, 'from-before' ); @@ -232,7 +234,7 @@ class HivepressTransientFilterTest extends TestCase { 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 ) ); + $this->assertSame( 'from-before', get_option( '_transient_' . $translated ), 'left for cleanup-hp-transients --options' ); } // ── count/purge legacy ──────────────────────────────────────────────────