Skip to content

Introduce taxonomy-specific cache invalidation methods #8634

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: trunk
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion src/wp-includes/class-wp-query.php
Original file line number Diff line number Diff line change
@@ -5064,7 +5064,13 @@ static function ( &$value ) use ( $wpdb, $placeholder ) {

$last_changed = wp_cache_get_last_changed( 'posts' );
if ( ! empty( $this->tax_query->queries ) ) {
$last_changed .= wp_cache_get_last_changed( 'terms' );
$taxonomies = array();
foreach ( $this->tax_query->queries as $tax_query ) {
if ( isset( $tax_query['taxonomy'] ) ) {
$taxonomies[] = $tax_query['taxonomy'];
}
}
$last_changed .= wp_cache_get_taxonomies_last_changed( $taxonomies );
}

$this->query_cache_key = "wp_query:$key:$last_changed";
18 changes: 17 additions & 1 deletion src/wp-includes/class-wp-term-query.php
Original file line number Diff line number Diff line change
@@ -1172,7 +1172,23 @@ protected function generate_cache_key( array $args, $sql ) {
$sql = $wpdb->remove_placeholder_escape( $sql );

$key = md5( serialize( $cache_args ) . $sql );
$last_changed = wp_cache_get_last_changed( 'terms' );
$last_changed = wp_cache_get_taxonomies_last_changed( (array) $args['taxonomy'] );
$meta_keys = array(
'meta_key',
'meta_value',
'meta_compare',
'meta_compare_key',
'meta_type',
'meta_type_key',
'meta_query',
);
foreach ( $meta_keys as $meta_key ) {
if ( isset( $cache_args[ $meta_key ] ) ) {
$last_changed .= ':' . wp_cache_get_last_changed( 'term_meta' );
break;
}
}

return "get_terms:$key:$last_changed";
}
}
6 changes: 3 additions & 3 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
@@ -135,9 +135,9 @@
add_action( 'remove_user_role', 'wp_cache_set_users_last_changed' );

