Add query result cache feature

This commit is contained in:
Tetrakern 2024-08-16 15:56:17 +02:00
parent a61fdbbef3
commit 30dcc606d4
8 changed files with 271 additions and 5 deletions

View File

@ -298,7 +298,7 @@ Fictioneer customizes WordPress by using as many standard action and filter hook
| `restrict_manage_posts` | `fictioneer_add_chapter_story_filter_dropdown`
| `save_post` | `fictioneer_refresh_chapters_schema`, `fictioneer_refresh_chapter_schema`, `fictioneer_refresh_collections_schema`, `fictioneer_refresh_post_caches`, `fictioneer_refresh_post_schema`, `fictioneer_refresh_recommendations_schema`, `fictioneer_refresh_recommendation_schema`, `fictioneer_refresh_stories_schema`, `fictioneer_refresh_story_schema`, `fictioneer_save_seo_metabox`, `fictioneer_save_word_count`, `fictioneer_track_chapter_and_story_updates`, `fictioneer_update_modified_date_on_story_for_chapter`, `fictioneer_update_shortcode_relationships`, `fictioneer_purge_transients`, `fictioneer_save_story_metaboxes`, `fictioneer_save_chapter_metaboxes`, `fictioneer_save_advanced_metabox`, `fictioneer_save_support_links_metabox`, `fictioneer_save_collection_metaboxes`, `fictioneer_save_recommendation_metaboxes`, `fictioneer_save_post_metaboxes`, `fictioneer_delete_cached_story_card_after_update`
| `show_user_profile` | `fictioneer_custom_profile_fields`
| `shutdown` | `fictioneer_save_story_card_cache`
| `shutdown` | `fictioneer_save_story_card_cache`, `fictioneer_save_query_result_cache_registry`
| `switch_theme` | `fictioneer_theme_deactivation`
| `template_redirect` | `fictioneer_disable_date_archives`, `fictioneer_generate_epub`, `fictioneer_handle_oauth`, `fictioneer_logout`, `fictioneer_disable_attachment_pages`, `fictioneer_gate_unpublished_content`, `fictioneer_serve_sitemap`, `fictioneer_redirect_story`
| `transition_post_status` | `fictioneer_log_story_chapter_status_changes`, `fictioneer_chapter_future_to_publish`, `fictioneer_post_story_to_discord`, `fictioneer_post_chapter_to_discord`

View File

@ -1597,9 +1597,11 @@ define( 'CONSTANT_NAME', value );
| FICTIONEER_QUERY_ID_ARRAY_LIMIT | integer | Maximum allowed IDs in 'post__{not}_in' query arguments. Default `1000`.
| FICTIONEER_PATREON_EXPIRATION_TIME | integer | Time until a users Patreon data expires in seconds. Default `WEEK_IN_SECONDS`.
| FICTIONEER_PARTIAL_CACHE_EXPIRATION_TIME | integer | Time until a cached partial expires in seconds. Default `4 * HOUR_IN_SECONDS`.
| FICTIONEER_CARD_CACHE_LIMIT | integer | Number of cards cached by the story card cache feature if enabled. Default `50`.
| FICTIONEER_CARD_CACHE_LIMIT | integer | Number of story cards cached if the feature is enabled. Default `50`.
| FICTIONEER_CARD_CACHE_EXPIRATION_TIME | integer | Time until the whole story card cache expires in seconds. Default `HOUR_IN_SECONDS`.
| FICTIONEER_STORY_CARD_CHAPTER_LIMIT | integer | Maximum number of chapters shown on story cards. Default 3.
| FICTIONEER_QUERY_RESULT_CACHE_THRESHOLD | integer | Count of a query result required to be eligible for caching. Default `75`.
| FICTIONEER_QUERY_RESULT_CACHE_LIMIT | integer | Number of query results cached if the feature is enabled. Default `50`.
| FICTIONEER_CACHE_PURGE_ASSIST | boolean | Whether to call the cache purge assist function on post updates. Default `true`.
| FICTIONEER_RELATIONSHIP_PURGE_ASSIST | boolean | Whether to purge related post caches. Default `true`.
| FICTIONEER_CHAPTER_LIST_TRANSIENTS | boolean | Whether to cache chapter lists on story pages as Transients. Default `true`.

View File

