Skip to content

Meta: Allow registering post meta keys that skip query cache invalidation#11290

Open
ellatrix wants to merge 11 commits intoWordPress:trunkfrom
ellatrix:fix/meta-query-cache-invalidation
Open

Meta: Allow registering post meta keys that skip query cache invalidation#11290
ellatrix wants to merge 11 commits intoWordPress:trunkfrom
ellatrix:fix/meta-query-cache-invalidation

Conversation

@ellatrix
Copy link
Member

Trac ticket: https://core.trac.wordpress.org/ticket/64696

Alternative approach to the cache invalidation problem: instead of a new table, allow post meta keys to be registered as non-query-cacheable via register_meta() / register_post_meta().

What it does

Adds an invalidates_query_cache parameter to register_meta() (post meta only). Keys registered with false:

  • Do not bump the last_changed timestamp in the posts cache group when added, updated, or deleted
  • Are refused in WP_Meta_Query clauses (with _doing_it_wrong) when the post type is known, to prevent stale cached results
  • Can still be read/written normally via get_post_meta() / update_post_meta() / etc.

Design

Cache correctness relies on two complementary mechanisms:

  1. Cache invalidation (wp_cache_set_posts_last_changed): the authoritative check. Meta write hooks always provide the object ID, so the exact post type is always known. Non-cacheable meta never incorrectly bumps last_changed.

  2. Meta query blocking (WP_Meta_Query): a conservative safety net. Only refuses non-cacheable keys when the post type is known from the parent WP_Query context. Without context, allows the query through — better to allow a query than incorrectly block a legitimate one.

Supports per-post-type registration: a key can be non-cacheable for one post type but cacheable for another.

Testing

15 unit tests covering registration, cache invalidation (add/update/delete), meta query blocking (with/without context, per-post-type, multi-post-type), and the _doing_it_wrong notice.

phpunit --filter Tests_Meta_InvalidatesQueryCache

Use of AI Tools

Co-authored with Claude Code (Opus 4.6).


This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See GitHub Pull Requests for Code Review in the Core Handbook for more details.

@github-actions
Copy link

github-actions bot commented Mar 18, 2026

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

Core Committers: Use this line as a base for the props when committing in SVN:

Props ellatrix, paulkevan, peterwilsoncc, czarate.

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@github-actions
Copy link

Test using WordPress Playground

The changes in this pull request can previewed and tested using a WordPress Playground instance.

WordPress Playground is an experimental project that creates a full WordPress instance entirely within the browser.

Some things to be aware of

  • All changes will be lost when closing a tab with a Playground instance.
  • All changes will be lost when refreshing the page.
  • A fresh instance is created each time the link below is clicked.
  • Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance,
    it's possible that the most recent build failed, or has not completed. Check the list of workflow runs to be sure.

For more details about these limitations and more, check out the Limitations page in the WordPress Playground documentation.

Test this pull request with WordPress Playground.

@peterwilsoncc
Copy link
Contributor

Dropping this here rather than on the ticket as it's a implimentation consideration rather than a architectural consideration. One of the risks we'd need to consider is demonstrated by these two tests

public function test_wp_query_side_effects_one() {
	register_post_meta(
		'post',
		'uncached_meta_key',
		array( 'invalidates_query_cache' => false )
	);

	add_post_meta( self::$post_id, 'uncached_meta_key', 'uncached_meta_value' );
	$query1 = new WP_Query(
		array(
			'fields'     => 'ids',
			'meta_value' => 'uncached_meta_value',
		)
	);
	$this->assertEmpty( $query1->posts, 'WP_Query should not query non-cacheable meta data.' );
}

public function test_wp_query_side_effects_two() {
	register_post_meta(
		'post',
		'uncached_meta_key',
		array( 'invalidates_query_cache' => false )
	);

	add_post_meta( self::$post_id, 'uncached_meta_key', 'uncached_meta_value' );
	$query1 = new WP_Query(
		array(
			'fields'     => 'ids',
			'meta_value' => 'uncached_meta_value',
		)
	);

	delete_post_meta( self::$post_id, 'uncached_meta_key' );
	$query2 = new WP_Query(
		array(
			'fields'     => 'ids',
			'meta_value' => 'uncached_meta_value',
		)
	);
	$this->assertEmpty( $query2->posts, 'WP_Query should not cache non-cacheable meta queries.' );
}

Results:

$ phpunit --filter test_wp_query_side_effects
Installing...
Running as single site... To run multisite, use -c tests/phpunit/multisite.xml
Not running ajax tests. To execute these, use --group ajax.
Not running ms-files tests. To execute these, use --group ms-files.
Not running external-http tests. To execute these, use --group external-http.
PHPUnit 9.6.34 by Sebastian Bergmann and contributors.

Warning:       Your XML configuration validates against a deprecated schema.
Suggestion:    Migrate your XML configuration using "--migrate-configuration"!

FF                                                                  2 / 2 (100%)

Time: 00:00.206, Memory: 235.92 MB

There were 2 failures:

1) Tests_Meta_InvalidatesQueryCache::test_wp_query_side_effects_one
WP_Query should not query non-cacheable meta data.
Failed asserting that an array is empty.

/vagrant/wordpress-develop/tests/phpunit/tests/meta/invalidatesQueryCache.php:391

