From d129139c7d5ab071e6d066652dfccec89d713add Mon Sep 17 00:00:00 2001 From: wpdev Date: Sun, 27 Sep 2026 16:42:52 +0800 Subject: [PATCH] perf(2meet-data-optimizer-hivepress-addon): expire object-cache transients MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - with an external object cache, a HivePress timeout entry expires when it records; the value (written before its timeout) after at most a week — a miss only makes HivePress rebuild it. Entries of deleted posts no longer accumulate without end - Verify: HivepressTransientFilterTest TTL test, integration 53, unit 145; dev36 Redis TTL on tmdo_hp_transient keys - PR: PR-099 --- .../class-tmdo-hivepress-transient-filter.php | 12 ++++++++--- .../HivepressTransientFilterTest.php | 20 +++++++++++++++++++ tests/integration/bootstrap.php | 8 ++++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/includes/class-tmdo-hivepress-transient-filter.php b/includes/class-tmdo-hivepress-transient-filter.php index 8adb2d5..bb17a46 100644 --- a/includes/class-tmdo-hivepress-transient-filter.php +++ b/includes/class-tmdo-hivepress-transient-filter.php @@ -80,6 +80,9 @@ final class TMDO_Hivepress_Transient_Filter { /** Object cache group used instead of wp_options when a persistent cache is present. */ public const CACHE_GROUP = 'tmdo_hp_transient'; + /** Longest an object-cache entry lives: a week (HivePress's default is a day). */ + public const MAX_CACHE_TTL = 604800; + /** * Register the four metadata filters on `init`. Idempotent — safe to call * multiple times. @@ -267,8 +270,10 @@ final class TMDO_Hivepress_Transient_Filter { * In wp_options both halves of the transient pair are written with * `autoload=no` (they are read on-demand only — never via * 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. + * the point of the move). In the object cache a timeout entry expires when + * it says; the value, written before its timeout, after MAX_CACHE_TTL — a + * miss only makes HivePress rebuild it — so entries of deleted posts do + * not pile up (2.3 PR-099). * * @param int $post_id Post ID. * @param string $meta_key Original `_transient_*` meta_key. @@ -281,7 +286,8 @@ final class TMDO_Hivepress_Transient_Filter { $option_name = ( $is_timeout ? '_transient_timeout_' : '_transient_' ) . $translated; if ( wp_using_ext_object_cache() ) { - wp_cache_set( $option_name, $meta_value, self::CACHE_GROUP ); + $ttl = $is_timeout ? min( self::MAX_CACHE_TTL, max( 1, (int) $meta_value - time() ) ) : self::MAX_CACHE_TTL; + wp_cache_set( $option_name, $meta_value, self::CACHE_GROUP, $ttl ); return; } update_option( $option_name, $meta_value, false ); diff --git a/tests/integration/HivepressTransientFilterTest.php b/tests/integration/HivepressTransientFilterTest.php index 3547f20..3b70036 100644 --- a/tests/integration/HivepressTransientFilterTest.php +++ b/tests/integration/HivepressTransientFilterTest.php @@ -213,6 +213,26 @@ class HivepressTransientFilterTest extends TestCase { $this->assertSame( array( '99999999' ), WPDO_Hivepress_Transient_Filter::on_read( null, 100, '_transient_timeout_hp_models/cat/v1', true ) ); } + /** + * Entries expire in the object cache too (2.3 PR-099): the timeout at the + * time it records, the value (written first, before its timeout is known) + * after at most a week. Orphans of deleted posts no longer pile up. + */ + public function test_with_an_object_cache_entries_get_a_ttl(): void { + $GLOBALS['_wp_using_ext_object_cache'] = true; + $key = '_transient_hp_models/cat/v1'; + $translated = WPDO_Hivepress_Transient_Filter::translate_key( 100, $key ); + + WPDO_Hivepress_Transient_Filter::on_update( null, 100, $key, 'val', '' ); + WPDO_Hivepress_Transient_Filter::on_update( null, 100, '_transient_timeout_hp_models/cat/v1', (string) ( time() + 3600 ), '' ); + WPDO_Hivepress_Transient_Filter::on_update( null, 101, '_transient_timeout_hp_models/cat/v1', (string) ( time() - 5 ), '' ); + + $ttl = $GLOBALS['_wp_cache_ttl'][ WPDO_Hivepress_Transient_Filter::CACHE_GROUP ]; + $this->assertSame( WPDO_Hivepress_Transient_Filter::MAX_CACHE_TTL, $ttl[ '_transient_' . $translated ] ); + $this->assertEqualsWithDelta( 3600, $ttl[ '_transient_timeout_' . $translated ], 2 ); + $this->assertSame( 1, $ttl[ '_transient_timeout_' . WPDO_Hivepress_Transient_Filter::translate_key( 101, $key ) ], 'already expired' ); + } + 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. diff --git a/tests/integration/bootstrap.php b/tests/integration/bootstrap.php index c5be687..6e637f1 100644 --- a/tests/integration/bootstrap.php +++ b/tests/integration/bootstrap.php @@ -21,6 +21,14 @@ if ( ! file_exists( $tmdo_core_bootstrap ) ) { . dirname( __DIR__, 3 ) . '/2meet-data-optimizer' ); } +// Defined before the core bootstrap (which only adds it when missing) so tests can read the TTL. +if ( ! function_exists( 'wp_cache_set' ) ) { + function wp_cache_set( $key, $value, $group = '', $ttl = 0 ): bool { + $GLOBALS['_wp_cache'][ $group ][ $key ] = $value; + $GLOBALS['_wp_cache_ttl'][ $group ][ $key ] = (int) $ttl; + return true; + } +} require_once $tmdo_core_bootstrap; // External object cache switch for the transient filter tests (Redis on real sites).