@ -303,6 +303,16 @@ if ( ! defined( 'FICTIONEER_STORY_CARD_CHAPTER_LIMIT' ) ) {
define( 'FICTIONEER_STORY_CARD_CHAPTER_LIMIT', 3 );
}
// Integer: Count of the query results required to be eligible for caching
if ( ! defined( 'FICTIONEER_QUERY_RESULT_CACHE_THRESHOLD' ) ) {
define( 'FICTIONEER_QUERY_RESULT_CACHE_THRESHOLD', 75 );
}
// Integer: Maximum query results cached as Transients
if ( ! defined( 'FICTIONEER_QUERY_RESULT_CACHE_LIMIT' ) ) {
define( 'FICTIONEER_QUERY_RESULT_CACHE_LIMIT', 50 );
}
/*
* Booleans
*/

View File

@ -83,7 +83,7 @@ if ( ! defined( 'FICTIONEER_ENABLE_PARTIAL_CACHING' ) ) {
);
}
// Boolean: Partial caching enabled?
// Boolean: Story card caching enabled?
if ( ! defined( 'FICTIONEER_ENABLE_STORY_CARD_CACHING' ) ) {
define(
'FICTIONEER_ENABLE_STORY_CARD_CACHING',
@ -91,6 +91,14 @@ if ( ! defined( 'FICTIONEER_ENABLE_STORY_CARD_CACHING' ) ) {
);
}
// Boolean: Query result caching enabled=
if ( ! defined( 'FICTIONEER_ENABLE_QUERY_RESULT_CACHING' ) ) {
define(
'FICTIONEER_ENABLE_QUERY_RESULT_CACHING',
get_option( 'fictioneer_enable_query_result_caching' ) && ! is_customize_preview()
);
}
// =============================================================================
// ENABLE SHORTCODE TRANSIENTS?
// =============================================================================
@ -683,6 +691,9 @@ if ( ! function_exists( 'fictioneer_track_chapter_and_story_updates' ) ) {
delete_post_meta( $story_id, 'fictioneer_story_data_collection' );
delete_post_meta( $story_id, 'fictioneer_story_chapter_index_html' );
// Delete cached query results
fictioneer_delete_transients_like( "fictioneer_query_{$story_id}" );
// Refresh cached HTML output
delete_transient( 'fictioneer_story_chapter_list_html' . $story_id );
@ -1326,3 +1337,185 @@ function fictioneer_get_cached_story_card( $key ) {
// Return cached card
return $card;
}
// =============================================================================
// TRANSIENT QUERY RESULT CACHE
// =============================================================================
/**
* Returns the global registry array for cached query results
*
* Note: The registry is stored as auto-loaded options in the
* database and set up as global variable on request. It is
* only updated in the 'shutdown' action.
*
* @since 5.22.3
* @global $fictioneer_query_result_registry
*
* @return array Array with Transient keys for cached query results.
*/
function fictioneer_get_query_result_cache_registry() {
// Abort if...
if ( ! FICTIONEER_ENABLE_QUERY_RESULT_CACHING ) {
return [];
}
// Initialize global cache variable
global $fictioneer_query_result_registry;
if ( ! $fictioneer_query_result_registry ) {
$fictioneer_query_result_registry = get_option( 'fictioneer_query_cache_registry' );
if ( ! is_array( $fictioneer_query_result_registry ) ) {
$fictioneer_query_result_registry = [];
update_option( 'fictioneer_query_cache_registry', [] );
}
}
// Return array for good measure, but the global will do
return $fictioneer_query_result_registry;
}
/**
* Adds query result to the global query result cache
*
* Note: Only query results that exceed a certain threshold (75)
* are stored in the cache and only the latest n items (50).
*
* @since 5.22.3
* @global $fictioneer_query_result_registry
* @see FICTIONEER_QUERY_RESULT_CACHE_THRESHOLD
* @see FICTIONEER_QUERY_RESULT_CACHE_LIMIT
*
* @param string $key The cache key of the query.
* @param array $result The result of the query.
*/
function fictioneer_cache_query_result( $key, $result ) {
// Abort if...
if ( ! FICTIONEER_ENABLE_QUERY_RESULT_CACHING ) {
return;
}
if ( count( $result ) < FICTIONEER_QUERY_RESULT_CACHE_THRESHOLD ) {
return;
}
// Initialize global cache variable
global $fictioneer_query_result_registry;
if ( ! $fictioneer_query_result_registry ) {
$fictioneer_query_result_registry = fictioneer_get_query_result_cache_registry();
}
// Build key; must begin with fictioneer_query_{$post_id} for clearing purposes
$transient_key = "fictioneer_query_{$key}";
// Prepend result to stack
$fictioneer_query_result_registry = array_merge( array( $key => $transient_key ), $fictioneer_query_result_registry );
// Randomized expiration to avoid large sets of results to expire simultaneously
set_transient( $transient_key, $result, 8 * HOUR_IN_SECONDS + rand( 0, 4 * HOUR_IN_SECONDS ) );
// Only allow n items in the cache to avoid bloating the database
if ( count( $fictioneer_query_result_registry ) > FICTIONEER_QUERY_RESULT_CACHE_LIMIT ) {
array_pop( $fictioneer_query_result_registry ); // Drop oldest entry
}
}
/**
* Adds query result to the global query result cache
*
* Note: Only query results that exceed a certain threshold (75)
* are stored in the cache and only the latest n items (50).
*
* @since 5.22.3
* @global $fictioneer_query_result_registry
*
* @param string $key The cache key of the query.
*
* @return array|null Array of WP_Post objects or null if not cached or expired.
*/
function fictioneer_get_cached_query_result( $key ) {
// Abort if...
if ( ! FICTIONEER_ENABLE_QUERY_RESULT_CACHING ) {
return null;
}
// Initialize global cache variable
global $fictioneer_query_result_registry;
if ( ! $fictioneer_query_result_registry ) {
$fictioneer_query_result_registry = fictioneer_get_query_result_cache_registry();
}
// Look for cached result...
if ( isset( $fictioneer_query_result_registry[ $key ] ) ) {
$transient = get_transient( $fictioneer_query_result_registry[ $key ] );
if ( is_array( $transient ) ) {
// Hit
return $transient;
} else {
// Miss
fictioneer_delete_cached_query_result( $key ); // Cleanup
}
}
// Nothing cached or expired
return null;
}
/**
* Deletes query result from the global query result cache
*
* @since 5.22.3
* @global $fictioneer_query_result_registry
*
* @param string $key The cache key of the query.
*/
function fictioneer_delete_cached_query_result( $key ) {
// Abort if...
if ( ! FICTIONEER_ENABLE_QUERY_RESULT_CACHING ) {
return;
}
// Initialize global cache variable
global $fictioneer_query_result_registry;
if ( ! $fictioneer_query_result_registry ) {
$fictioneer_query_result_registry = fictioneer_get_query_result_cache_registry();
}
// Remove entry (if present)
unset( $fictioneer_query_result_registry[ $key ] );
}
/**
* Saves the final global query result cache to the database
*
* @since 5.22.3
* @global $fictioneer_query_result_registry
*/
function fictioneer_save_query_result_cache_registry() {
// Abort if...
if ( ! FICTIONEER_ENABLE_QUERY_RESULT_CACHING ) {
return;
}
// Initialize global cache variable
global $fictioneer_query_result_registry;
// Anything to save?
if (
! is_null( $fictioneer_query_result_registry ) &&
$fictioneer_query_result_registry !== get_option( 'fictioneer_query_cache_registry' )
) {
update_option( 'fictioneer_query_cache_registry', $fictioneer_query_result_registry ?: [] );
}
}
add_action( 'shutdown', 'fictioneer_save_query_result_cache_registry' );

View File

@ -196,16 +196,20 @@ if ( ! function_exists( 'fictioneer_get_last_fiction_update' ) ) {
/**
* Returns array of chapter posts for a story
*
* Note: Returns reduced WP_Post objects with several properties stripped,
* such as the content and excerpt.
*
* @since 5.9.2
* @since 5.22.3 - Refactored.
*
* @param int $story_id ID of the story.
* @param array $args Optional. Additional query arguments.
* @param bool $full Optional. Whether to not reduce the posts. Default false.
*
* @return array Array of chapter posts or empty.
*/
function fictioneer_get_story_chapter_posts( $story_id, $args = [] ) {
function fictioneer_get_story_chapter_posts( $story_id, $args = [], $full = false ) {
// Static variable cache
static $cached_results = [];
@ -240,6 +244,13 @@ function fictioneer_get_story_chapter_posts( $story_id, $args = [] ) {
return $cached_results[ $cache_key ];
}
// Query result cache registry hit?
$cached_query_result = fictioneer_get_cached_query_result( $cache_key );
if ( $cached_query_result ) {
return $cached_query_result;
}
// Batched or one go?
if ( count( $chapter_ids ) <= FICTIONEER_QUERY_ID_ARRAY_LIMIT ) {
$query_args['post__in'] = $chapter_ids ?: [0];
@ -263,9 +274,34 @@ function fictioneer_get_story_chapter_posts( $story_id, $args = [] ) {
return $chapter_positions[ $a->ID ] - $chapter_positions[ $b->ID ];
});
// F-REF-1
if ( ! $full ) {
foreach ( $chapter_posts as $post ) {
// Chapter contents are extremely large and this kind of
// query does not need them, but we leave a reference
// in case this causes issues in the future.
$post->post_content = 'F-REF-1';
unset(
$post->post_type, // We know this if fcn_chapter
$post->ping_status, // Unused
$post->to_ping, // Unused
$post->pinged, // Unused
$post->menu_order, // Unused
$post->post_mime_type, // Unused
$post->post_parent, // Unused
$post->post_content_filtered, // Unused here
$post->guid, // Unused here
$post->post_excerpt // Unused here
);
}
}
// Cache for subsequent calls
$cached_results[ $cache_key ] = $chapter_posts;
fictioneer_cache_query_result( $cache_key, $chapter_posts );
// Return chapters selected in story
return $chapter_posts;
}
@ -2564,7 +2600,7 @@ if ( ! function_exists( 'fictioneer_multi_save_guard' ) ) {
function fictioneer_multi_save_guard( $post_id ) {
if (
( defined('REST_REQUEST') && REST_REQUEST ) ||
( defined( 'REST_REQUEST' ) && REST_REQUEST ) ||
wp_is_post_autosave( $post_id ) ||
wp_is_post_revision( $post_id ) ||
get_post_status( $post_id ) === 'auto-draft'

View File

@ -683,6 +683,12 @@ define( 'FICTIONEER_OPTIONS', array(
'group' => 'fictioneer-settings-general-group',
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
'default' => 0
),
'fictioneer_enable_query_result_caching' => array(
'name' => 'fictioneer_enable_query_result_caching',
'group' => 'fictioneer-settings-general-group',
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
'default' => 0
)
),
'integers' => array(
@ -1148,6 +1154,7 @@ function fictioneer_get_option_label( $option ) {
'fictioneer_disable_anti_flicker' => __( 'Disable anti-flicker script', 'fictioneer' ),
'fictioneer_hide_categories' => __( 'Hide categories on posts', 'fictioneer' ),
'fictioneer_enable_story_card_caching' => __( 'Enable caching of story cards', 'fictioneer' ),
'fictioneer_enable_query_result_caching' => __( 'Enable caching of large query results', 'fictioneer' ),
);
}

View File

@ -487,6 +487,9 @@ function fictioneer_purge_theme_caches() {
// Cache busting string
fictioneer_regenerate_cache_bust();
// Query result cache registry
delete_option( 'fictioneer_query_cache_registry' );
// Log
fictioneer_log( __( 'Purged theme caches.', 'fictioneer' ) );
}

View File

@ -748,6 +748,21 @@
?>
</div>
<div class="fictioneer-card__row">
<?php
fictioneer_settings_label_checkbox(
'fictioneer_enable_query_result_caching',
__( 'Enable caching of large query results', 'fictioneer' ),
sprintf(
__( 'Caches the latest %d query results with %s or more posts in the database to speed up loading.', 'fictioneer' ),
FICTIONEER_QUERY_RESULT_CACHE_LIMIT,
FICTIONEER_QUERY_RESULT_CACHE_THRESHOLD
),
__( '<p>Queries that return many results are resource-intensive and can slow down your site, especially for stories with hundreds of chapters. This feature mitigates the slowdown by caching the results of the largest queries in the database, typically resulting in faster loading times.</p><p>You can use the <code>FICTIONEER_QUERY_RESULT_CACHE_LIMIT</code> constant to change the number of cached results (default is 50) and the <code>FICTIONEER_QUERY_RESULT_CACHE_THRESHOLD</code> constant to change what constitutes as large result (default is 75). Be aware that increasing these numbers will result in higher RAM consumption.</p>', 'fictioneer' )
);
?>
</div>
</div>
</div>
</div>