From 0d2ffede49ed7b59ccb99740f66e6f9b591e0cbb Mon Sep 17 00:00:00 2001 From: Ian Dunn Date: Wed, 27 Sep 2023 16:07:35 -0700 Subject: [PATCH] Events: Cache event queries for performance. Co-Authored-By: Paul Kevan --- .../wporg-events-2023/inc/event-getters.php | 120 +++++++++++++++++- .../patterns/city-landing-page-map.php | 4 +- 2 files changed, 117 insertions(+), 7 deletions(-) diff --git a/public_html/wp-content/themes/wporg-events-2023/inc/event-getters.php b/public_html/wp-content/themes/wporg-events-2023/inc/event-getters.php index fcdd2b492..0e0fb544d 100644 --- a/public_html/wp-content/themes/wporg-events-2023/inc/event-getters.php +++ b/public_html/wp-content/themes/wporg-events-2023/inc/event-getters.php @@ -5,6 +5,72 @@ defined( 'WPINC' ) || die(); + +add_action( 'init', __NAMESPACE__ . '\schedule_cron_jobs' ); +add_action( 'events_landing_prime_query_cache', __NAMESPACE__ . '\prime_query_cache' ); + +/** + * Schedule cron jobs. + */ +function schedule_cron_jobs(): void { + if ( ! wp_next_scheduled( 'events_landing_prime_query_cache' ) ) { + wp_schedule_event( time(), 'hourly', 'events_landing_prime_query_cache' ); + } +} + +/** + * Prime the caches of events. + * + * Without this, users would have to wait for new results to be generated every time the cache expires. That could + * make the front end very slow. This will refresh the cache before it expires, so that the front end can always + * load cached results. + */ +function prime_query_cache(): void { + get_all_upcoming_events( true ); + + $city_landing_uris = get_known_city_landing_request_uris(); + + foreach ( $city_landing_uris as $request_uri ) { + get_city_landing_page_events( $request_uri, true ); + } +} + +/** + * Get a list of all known city landing page request URIs. + * + * For example, `/rome/`, `/rome/training/`, `/rome/2023/`. + */ +function get_known_city_landing_request_uris(): array { + $city_landing_pages = array(); + + $city_sites = get_sites( array( + 'network_id' => EVENTS_NETWORK_ID, + 'path__not_in' => array( '/' ), + 'number' => false, + 'public' => 1, + 'archived' => 0, + 'deleted' => 0, + ) ); + + foreach ( $city_sites as $site ) { + $parts = explode( '/', trim( $site->path, '/' ) ); + + if ( 3 !== count( $parts ) ) { + continue; + } + + $city = $parts[0]; + $year = absint( $parts[1] ); + $title = preg_replace( '#^(.*)(-\d+)$#', '$1', $parts[2] ); // Strip any `-2`, `-3`, `-n` suffixes. + + $city_landing_pages[ sprintf( '/%s/', $city ) ] = true; + $city_landing_pages[ sprintf( '/%s/%s/', $city, $year ) ] = true; + $city_landing_pages[ sprintf( '/%s/%s/', $city, $title ) ] = true; + } + + return array_keys( $city_landing_pages ); +} + /** * Query a table that's encoded with the `latin1` charset. * @@ -13,6 +79,8 @@ * `utf8mb4`, you'll get Mojibake. * * @param string $prepared_query ⚠️ This must have already be ran through `$wpdb->prepare()` if needed. + * + * @return object|null */ function get_latin1_results_with_prepared_query( string $prepared_query ) { global $wpdb; @@ -33,8 +101,16 @@ function get_latin1_results_with_prepared_query( string $prepared_query ) { /** * Get a list of all upcoming events across all sites. */ -function get_all_upcoming_events(): array { - global $wpdb; +function get_all_upcoming_events( bool $force_refresh = false ): array { + $cache_key = 'event_landing_all_upcoming_events'; + + if ( ! $force_refresh ) { + $cached_events = get_transient( $cache_key ); + + if ( $cached_events ) { + return $cached_events; + } + } $events = get_latin1_results_with_prepared_query( ' SELECT @@ -62,6 +138,9 @@ function get_all_upcoming_events(): array { unset( $event->date_utc ); } + // `prime_query_cache()` should update this hourly, but expire after a day just in case it doesn't. + set_transient( $cache_key, $events, DAY_IN_SECONDS ); + return $events; } @@ -70,8 +149,22 @@ function get_all_upcoming_events(): array { * * See `get_city_landing_sites()` for how request URIs map to events. */ -function get_city_landing_page_events( string $request_uri ): array { - $limit = 300; +function get_city_landing_page_events( string $request_uri, bool $force_refresh = false ): array { + $limit = 300; + $cache_key = 'event_landing_city_events_' . md5( $request_uri ); + + if ( empty( $request_uri ) || '/' === $request_uri ) { + return array(); + } + + if ( ! $force_refresh ) { + $cached_events = get_transient( $cache_key ); + + if ( $cached_events ) { + return $cached_events; + } + } + $sites = get_city_landing_sites( $request_uri, $limit ); switch_to_blog( WORDCAMP_ROOT_BLOG_ID ); @@ -109,14 +202,31 @@ function get_city_landing_page_events( string $request_uri ): array { restore_current_blog(); + // `prime_query_cache()` should update this hourly, but expire after a day just in case it doesn't find all the + // valid request URIs. + set_transient( $cache_key, $events, DAY_IN_SECONDS ); + return $events; } +/** + * Standardize request URIs so they can be reliably used as cache keys. + * + * For example, `/rome`, `/rome/` and `/rome/?foo=bar` should all be `/rome/`. + */ +function normalize_request_uri( $raw_uri, $query_string ) { + $clean_uri = str_replace( '?' . $query_string, '', $raw_uri ); + $clean_uri = trailingslashit( $clean_uri ); + $clean_uri = '/' . ltrim( $clean_uri, '/' ); + + return $clean_uri; +} + /** * Get sites that match the given request URI. * * /rome/ -> All sites in Rome - * /rome/training/ -> All training sites in Rome + * /rome/training/ -> All training sites in Rome (including those with slugs like `training-2`) * /rome/2023/ -> All sites in Rome in 2023 */ function get_city_landing_sites( string $request_uri, int $limit ): array { diff --git a/public_html/wp-content/themes/wporg-events-2023/patterns/city-landing-page-map.php b/public_html/wp-content/themes/wporg-events-2023/patterns/city-landing-page-map.php index d189e07c8..92fcedea3 100644 --- a/public_html/wp-content/themes/wporg-events-2023/patterns/city-landing-page-map.php +++ b/public_html/wp-content/themes/wporg-events-2023/patterns/city-landing-page-map.php @@ -14,8 +14,8 @@ return; } -// Can't use $wp->request because Gutenberg calls this on `init`, before `parse_request`. -$request_uri = str_replace( '?' . $_SERVER['QUERY_STRING'], '', $_SERVER['REQUEST_URI'] ); +// Can't use `$wp->request` because Gutenberg calls this on `init`, before `parse_request`. +$request_uri = normalize_request_uri( $_SERVER['REQUEST_URI'], $_SERVER['QUERY_STRING'] ); $map_options = array( 'id' => 'city-landing-page',