From 4f62ae4f60e73f0efc1bab71b0d65418446ff3df Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 24 Oct 2024 17:39:35 +1300 Subject: [PATCH] Stats: Use option value instead of transient for cache buster (#39887) --- ...date-use-option-value-instead-of-transient | 4 + .../stats-admin/src/class-odyssey-assets.php | 40 ++++++-- .../stats-admin/tests/php/class-test-case.php | 2 + .../tests/php/test-odyssey-assets.php | 96 ++++++++++++++++++- 4 files changed, 132 insertions(+), 10 deletions(-) create mode 100644 projects/packages/stats-admin/changelog/update-use-option-value-instead-of-transient diff --git a/projects/packages/stats-admin/changelog/update-use-option-value-instead-of-transient b/projects/packages/stats-admin/changelog/update-use-option-value-instead-of-transient new file mode 100644 index 0000000000000..6e6660e388644 --- /dev/null +++ b/projects/packages/stats-admin/changelog/update-use-option-value-instead-of-transient @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Odyssey Stats cache busting: use optioin instead of transient diff --git a/projects/packages/stats-admin/src/class-odyssey-assets.php b/projects/packages/stats-admin/src/class-odyssey-assets.php index 8117e650ae5fa..98d59fc3bb3ad 100644 --- a/projects/packages/stats-admin/src/class-odyssey-assets.php +++ b/projects/packages/stats-admin/src/class-odyssey-assets.php @@ -75,36 +75,58 @@ public function load_admin_scripts( $asset_handle, $asset_name, $options = array /** * Returns cache buster string for assets. * Development mode doesn't need this, as it's handled by `Assets` class. + * + * @return string */ protected function get_cdn_asset_cache_buster() { + $now_in_ms = floor( microtime( true ) * 1000 ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( isset( $_GET['force_refresh'] ) ) { - set_transient( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, floor( microtime( true ) * 1000 ), 15 * MINUTE_IN_SECONDS ); + update_option( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, $this->get_cache_buster_option_value( $now_in_ms ), false ); } // Use cached cache buster in production. - $remote_asset_version = get_transient( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY ); + $remote_asset_version = get_option( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY ); if ( ! empty( $remote_asset_version ) ) { - return $remote_asset_version; + $remote_asset_version = json_decode( $remote_asset_version, true ); + // If cache buster is cached and not expired (valid in 15 min), return it. + if ( ! empty( $remote_asset_version['cache_buster'] ) && $remote_asset_version['cached_at'] > $now_in_ms - MINUTE_IN_SECONDS * 1000 * 15 ) { + return $remote_asset_version['cache_buster']; + } } // If no cached cache buster, we fetch it from CDN and set to transient. - $response = wp_remote_get( sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, 'build_meta.json?t=' . time() ), array( 'timeout' => 5 ) ); + $response = wp_remote_get( sprintf( self::ODYSSEY_CDN_URL, self::ODYSSEY_STATS_VERSION, 'build_meta.json?t=' . $now_in_ms ), array( 'timeout' => 5 ) ); if ( is_wp_error( $response ) ) { - // fallback to the package version. - return Main::VERSION; + // fallback to current timestamp. + return (string) $now_in_ms; } $build_meta = json_decode( wp_remote_retrieve_body( $response ), true ); if ( ! empty( $build_meta['cache_buster'] ) ) { // Cache the cache buster for 15 mins. - set_transient( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, $build_meta['cache_buster'], 15 * MINUTE_IN_SECONDS ); + update_option( self::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, $this->get_cache_buster_option_value( $build_meta['cache_buster'] ), false ); return $build_meta['cache_buster']; } - // fallback to the package version. - return Main::VERSION; + // fallback to current timestamp. + return (string) $now_in_ms; + } + + /** + * Get the cache buster option value. + * + * @param string|int|float $cache_buster The cache buster. + * @return string|false + */ + protected function get_cache_buster_option_value( $cache_buster ) { + return wp_json_encode( + array( + 'cache_buster' => (string) $cache_buster, + 'cached_at' => floor( microtime( true ) * 1000 ), // milliseconds. + ) + ); } } diff --git a/projects/packages/stats-admin/tests/php/class-test-case.php b/projects/packages/stats-admin/tests/php/class-test-case.php index e6b919b204a4f..0d21e264cf842 100644 --- a/projects/packages/stats-admin/tests/php/class-test-case.php +++ b/projects/packages/stats-admin/tests/php/class-test-case.php @@ -60,6 +60,7 @@ public function set_up() { add_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ), 10, 2 ); add_filter( 'pre_http_request', array( $this, 'plan_http_response_fixture' ), 10, 3 ); + delete_option( Odyssey_Assets::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY ); } /** @@ -76,6 +77,7 @@ public function tear_down() { remove_filter( 'pre_http_request', array( $this, 'plan_http_response_fixture' ) ); remove_filter( 'jetpack_options', array( $this, 'mock_jetpack_site_connection_options' ) ); + delete_option( Odyssey_Assets::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY ); } /** diff --git a/projects/packages/stats-admin/tests/php/test-odyssey-assets.php b/projects/packages/stats-admin/tests/php/test-odyssey-assets.php index f99eca36ad1fe..14e1fb5c3bec9 100644 --- a/projects/packages/stats-admin/tests/php/test-odyssey-assets.php +++ b/projects/packages/stats-admin/tests/php/test-odyssey-assets.php @@ -2,6 +2,7 @@ namespace Automattic\Jetpack\Stats_Admin; use Automattic\Jetpack\Stats_Admin\Test_Case as Stats_Test_Case; +use WP_Error; /** * Unit tests for the Odyssey_Assets class. @@ -9,13 +10,106 @@ * @package automattic/jetpack-stats-admin */ class Test_Odyssey_Assets extends Stats_Test_Case { + /** * Test remote cache buster. */ public function test_get_cdn_asset_cache_buster() { + $this->assertEquals( 'calypso-4917-8664-g72a154d63a', $this->get_cdn_asset_cache_buster_callable() ); + } + + /** + * Test remote cache buster remote error. + */ + public function test_get_cdn_asset_cache_buster_remote_error() { + add_filter( 'pre_http_request', array( $this, 'break_cdn_cache_buster_request' ), 15, 3 ); + $this->assertEquals( time(), floor( $this->get_cdn_asset_cache_buster_callable() / 1000 ) ); + remove_filter( 'pre_http_request', array( $this, 'break_cdn_cache_buster_request' ), 15 ); + } + + /** + * Test already cached cache buster. + */ + public function test_get_cdn_asset_cache_buster_already_cached() { + update_option( + Odyssey_Assets::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, + wp_json_encode( + array( + 'cache_buster' => 'calypso-4917-8664-123456', + 'cached_at' => floor( microtime( true ) * 1000 ), // milliseconds. + ) + ), + false + ); + $this->assertEquals( 'calypso-4917-8664-123456', $this->get_cdn_asset_cache_buster_callable() ); + } + + /** + * Test already cached cache buster expired. + */ + public function test_get_cdn_asset_cache_buster_already_cached_expired() { + update_option( + Odyssey_Assets::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, + wp_json_encode( + array( + 'cache_buster' => 'calypso-4917-8664-123456', + 'cached_at' => floor( microtime( true ) * 1000 - MINUTE_IN_SECONDS * 1000 * 20 ), // milliseconds. + ) + ), + false + ); + $this->assertEquals( 'calypso-4917-8664-g72a154d63a', $this->get_cdn_asset_cache_buster_callable() ); + } + + /** + * Test already cached cache buster expired and failed to fetch new one. + */ + public function test_get_cdn_asset_cache_buster_failed_to_fetch() { + add_filter( 'pre_http_request', array( $this, 'break_cdn_cache_buster_request' ), 15, 3 ); + update_option( + Odyssey_Assets::ODYSSEY_STATS_CACHE_BUSTER_CACHE_KEY, + wp_json_encode( + array( + 'cache_buster' => 'calypso-4917-8664-123456', + 'cached_at' => floor( microtime( true ) * 1000 - MINUTE_IN_SECONDS * 1000 * 20 ), // milliseconds. + ) + ), + false + ); + $this->assertEquals( time(), floor( $this->get_cdn_asset_cache_buster_callable() / 1000 ) ); + remove_filter( 'pre_http_request', array( $this, 'break_cdn_cache_buster_request' ), 15 ); + } + + /** + * Test force refresh cache buster. + */ + public function test_get_cdn_asset_cache_buster_force_refresh_expired() { + $_GET['force_refresh'] = 1; + $this->assertEquals( time(), floor( $this->get_cdn_asset_cache_buster_callable() / 1000 ) ); + } + + /** + * Test remote cache buster. + * + * @param mixed $response The response array. + * @param mixed $parsed_args The parsed args. + * @param mixed $url The URL. + * @return WP_Error | void + */ + public function break_cdn_cache_buster_request( $response, $parsed_args, $url ) { + if ( strpos( $url, '/build_meta.json' ) !== false ) { + return new WP_Error( 500, 'Internal Server Error' ); + } + } + + /** + * Get CDN asset cache buster. + */ + protected function get_cdn_asset_cache_buster_callable() { $odyssey_assets = new Odyssey_Assets(); $get_cdn_asset_cache_buster = new \ReflectionMethod( $odyssey_assets, 'get_cdn_asset_cache_buster' ); $get_cdn_asset_cache_buster->setAccessible( true ); - $this->assertEquals( 'calypso-4917-8664-g72a154d63a', $get_cdn_asset_cache_buster->invoke( $odyssey_assets ) ); + + return $get_cdn_asset_cache_buster->invoke( $odyssey_assets ); } }