perf(2meet-data-optimizer): keep cached routed counts across unrelated post meta writes

- cached counts followed posts' last_changed, which WordPress moves on any
  post meta write; themes write meta on page views (on dev36 Blocksy saved a
  dynamic-CSS descriptor for each listing a crawler visited), so the cache
  was invalidated every few seconds and keyword/filter searches recounted
- counts now follow their own last_changed group (tmdo_posts), bumped on
  clean_post_cache (post saved or deleted; hooked in every mode) and on flat
  writes through the Hook Bus, plus terms' last_changed; a routed search reads
  no unmanaged meta
- Verify: PostQueryRouterFoundRowsTest (unrelated meta write keeps the count,
  save or flat write recounts), integration 588, unit 703, phpcs, stan;
  dev36 front-end
- PR: PR-117 (2.3 acceptance)
This commit is contained in:
2026-09-27 17:46:54 +08:00
parent 7851f150fa
commit ca2cbdab65
5 changed files with 32 additions and 5 deletions
+3
View File
@@ -261,6 +261,9 @@ class TMDO_Core {
$post_router = new TMDO_Post_Query_Router();
$post_router->register_hooks();
}
if ( class_exists( 'TMDO_Post_Query_Router' ) ) {
add_action( 'clean_post_cache', array( 'TMDO_Post_Query_Router', 'bump_count_version' ) );
}
// Zone B: Warm cleanup cron handler.
add_action( 'wpdo_warm_cleanup', array( $this, 'cleanup_warm_zone' ) );
+1
View File
@@ -912,6 +912,7 @@ final class TMDO_Hook_Bus {
private static function touch_posts_last_changed( string $type ): void {
if ( 'post' === $type ) {
wp_cache_set_posts_last_changed();
TMDO_Post_Query_Router::bump_count_version();
}
}
@@ -31,8 +31,11 @@
*
* Found rows (O-4): routed searches drop SQL_CALC_FOUND_ROWS (it counts every
* match inside the page query); found_posts_query answers from a COUNT of the
* same query, kept in the object cache until posts' last_changed moves (core
* post saves, and flat writes via TMDO_Hook_Bus).
* same query, kept in the object cache until a post is saved or deleted, a
* flat write goes through TMDO_Hook_Bus (bump_count_version()) or terms'
* last_changed moves. Not posts' last_changed: WordPress moves it on any post
* meta write, which themes do on page views (Blocksy's dynamic CSS), and a
* routed search reads no unmanaged meta.
*
* 🔒 v2.9.x → v2.10.x frozen contract: never touches user entity. Routes
* only entity_type='post'.
@@ -69,6 +72,9 @@ class TMDO_Post_Query_Router {
/** Object cache group for found-rows counts. */
private const COUNT_CACHE_GROUP = 'tmdo_found_rows';
/** The last_changed group the cached counts follow (bump_count_version()). */
public const COUNT_VERSION_GROUP = 'tmdo_posts';
/** Seconds a "every post of the type has a flat row" answer is reused. */
private const COVERAGE_TTL = 900;
@@ -436,7 +442,7 @@ class TMDO_Post_Query_Router {
// Keyword searches carry wpdb's placeholder escape for "%", which is new
// in every request: without it the key would never match again.
$stable = method_exists( $wpdb, 'remove_placeholder_escape' ) ? $wpdb->remove_placeholder_escape( $count_sql ) : $count_sql;
$key = md5( $stable ) . ':' . wp_cache_get_last_changed( 'posts' );
$key = md5( $stable ) . ':' . wp_cache_get_last_changed( self::COUNT_VERSION_GROUP ) . ':' . wp_cache_get_last_changed( 'terms' );
$total = wp_cache_get( $key, self::COUNT_CACHE_GROUP );
if ( false === $total ) {
$total = (int) $wpdb->get_var( $count_sql );
@@ -452,6 +458,17 @@ class TMDO_Post_Query_Router {
// Helpers
// ─────────────────────────────────────────────────────────────────────────
/**
* A post was saved or deleted, or a managed key written: cached routed
* counts are stale. Hooked on clean_post_cache whatever the mode, so a
* count cached before the router was last active is not reused.
*
* @return void
*/
public static function bump_count_version(): void {
wp_cache_set_last_changed( self::COUNT_VERSION_GROUP );
}
/**
* Whether the router is mode-active (post mode is shadow_read or aeav_only).
*
@@ -128,7 +128,10 @@ class PostQueryRouterFoundRowsTest extends TestCase {
$wpdb->query( 'UPDATE `' . self::TABLE . '` SET fr_price = 5 WHERE post_id = 2' );
$this->assertSame( '2', $wpdb->get_var( $router->found_posts_query( 'SELECT FOUND_ROWS()', $query ) ), 'cached' );
wp_cache_set_posts_last_changed();
wp_cache_set_posts_last_changed(); // Any post meta write, e.g. a theme's CSS descriptor on a page view.
$this->assertSame( '2', $wpdb->get_var( $router->found_posts_query( 'SELECT FOUND_ROWS()', $query ) ), 'still cached' );
TMDO_Post_Query_Router::bump_count_version(); // A post saved, or a managed key written.
$this->assertSame( '1', $wpdb->get_var( $router->found_posts_query( 'SELECT FOUND_ROWS()', $query ) ), 'recounted' );
}
@@ -153,7 +156,7 @@ class PostQueryRouterFoundRowsTest extends TestCase {
$this->assertSame( '2', $count( '1' ), 'one cached count serves every exclusion list; 1 does not match' );
$this->assertSame( '1', $count( '2' ) );
wp_cache_set_posts_last_changed();
TMDO_Post_Query_Router::bump_count_version();
$this->assertSame( '0', $count( '2' ), 'recounted' );
}
@@ -203,6 +206,7 @@ class PostQueryRouterFoundRowsTest extends TestCase {
public function test_flat_writes_and_deletes_move_posts_last_changed(): void {
$GLOBALS['_wp_current_filter'] = 'update_post_metadata';
$before = wp_cache_get_last_changed( 'posts' );
$version = wp_cache_get_last_changed( TMDO_Post_Query_Router::COUNT_VERSION_GROUP );
WPDO_Hook_Bus::intercept_update( null, 1, 'fr_price', '11', '' );
$after_write = wp_cache_get_last_changed( 'posts' );
@@ -211,6 +215,7 @@ class PostQueryRouterFoundRowsTest extends TestCase {
$this->assertNotSame( $before, $after_write );
$this->assertNotSame( $after_write, wp_cache_get_last_changed( 'posts' ) );
$this->assertNotSame( $version, wp_cache_get_last_changed( TMDO_Post_Query_Router::COUNT_VERSION_GROUP ), 'routed counts follow flat writes' );
}
// ── flat-first: a filtered search ordered by a flat column ─────────────
+1
View File
@@ -434,6 +434,7 @@ if ( ! function_exists( 'wp_mkdir_p' ) ) {
$GLOBALS['_wp_cache'] = [];
if ( ! function_exists( 'wp_cache_get_last_changed' ) ) { function wp_cache_get_last_changed( $group ) { return $GLOBALS['_wp_last_changed'][ $group ] ??= '0'; } }
if ( ! function_exists( 'wp_cache_set_last_changed' ) ) { function wp_cache_set_last_changed( $group ) { $GLOBALS['_wp_last_changed'][ $group ] = (string) hrtime( true ); return $GLOBALS['_wp_last_changed'][ $group ]; } }
if ( ! function_exists( 'wp_cache_set_posts_last_changed' ) ) { function wp_cache_set_posts_last_changed() { $GLOBALS['_wp_last_changed']['posts'] = (string) hrtime( true ); } }
if ( ! function_exists( 'wp_cache_get' ) ) { function wp_cache_get( $key, $group = '' ) { return $GLOBALS['_wp_cache'][ $group ][ $key ] ?? false; } }
if ( ! function_exists( 'wp_cache_set' ) ) { function wp_cache_set( $key, $value, $group = '', $ttl = 0 ): bool { $GLOBALS['_wp_cache'][ $group ][ $key ] = $value; return true; } }