// Term meta.
add_action( 'added_term_meta', 'wp_cache_set_terms_last_changed' );
add_action( 'updated_term_meta', 'wp_cache_set_terms_last_changed' );
add_action( 'deleted_term_meta', 'wp_cache_set_terms_last_changed' );
add_action( 'added_term_meta', 'wp_cache_clear_term_meta' );
add_action( 'updated_term_meta', 'wp_cache_clear_term_meta' );
add_action( 'deleted_term_meta', 'wp_cache_clear_term_meta' );
add_filter( 'get_term_metadata', 'wp_check_term_meta_support_prefilter' );
add_filter( 'add_term_metadata', 'wp_check_term_meta_support_prefilter' );
add_filter( 'update_term_metadata', 'wp_check_term_meta_support_prefilter' );
2 changes: 1 addition & 1 deletion src/wp-includes/link-template.php
Original file line number Diff line number Diff line change
@@ -2007,7 +2007,7 @@ function get_adjacent_post( $in_same_term = false, $excluded_terms = '', $previo
$key = md5( $query );
$last_changed = wp_cache_get_last_changed( 'posts' );
if ( $in_same_term || ! empty( $excluded_terms ) ) {
$last_changed .= wp_cache_get_last_changed( 'terms' );
$last_changed .= wp_cache_get_taxonomy_last_changed( $taxonomy );
}
$cache_key = "adjacent_post:$key:$last_changed";

95 changes: 92 additions & 3 deletions src/wp-includes/taxonomy.php
Original file line number Diff line number Diff line change
@@ -890,14 +890,15 @@ function get_objects_in_term( $term_ids, $taxonomies, $args = array() ) {

$term_ids = array_map( 'intval', $term_ids );

$last_changed = wp_cache_get_taxonomies_last_changed( $taxonomies );

$taxonomies = "'" . implode( "', '", array_map( 'esc_sql', $taxonomies ) ) . "'";
$term_ids = "'" . implode( "', '", $term_ids ) . "'";

$sql = "SELECT tr.object_id FROM $wpdb->term_relationships AS tr INNER JOIN $wpdb->term_taxonomy AS tt ON tr.term_taxonomy_id = tt.term_taxonomy_id WHERE tt.taxonomy IN ($taxonomies) AND tt.term_id IN ($term_ids) ORDER BY tr.object_id $order";

$last_changed = wp_cache_get_last_changed( 'terms' );
$cache_key = 'get_objects_in_term:' . md5( $sql ) . ":$last_changed";
$cache = wp_cache_get( $cache_key, 'term-queries' );
$cache_key = 'get_objects_in_term:' . md5( $sql ) . ":$last_changed";
$cache = wp_cache_get( $cache_key, 'term-queries' );
if ( false === $cache ) {
$object_ids = $wpdb->get_col( $sql );
wp_cache_set( $cache_key, $object_ids, 'term-queries' );
@@ -2950,6 +2951,7 @@ function wp_set_object_terms( $object_id, $terms, $taxonomy, $append = false ) {

wp_cache_delete( $object_id, $taxonomy . '_relationships' );
wp_cache_set_terms_last_changed();
wp_cache_set_taxonomy_last_changed( $taxonomy );

/**
* Fires after an object's terms have been set.
@@ -3048,6 +3050,7 @@ function wp_remove_object_terms( $object_id, $terms, $taxonomy ) {

wp_cache_delete( $object_id, $taxonomy . '_relationships' );
wp_cache_set_terms_last_changed();
wp_cache_set_taxonomy_last_changed( $taxonomy );

/**
* Fires immediately after an object-term relationship is deleted.
@@ -3631,10 +3634,14 @@ function clean_object_term_cache( $object_ids, $object_type ) {

$taxonomies = get_object_taxonomies( $object_type );

$cache_keys = array();
foreach ( $taxonomies as $taxonomy ) {
wp_cache_delete_multiple( $object_ids, "{$taxonomy}_relationships" );
$cache_keys[] = $taxonomy . ':last_changed';
}

wp_cache_delete_multiple( $cache_keys, 'terms' );

wp_cache_set_terms_last_changed();

/**
@@ -3697,6 +3704,8 @@ function clean_term_cache( $ids, $taxonomy = '', $clean_taxonomy = true ) {
clean_taxonomy_cache( $taxonomy );
}

wp_cache_set_taxonomy_last_changed( $taxonomy );

/**
* Fires once after each taxonomy's term cache has been cleaned.
*
@@ -3723,6 +3732,7 @@ function clean_term_cache( $ids, $taxonomy = '', $clean_taxonomy = true ) {
function clean_taxonomy_cache( $taxonomy ) {
wp_cache_delete( 'all_ids', $taxonomy );
wp_cache_delete( 'get', $taxonomy );
wp_cache_set_taxonomy_last_changed( $taxonomy );
wp_cache_set_terms_last_changed();

// Regenerate cached hierarchy.
@@ -5105,6 +5115,85 @@ function is_term_publicly_viewable( $term ) {
return is_taxonomy_viewable( $term->taxonomy );
}


/**
* Clears the term meta cache and updates the last changed timestamp for the term and its taxonomy.
*
* This function ensures that the cache for the given term and its associated taxonomy is invalidated,
* and marks them as "last changed" to reflect the updates or changes made to the meta data.
*
* @since x.x.x
*/
function wp_cache_clear_term_meta() {
wp_cache_set_terms_last_changed();
wp_cache_set_last_changed( 'term_meta' );
}

/**
* Sets the last changed time for a specific taxonomy in the cache.
*
* This function updates the cached value to track when the taxonomy data was last changed.
*
* @since x.x.x
*
* @param string $taxonomy The taxonomy slug to update the last changed time for.
* @return string The microtime when the taxonomy was last changed.
*/
function wp_cache_set_taxonomy_last_changed( $taxonomy ) {
$time = microtime();

wp_cache_set( $taxonomy . ':last_changed', $time, 'terms' );

return $time;
}

/**
* Retrieves the last changed time for a taxonomy.
*
* @since x.x.x
*
* @param string $taxonomy Taxonomy name.
* @return float UNIX timestamp with microseconds representing when the taxonomy was last changed.
*/
function wp_cache_get_taxonomy_last_changed( $taxonomy ) {
if ( ! $taxonomy ) {
return wp_cache_get_last_changed( 'terms' );
}
$last_changed = wp_cache_get( $taxonomy . ':last_changed', 'terms' );

if ( $last_changed ) {
return $last_changed;
}

return wp_cache_set_taxonomy_last_changed( $taxonomy );
}

/**
* Retrieves the last changed time for the 'taxonomies' cache group.
*
* @since x.x.x
*
* @return string UNIX timestamp with microseconds representing when the group was last changed.
*/
function wp_cache_get_taxonomies_last_changed( array $taxonomies ) {
$taxonomies = array_unique( $taxonomies );
if ( empty( $taxonomies ) ) {
return wp_cache_get_last_changed( 'terms' );
}
sort( $taxonomies );
$cache_keys = array_map(
static function ( $taxonomy ) {
return $taxonomy . ':last_changed';
},
array_filter( $taxonomies )
);
wp_cache_get_multiple( $cache_keys, 'terms' );
$last_changes = array_map( 'wp_cache_get_taxonomy_last_changed', $taxonomies );
$last_changes = array_map( 'floatval', $last_changes );

return (string) array_sum( $last_changes );
}

/**
* Sets the last changed time for the 'terms' cache group.
*
2 changes: 1 addition & 1 deletion tests/phpunit/tests/link/getAdjacentPost.php
Original file line number Diff line number Diff line change
@@ -426,6 +426,6 @@ public function test_get_adjacent_post_cache() {

$num_queries = get_num_queries();
$this->assertEquals( $post_four, get_adjacent_post( true, '', false ), 'Result of function call is wrong after after adding new term' );
$this->assertSame( get_num_queries() - $num_queries, 2, 'Number of queries run was not two after adding new term' );
$this->assertSame( get_num_queries() - $num_queries, 0, 'Number of queries run was not two after adding new term' );
}
}
84 changes: 83 additions & 1 deletion tests/phpunit/tests/query/cacheResults.php
Original file line number Diff line number Diff line change
@@ -26,6 +26,14 @@ class Test_Query_CacheResults extends WP_UnitTestCase {
*/
public static $t1;


/**
* Term ID.
*
* @var int
*/
public static $t2;

/**
* Author's user ID.
*
@@ -62,7 +70,16 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) {
)
);

self::$t2 = $factory->term->create(
array(
'taxonomy' => 'post_tag',
'slug' => 'bar',
'name' => 'Bar',
)
);

wp_set_post_terms( self::$posts[0], self::$t1, 'category' );
wp_set_post_terms( self::$posts[0], self::$t2, 'post_tag' );
add_post_meta( self::$posts[0], 'color', '#000000' );

// Make a user.
@@ -1986,6 +2003,70 @@ public function data_author_cache_warmed_by_the_loop() {
);
}

public function test_query_cache_empty_taxonomies() {
add_filter( 'split_the_query', '__return_false' );
$query1 = new WP_Query();
$query_args_1 = array(
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'no_found_rows' => true,
'tax_query' => array(
array(
'terms' => array( self::$t1 ),
),
),
);
$query1->query( $query_args_1 );

clean_term_cache( self::$t1, 'category' );
$num_queries = get_num_queries();

$query1->query( $query_args_1 );

$this->assertSame( $num_queries + 1, get_num_queries(), 'Query not should be cached.' );
}
public function test_query_cache_different_taxonomies() {
add_filter( 'split_the_query', '__return_false' );
$query1 = new WP_Query();
$query_args_1 = array(
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'no_found_rows' => true,
'tax_query' => array(
array(
'taxonomy' => 'category',
'terms' => array( self::$t1 ),
),
),
);
$query1->query( $query_args_1 );
$query_args_2 = array(
'update_post_term_cache' => false,
'update_post_meta_cache' => false,
'no_found_rows' => true,
'tax_query' => array(
array(
'taxonomy' => 'post_tag',
'terms' => array( self::$t2 ),
),
),
);
$query2 = new WP_Query();
$query2->query( $query_args_2 );

clean_term_cache( self::$t1, 'category' );
$num_queries = get_num_queries();

$query1->query( $query_args_1 );

$this->assertSame( $num_queries + 2, get_num_queries(), 'Query not should be cached.' );
$num_queries = get_num_queries();
$query2->query( $query_args_2 );
remove_filter( 'split_the_query', '__return_false' );

$this->assertSame( $num_queries, get_num_queries(), 'Query should be cached.' );
}

/**
* Ensure lazy loading term meta queries all term meta in a single query.
*
@@ -2025,8 +2106,9 @@ public function test_get_post_meta_lazy_loads_all_term_meta_data() {
* 2: Post data
* 3: Post meta data.
* 4: Post term data.
* 5. Term data
*/
$this->assertSame( 4, $num_queries, 'Unexpected number of queries while querying posts.' );
$this->assertSame( 5, $num_queries, 'Unexpected number of queries while querying posts.' );
$this->assertNotEmpty( $query_posts, 'Query posts is empty.' );

$num_queries_start = get_num_queries();
31 changes: 31 additions & 0 deletions tests/phpunit/tests/taxonomy.php
Original file line number Diff line number Diff line change
@@ -452,6 +452,37 @@ public function test_post_deletion_should_invalidate_get_objects_in_term_cache()
$this->assertSame( array(), $after );
}

public function test_invalidate_different_taxonomy_get_objects_in_term_cache() {
register_taxonomy( 'wptests_tax_1', 'post' );
register_taxonomy( 'wptests_tax_2', 'post' );

$posts = self::factory()->post->create_many( 2 );
$term_id_1 = self::factory()->term->create(
array(
'taxonomy' => 'wptests_tax_1',
)
);
$term_id_2 = self::factory()->term->create(
array(
'taxonomy' => 'wptests_tax_2',
)
);

wp_set_object_terms( $posts[1], $term_id_1, 'wptests_tax_1' );
wp_set_object_terms( $posts[1], $term_id_2, 'wptests_tax_2' );

// Prime cache.
$before = get_objects_in_term( $term_id_1, 'wptests_tax_1' );
$this->assertEqualSets( array( $posts[1] ), $before );

clean_term_cache( $term_id_2, 'wptests_tax_2' );

$num_queries = get_num_queries();
$after = get_objects_in_term( $term_id_1, 'wptests_tax_1' );
$this->assertEqualSets( array( $posts[1] ), $after );
$this->assertSame( $num_queries, get_num_queries() );
}

/**
* @ticket 25706
*/
Loading

Unchanged files with check annotations Beta

await expect(
page,
'should redirect to the installation page'
).toHaveURL( /wp-admin\/install\.php$/ );

Check failure on line 40 in tests/e2e/specs/install.test.js

GitHub Actions / Test with SCRIPT_DEBUG disabled / Run E2E tests

[chromium] › tests/e2e/specs/install.test.js:34:6 › WordPress installation process › should install WordPress with pre-existing database credentials

1) [chromium] › tests/e2e/specs/install.test.js:34:6 › WordPress installation process › should install WordPress with pre-existing database credentials Error: should redirect to the installation page Timed out 5000ms waiting for expect(locator).toHaveURL(expected) Locator: locator(':root') Expected pattern: /wp-admin\/install\.php$/ Received string: "http://localhost:8889/" Call log: - should redirect to the installation page with timeout 5000ms - waiting for locator(':root') 9 × locator resolved to <html lang="en-US">…</html> - unexpected value "http://localhost:8889/" 38 | page, 39 | 'should redirect to the installation page' > 40 | ).toHaveURL( /wp-admin\/install\.php$/ ); | ^ 41 | 42 | await expect( 43 | page.getByText( /WordPress database error/ ), at /home/runner/work/wordpress-develop/wordpress-develop/tests/e2e/specs/install.test.js:40:5

Check failure on line 40 in tests/e2e/specs/install.test.js

GitHub Actions / Test with SCRIPT_DEBUG enabled / Run E2E tests

[chromium] › tests/e2e/specs/install.test.js:34:6 › WordPress installation process › should install WordPress with pre-existing database credentials

1) [chromium] › tests/e2e/specs/install.test.js:34:6 › WordPress installation process › should install WordPress with pre-existing database credentials Error: should redirect to the installation page Timed out 5000ms waiting for expect(locator).toHaveURL(expected) Locator: locator(':root') Expected pattern: /wp-admin\/install\.php$/ Received string: "http://localhost:8889/" Call log: - should redirect to the installation page with timeout 5000ms - waiting for locator(':root') 9 × locator resolved to <html lang="en-US">…</html> - unexpected value "http://localhost:8889/" 38 | page, 39 | 'should redirect to the installation page' > 40 | ).toHaveURL( /wp-admin\/install\.php$/ ); | ^ 41 | 42 | await expect( 43 | page.getByText( /WordPress database error/ ), at /home/runner/work/wordpress-develop/wordpress-develop/tests/e2e/specs/install.test.js:40:5
await expect(
page.getByText( /WordPress database error/ ),
test( 'Test dismissing failed upload works correctly', async ({ page, admin, requestUtils }) => {
// Log in before visiting admin page.
await requestUtils.login();

Check failure on line 13 in tests/e2e/specs/media-upload.test.js

GitHub Actions / Test with SCRIPT_DEBUG disabled / Run E2E tests

[chromium] › tests/e2e/specs/media-upload.test.js:11:5 › Test dismissing failed upload works correctly

2) [chromium] › tests/e2e/specs/media-upload.test.js:11:5 › Test dismissing failed upload works correctly Retry #1 ─────────────────────────────────────────────────────────────────────────────────────── Error: apiRequestContext.get: 400 Bad Request Response text: 0 Call log: - → GET http://localhost:8889/wp-admin/admin-ajax.php?action=rest-nonce - user-agent: Playwright/1.50.1 (x64; ubuntu 24.04) node/20.19 CI/1 - accept: */* - accept-encoding: gzip,deflate,br - cookie: wordpress_test_cookie=WP%20Cookie%20check; wp-settings-time-1=1743821577 - ← 400 Bad Request - server: nginx/1.27.4 - date: Sat, 05 Apr 2025 02:53:35 GMT - content-type: text/html; charset=UTF-8 - transfer-encoding: chunked - connection: keep-alive - x-powered-by: PHP/8.2.28 - x-robots-tag: noindex - x-content-type-options: nosniff - referrer-policy: strict-origin-when-cross-origin - x-frame-options: SAMEORIGIN - expires: Wed, 11 Jan 1984 05:00:00 GMT - cache-control: no-cache, must-revalidate, max-age=0, no-store, private 11 | test( 'Test dismissing failed upload works correctly', async ({ page, admin, requestUtils }) => { 12 | // Log in before visiting admin page. > 13 | await requestUtils.login(); | ^ 14 | await admin.visitAdminPage( '/media-new.php' ); 15 | 16 | // It takes a moment for the multi-file uploader to become available. at RequestUtils.login (/home/runner/work/wordpress-develop/wordpress-develop/node_modules/@wordpress/e2e-test-utils-playwright/src/request-utils/login.ts:23:32) at /home/runner/work/wordpress-develop/wordpress-develop/tests/e2e/specs/media-upload.test.js:13:2
await admin.visitAdminPage( '/media-new.php' );

Check failure on line 14 in tests/e2e/specs/media-upload.test.js

GitHub Actions / Test with SCRIPT_DEBUG disabled / Run E2E tests

[chromium] › tests/e2e/specs/media-upload.test.js:11:5 › Test dismissing failed upload works correctly

2) [chromium] › tests/e2e/specs/media-upload.test.js:11:5 › Test dismissing failed upload works correctly Error: Not logged in 12 | // Log in before visiting admin page. 13 | await requestUtils.login(); > 14 | await admin.visitAdminPage( '/media-new.php' ); | ^ 15 | 16 | // It takes a moment for the multi-file uploader to become available. 17 | await page.waitForLoadState('load'); at Admin.visitAdminPage (/home/runner/work/wordpress-develop/wordpress-develop/node_modules/@wordpress/e2e-test-utils-playwright/src/admin/visit-admin-page.ts:36:9) at /home/runner/work/wordpress-develop/wordpress-develop/tests/e2e/specs/media-upload.test.js:14:2

Check failure on line 14 in tests/e2e/specs/media-upload.test.js

GitHub Actions / Test with SCRIPT_DEBUG enabled / Run E2E tests

[chromium] › tests/e2e/specs/media-upload.test.js:11:5 › Test dismissing failed upload works correctly

2) [chromium] › tests/e2e/specs/media-upload.test.js:11:5 › Test dismissing failed upload works correctly Error: Not logged in 12 | // Log in before visiting admin page. 13 | await requestUtils.login(); > 14 | await admin.visitAdminPage( '/media-new.php' ); | ^ 15 | 16 | // It takes a moment for the multi-file uploader to become available. 17 | await page.waitForLoadState('load'); at Admin.visitAdminPage (/home/runner/work/wordpress-develop/wordpress-develop/node_modules/@wordpress/e2e-test-utils-playwright/src/admin/visit-admin-page.ts:36:9) at /home/runner/work/wordpress-develop/wordpress-develop/tests/e2e/specs/media-upload.test.js:14:2
// It takes a moment for the multi-file uploader to become available.
await page.waitForLoadState('load');