2) Tests_Meta_InvalidatesQueryCache::test_wp_query_side_effects_two
WP_Query should not cache non-cacheable meta queries.
Failed asserting that an array is empty.

/vagrant/wordpress-develop/tests/phpunit/tests/meta/invalidatesQueryCache.php:417

ellatrix and others added 9 commits March 19, 2026 09:22
Add `invalidates_query_cache` parameter to `register_meta()`. Meta keys
registered with this set to `false` will not bump the `last_changed`
timestamp for the parent object's cache group when added, updated, or
deleted. This prevents high-frequency meta writes from invalidating
query caches site-wide.

Meta keys registered as non-query-cacheable are also blocked from use
in `WP_Meta_Query` clauses, since skipping cache invalidation would
lead to stale query results.

Registers the real-time collaboration meta keys (`wp_sync_awareness`
and `wp_sync_update`) with this flag to resolve the cache invalidation
issue described in https://core.trac.wordpress.org/ticket/64696.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use empty string subtype so the keys are found during cache
invalidation lookups, which do not have access to the post type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Search across all registered subtypes when the caller does not provide
one, so that keys registered for a specific post type are still found
by wp_cache_set_posts_last_changed which only has the meta key.

Adds tests for subtype registration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use get_post_type() in wp_cache_set_posts_last_changed to check the
exact subtype when deciding whether to skip cache invalidation.

Extract post types from the parent WP_Query context in WP_Meta_Query
so that a key registered as non-cacheable for one post type is still
allowed in queries targeting a different post type.

Adds tests for per-subtype behavior: matching post type, different
post type, array of post types, and cache invalidation with mismatched
post type.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rename wp_meta_key_invalidates_query_cache to
wp_post_meta_invalidates_query_cache with a required post_type
parameter. Reject the flag for non-post object types in register_meta.

In WP_Meta_Query, only refuse non-cacheable keys when the post type
is known from the parent WP_Query context. Without context, allow the
query through — it is better to allow a query than to incorrectly
block a legitimate one. Cache correctness is guaranteed by the
invalidation side (wp_cache_set_posts_last_changed), which always
knows the post type from the object ID.

Add comments explaining this reasoning throughout the code and tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When a WP_Query uses meta_value without meta_key, the query matches
across all meta keys. Non-cacheable meta keys must be excluded from
these results, otherwise stale cached results would be served since
cache invalidation is skipped for those keys.

Adds a NOT IN clause to exclude registered non-cacheable keys when
a meta query clause has a value but no key.

Adds tests based on the cases raised in PR review.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add the new invalidates_query_cache default (true) to existing test
assertions that check the full shape of registered meta key args.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ellatrix ellatrix force-pushed the fix/meta-query-cache-invalidation branch from 74c24a3 to 3189f29 Compare March 19, 2026 08:28
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@ellatrix
Copy link
Member Author

ellatrix commented Mar 19, 2026

@peterwilsoncc Right, there's a decision to be made there for those queries that only provide a meta value to query without a key. Either we alter the query (commit), which tbh I find a little too aggressive and risky, or we are ok with this edge case. Either way, it's probably rare to have queries by meta value only?

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
$excluded_keys = $this->get_non_cacheable_meta_keys();
if ( ! empty( $excluded_keys ) ) {
$placeholders = implode( ',', array_fill( 0, count( $excluded_keys ), '%s' ) );
$sql_chunks['where'][] = $wpdb->prepare( "$alias.meta_key NOT IN ($placeholders)", $excluded_keys ); // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ellatrix and I discussed the complexity addition of a NOT IN query here. It seems like it would be not more complex with it since it would be a query on value only.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do lean towards removing the query modification and just leaving it as an edge case with a note. These kinds of meta value only queries should anyway be rare and discouraged (imo).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you checked how plugins are using meta_query to know how much of an edge case this is?

I agree it should be discouraged, but I don't know how prevalent this is.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you search for plugins using meta_value without meta_key?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a specific regex for that, so I would suggest manually checking the code. Otherwise, I think we shoud lean towards being as defensive as possible and not assuming it's an edge case.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How would you check them manually other than just a few?
What does defensive mean in this case? I guess you're arguing for keeping the query adjustment and excluding these keys?

@chriszarate
Copy link

I just tested this and compared against trunk. I can confirm that this PR prevents query cache invalidation for the two RTC post meta.

Before

before.mov

After

after.mov

pkevan added a commit to WordPress/gutenberg that referenced this pull request Mar 19, 2026
Prevents RTC meta keys (wp_sync_awareness, wp_sync_update) from invalidating the posts cache group's last_changed timestamp on every write. This avoids cache invalidation storms during high-frequency collaborative editing.

Uses hook-based interception with a standalone registry since Core's register_meta() cannot be modified. When WordPress 7.0 ships with the native implementation, this backport is automatically disabled via function_exists guard.

New file: lib/compat/wordpress-7.0/post-meta-cache.php
- gutenberg_register_non_cacheable_post_meta() to register keys
- wp_post_meta_invalidates_query_cache() public API
- Replaces Core hooks with conditional version that checks registry

Modified: lib/compat/wordpress-7.0/collaboration.php
- Register RTC meta keys as non-cacheable

Modified: lib/load.php
- Load post-meta-cache.php before collaboration.php

See: https://core.trac.wordpress.org/ticket/64696
See: WordPress/wordpress-develop#11290
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants