From cff2429a7f56938ca1ee4ee81298db0f518708c4 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Mon, 1 Sep 2025 16:17:21 +0200 Subject: [PATCH 1/6] Initial cache prefetch implementation --- ...lass-wc-stripe-database-cache-prefetch.php | 220 ++++++++++++++++++ includes/class-wc-stripe-database-cache.php | 54 ++++- ...c-stripe-payment-method-configurations.php | 2 +- includes/class-wc-stripe.php | 4 + ...WC_Stripe_Database_Cache_Prefetch_Test.php | 130 +++++++++++ 5 files changed, 404 insertions(+), 6 deletions(-) create mode 100644 includes/class-wc-stripe-database-cache-prefetch.php create mode 100644 tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php diff --git a/includes/class-wc-stripe-database-cache-prefetch.php b/includes/class-wc-stripe-database-cache-prefetch.php new file mode 100644 index 0000000000..7f8b4ee80f --- /dev/null +++ b/includes/class-wc-stripe-database-cache-prefetch.php @@ -0,0 +1,220 @@ + 10, + ]; + + /** + * The prefix used for prefetch tracking options. + * + * @var string + */ + private const PREFETCH_OPTION_PREFIX = 'wcstripe_prefetch_'; + + /** + * The singleton instance. + */ + private static ?WC_Stripe_Database_Cache_Prefetch $instance = null; + + /** + * Protected constructor to support singleton pattern. + */ + protected function __construct() {} + + /** + * Get the singleton instance. + * + * @return WC_Stripe_Database_Cache_Prefetch The singleton instance. + */ + public static function get_instance(): WC_Stripe_Database_Cache_Prefetch { + if ( null === self::$instance ) { + self::$instance = new self(); + } + return self::$instance; + } + + /** + * Check if the unprefixed cache key has prefetch enabled. + * + * @param string $key The unprefixed cache key to check. + * @return bool True if the cache key can be prefetched, false otherwise. + */ + public function should_prefetch_cache_key( string $key ): bool { + return isset( self::PREFETCH_CONFIG[ $key ] ) && self::PREFETCH_CONFIG[ $key ] > 0; + } + + /** + * Maybe queue a prefetch for a cache key. + * + * @param string $key The unprefixed cache key to prefetch. + * @param int $expiry_time The expiry time of the cache entry. + */ + public function maybe_queue_prefetch( string $key, int $expiry_time ): void { + if ( ! $this->should_prefetch_cache_key( $key ) ) { + return; + } + + $prefetch_window = self::PREFETCH_CONFIG[ $key ]; + + // If now plus the prefetch window is before the expiry time, do not trigger a prefetch. + if ( ( time() + $prefetch_window ) < $expiry_time ) { + return; + } + + $logging_context = [ + 'cache_key' => $key, + 'expiry_time' => $expiry_time, + ]; + + if ( $this->is_prefetch_queued( $key ) ) { + WC_Stripe_Logger::debug( 'Cache prefetch already pending', $logging_context ); + return; + } + + if ( ! did_action( 'action_scheduler_init' ) || ! function_exists( 'as_enqueue_async_action' ) ) { + WC_Stripe_Logger::debug( 'Unable to enqueue cache prefetch: Action Scheduler is not initialized or available', $logging_context ); + return; + } + + $prefetch_option_key = $this->get_prefetch_option_name( $key ); + update_option( $prefetch_option_key, time() ); + + $result = as_enqueue_async_action( self::ASYNC_PREFETCH_ACTION, [ $key ], 'woocommerce-gateway-stripe' ); + if ( 0 === $result ) { + WC_Stripe_Logger::warning( 'Failed to enqueue cache prefetch', $logging_context ); + } else { + WC_Stripe_Logger::debug( 'Enqueued cache prefetch', $logging_context ); + } + } + + /** + * Check if a prefetch is already queued up. + * + * @param string $key The unprefixed cache key to check. + * @return bool True if a prefetch is queued up, false otherwise. + */ + private function is_prefetch_queued( string $key ): bool { + if ( ! isset( self::PREFETCH_CONFIG[ $key ] ) ) { + return false; + } + + $prefetch_option_key = $this->get_prefetch_option_name( $key ); + + $prefetch_option = get_option( $prefetch_option_key, false ); + // We use ctype_digit() and the (string) cast to ensure we handle the option value being returned as a string. + if ( ! ctype_digit( (string) $prefetch_option ) ) { + return false; + } + + $now = time(); + $prefetch_window = self::PREFETCH_CONFIG[ $key ]; + + if ( $prefetch_option >= ( $now - $prefetch_window ) ) { + // If the prefetch entry expires in the future, or falls within the prefetch window for the key, we should consider the item live and queued. + // We use a prefetch window buffer to account for latency on the prefetch processing and to make sure we don't prefetch more than once during the prefetch window. + return true; + } + + return false; + } + + /** + * Get the name of the prefetch tracking option for a given cache key. + * + * @param string $key The unprefixed cache key to get the option name for. + * @return string The name of the prefetch option. + */ + private function get_prefetch_option_name( string $key ): string { + return self::PREFETCH_OPTION_PREFIX . $key; + } + + /** + * Handle the prefetch action. We are generally expecting this to be queued up by Action Scheduler using + * the action from {@see ASYNC_PREFETCH_ACTION}. + * + * @param string $key The unprefixed cache key to prefetch. + * @return void + */ + public function handle_prefetch_action( $key ): void { + if ( ! is_string( $key ) || empty( $key ) ) { + WC_Stripe_Logger::warning( + 'Invalid cache prefetch key', + [ + 'cache_key' => $key, + 'reason' => 'invalid_key', + ] + ); + return; + } + + if ( ! $this->should_prefetch_cache_key( $key ) ) { + WC_Stripe_Logger::warning( + 'Invalid cache prefetch key', + [ + 'cache_key' => $key, + 'reason' => 'unsupported_cache_key', + ] + ); + return; + } + + $this->prefetch_cache_key( $key ); + + // Regardless of whether the prefetch was successful or not, we should remove the prefetch tracking option. + delete_option( $this->get_prefetch_option_name( $key ) ); + } + + /** + * Helper method to implement prefetch/repopulation for supported cache entries. + * + * @param string $key The unprefixed cache key to prefetch. + * @return bool|null True if the prefetch was successful, false if the prefetch failed, or null if the prefetch was not attempted. + */ + protected function prefetch_cache_key( string $key ): ?bool { + $prefetched = null; + + switch ( $key ) { + case WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY: + if ( WC_Stripe_Payment_Method_Configurations::is_enabled() ) { + WC_Stripe_Payment_Method_Configurations::get_upe_enabled_payment_method_ids( true ); + $prefetched = true; + } else { + $prefetched = false; + WC_Stripe_Logger::debug( 'Unable to prefetch PMC cache as settings sync is disabled', [ 'cache_key' => $key ] ); + } + break; + default: + break; + } + + if ( true === $prefetched ) { + WC_Stripe_Logger::debug( 'Successfully prefetched cache key', [ 'cache_key' => $key ] ); + } elseif ( null === $prefetched ) { + WC_Stripe_Logger::warning( 'Prefetch cache key not handled', [ 'cache_key' => $key ] ); + } else { + WC_Stripe_Logger::debug( 'Failed to prefetch cache key', [ 'cache_key' => $key ] ); + } + + return $prefetched; + } +} diff --git a/includes/class-wc-stripe-database-cache.php b/includes/class-wc-stripe-database-cache.php index e6e2503c48..d0a854aed3 100644 --- a/includes/class-wc-stripe-database-cache.php +++ b/includes/class-wc-stripe-database-cache.php @@ -101,6 +101,8 @@ public static function get( $key ) { return null; } + self::maybe_trigger_prefetch( $key, $cache_contents ); + return $cache_contents['data']; } @@ -209,18 +211,17 @@ private static function get_from_cache( $prefixed_key ) { * @return boolean True if the contents are expired. False otherwise. */ private static function is_expired( $prefixed_key, $cache_contents ) { - if ( ! is_array( $cache_contents ) || ! isset( $cache_contents['updated'] ) || ! isset( $cache_contents['ttl'] ) ) { + if ( ! is_array( $cache_contents ) ) { // Treat bad/invalid cache contents as expired return true; } - // Double-check that we have integers for `updated` and `ttl`. - if ( ! is_int( $cache_contents['updated'] ) || ! is_int( $cache_contents['ttl'] ) ) { + $expires = self::get_expiry_time( $cache_contents ); + if ( null === $expires ) { return true; } - $expires = $cache_contents['updated'] + $cache_contents['ttl']; - $now = time(); + $now = time(); /** * Filters the result of the database cache entry expiration check. @@ -236,6 +237,49 @@ private static function is_expired( $prefixed_key, $cache_contents ) { return apply_filters( 'wc_stripe_database_cache_is_expired', $expires < $now, $prefixed_key, $cache_contents ); } + /** + * Get the expiry time for a cache entry. Includes validation for time-related fields in the array. + * + * @param array $cache_contents The cache contents. + * + * @return int|null The expiry time as a timestamp. Null if the expiry time can't be determined. + */ + private static function get_expiry_time( array $cache_contents ): ?int { + // If we don't have updated and ttl keys, expiry time is unknown. + if ( ! isset( $cache_contents['updated'], $cache_contents['ttl'] ) ) { + return null; + } + + // If we don't have integers for updated and ttl, expiry time is unknown. + if ( ! is_int( $cache_contents['updated'] ) || ! is_int( $cache_contents['ttl'] ) ) { + return null; + } + + return $cache_contents['updated'] + $cache_contents['ttl']; + } + + /** + * Maybe trigger a cache prefetch. + * + * @param string $key The unprefixed cache key. + * @param array $cache_contents The cache contents. + * + * @return void + */ + private static function maybe_trigger_prefetch( string $key, array $cache_contents ): void { + $prefetch = WC_Stripe_Database_Cache_Prefetch::get_instance(); + if ( ! $prefetch->should_prefetch_cache_key( $key ) ) { + return; + } + + $expires = self::get_expiry_time( $cache_contents ); + if ( null === $expires ) { + return; + } + + $prefetch->maybe_queue_prefetch( $key, $expires ); + } + /** * Adds the CACHE_KEY_PREFIX + plugin mode prefix to the key. * Ex: "wcstripe_cache_[mode]_[key]. diff --git a/includes/class-wc-stripe-payment-method-configurations.php b/includes/class-wc-stripe-payment-method-configurations.php index b92f52cace..7adf26568f 100644 --- a/includes/class-wc-stripe-payment-method-configurations.php +++ b/includes/class-wc-stripe-payment-method-configurations.php @@ -35,7 +35,7 @@ class WC_Stripe_Payment_Method_Configurations { * * @var string */ - const CONFIGURATION_CACHE_KEY = 'payment_method_configuration'; + public const CONFIGURATION_CACHE_KEY = 'payment_method_configuration'; /** * The payment method configuration cache expiration (TTL). diff --git a/includes/class-wc-stripe.php b/includes/class-wc-stripe.php index b143605e9f..ed7c8fe904 100644 --- a/includes/class-wc-stripe.php +++ b/includes/class-wc-stripe.php @@ -127,6 +127,7 @@ public function init() { require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-helper.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-database-cache.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-payment-method-configurations.php'; + require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-database-cache-prefetch.php'; include_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-api.php'; include_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-mode.php'; require_once WC_STRIPE_PLUGIN_PATH . '/includes/compat/class-wc-stripe-subscriptions-helper.php'; @@ -290,6 +291,9 @@ public function init() { add_action( WC_Stripe_Database_Cache::ASYNC_CLEANUP_ACTION, [ WC_Stripe_Database_Cache::class, 'delete_all_stale_entries_async' ], 10, 2 ); add_action( 'action_scheduler_run_recurring_actions_schedule_hook', [ WC_Stripe_Database_Cache::class, 'maybe_schedule_daily_async_cleanup' ], 10, 0 ); + + // Handle the async cache prefetch action. + add_action( WC_Stripe_Database_Cache_Prefetch::ASYNC_PREFETCH_ACTION, [ WC_Stripe_Database_Cache_Prefetch::get_instance(), 'handle_prefetch_action' ], 10, 1 ); } /** diff --git a/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php new file mode 100644 index 0000000000..808da39224 --- /dev/null +++ b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php @@ -0,0 +1,130 @@ + [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true ], + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', false ], + ]; + } + + /** + * Test {@see \WC_Stripe_Database_Cache_Prefetch::handle_prefetch_action()}. + * + * @param string $key The key to prefetch. + * @param bool $should_prefetch Whether we expect the key to be prefetched. + * + * @dataProvider provide_handle_prefetch_action_test_cases + */ + public function test_handle_prefetch_action( string $key, bool $should_prefetch ): void { + $mock_instance = $this->getMockBuilder( 'WC_Stripe_Database_Cache_Prefetch' ) + ->disableOriginalConstructor() + ->onlyMethods( [ 'prefetch_cache_key' ] ) + ->getMock(); + + $expected_prefetch_count = $should_prefetch ? $this->once() : $this->never(); + + $mock_instance->expects( $expected_prefetch_count ) + ->method( 'prefetch_cache_key' ) + ->with( $key ) + ->willReturn( true ); + + $mock_instance->handle_prefetch_action( $key ); + } + + /** + * Provide test cases for {@see test_maybe_queue_prefetch()}. + * + * @return array + */ + public function provide_maybe_queue_prefetch_test_cases(): array { + return [ + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', 5, false ], + 'pmc_key_expires_in_60_seconds_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 60, false ], + 'pmc_key_expires_in_5_seconds_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true ], + 'pmc_key_expires_in_5_seconds_with_option_set_2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, 2 ], + 'pmc_key_expires_in_5_seconds_with_option_set_-2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -2 ], + 'pmc_key_expires_in_5_seconds_with_option_set_-6s_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, -6 ], + 'pmc_key_expires_in_5_seconds_with_invalid_option_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, 'invalid' ], + ]; + } + + /** + * Test {@see \WC_Stripe_Database_Cache_Prefetch::maybe_queue_prefetch()}. + * + * @param string $key The key to prefetch. + * @param int $expiry_time_adjustment The adjustment in seconds to the expiry time of the cache entry. Computed relative to the current time when the test is run. + * @param bool $should_enqueue_action Whether we expect the action to be enqueued. + * @param mixed $option_adjusted_time The time to set the option to. Null is no value set; an integer will be treated as an adjustment from the runtime timestamp; any other value will be written to the option. + * + * @dataProvider provide_maybe_queue_prefetch_test_cases + */ + public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustment, bool $should_enqueue_action, $option_adjusted_time = null ): void { + $instance = \WC_Stripe_Database_Cache_Prefetch::get_instance(); + + $mock_class = $this->getMockBuilder( \stdClass::class ) + ->addMethods( [ 'test_stub_callback' ] ) + ->getMock(); + + $mock_class->expects( $should_enqueue_action ? $this->once() : $this->never() ) + ->method( 'test_stub_callback' ) + ->with( null, \WC_Stripe_Database_Cache_Prefetch::ASYNC_PREFETCH_ACTION, [ $key ], 'woocommerce-gateway-stripe' ) + ->willReturn( 1 ); + + add_filter( 'pre_as_enqueue_async_action', [ $mock_class, 'test_stub_callback' ], 10, 4 ); + + $test_args = [ $key, $expiry_time_adjustment, $should_enqueue_action, $option_adjusted_time ]; + + $option_name = 'wcstripe_prefetch_' . $key; + $initial_option_value = null; + + $start_time = time(); + $expiry_time = $start_time + $expiry_time_adjustment; + + if ( null == $option_adjusted_time ) { + delete_option( $option_name ); + } elseif ( is_int( $option_adjusted_time ) ) { + $initial_option_value = $start_time + $option_adjusted_time; + } else { + $initial_option_value = $option_adjusted_time; + } + + if ( null !== $initial_option_value ) { + update_option( $option_name, $initial_option_value ); + } + + $instance->maybe_queue_prefetch( $key, $expiry_time ); + $end_time = time(); + + remove_filter( 'pre_as_enqueue_async_action', [ $mock_class, 'test_stub_callback' ], 10 ); + + $option_value = get_option( $option_name, false ); + + delete_option( $option_name ); + + if ( $should_enqueue_action ) { + $this->assertIsInt( $option_value ); + $this->assertGreaterThanOrEqual( $start_time, $option_value ); + $this->assertLessThanOrEqual( $end_time, $option_value ); + } elseif ( null === $initial_option_value ) { + $this->assertFalse( $option_value ); + } else { + $this->assertEquals( $initial_option_value, $option_value ); + } + } +} From 99113ebc021cf7e414deae1581d577c36b8d7080 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Mon, 1 Sep 2025 16:55:43 +0200 Subject: [PATCH 2/6] Move option update --- includes/class-wc-stripe-database-cache-prefetch.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-wc-stripe-database-cache-prefetch.php b/includes/class-wc-stripe-database-cache-prefetch.php index 7f8b4ee80f..46c62a81d2 100644 --- a/includes/class-wc-stripe-database-cache-prefetch.php +++ b/includes/class-wc-stripe-database-cache-prefetch.php @@ -97,12 +97,12 @@ public function maybe_queue_prefetch( string $key, int $expiry_time ): void { } $prefetch_option_key = $this->get_prefetch_option_name( $key ); - update_option( $prefetch_option_key, time() ); $result = as_enqueue_async_action( self::ASYNC_PREFETCH_ACTION, [ $key ], 'woocommerce-gateway-stripe' ); if ( 0 === $result ) { WC_Stripe_Logger::warning( 'Failed to enqueue cache prefetch', $logging_context ); } else { + update_option( $prefetch_option_key, time() ); WC_Stripe_Logger::debug( 'Enqueued cache prefetch', $logging_context ); } } From 319c1d9bcff765cd7f6ac5b92892712f90bc4d6b Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Mon, 1 Sep 2025 16:58:10 +0200 Subject: [PATCH 3/6] Fix unit test --- tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php index 808da39224..9f0770bb6b 100644 --- a/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php +++ b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php @@ -59,7 +59,7 @@ public function provide_maybe_queue_prefetch_test_cases(): array { 'pmc_key_expires_in_5_seconds_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true ], 'pmc_key_expires_in_5_seconds_with_option_set_2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, 2 ], 'pmc_key_expires_in_5_seconds_with_option_set_-2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -2 ], - 'pmc_key_expires_in_5_seconds_with_option_set_-6s_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, -6 ], + 'pmc_key_expires_in_5_seconds_with_option_set_-11s_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, -11 ], 'pmc_key_expires_in_5_seconds_with_invalid_option_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, 'invalid' ], ]; } From 2145c04a73cfa30c9c5bc814e63a3b64c5048747 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Tue, 2 Sep 2025 14:14:33 +0200 Subject: [PATCH 4/6] Changelog --- changelog.txt | 1 + readme.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 9ad4c9c58c..ee43f822b4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ * Update - Add nightly task and WooCommerce tool to remove stale entries from our database cache * Dev - Make 'Add to cart' more robust in e2e tests * Dev - Ensure e2e tests enable or disable Optimized Checkout during setup +* Add - Implement cache prefetch for payment method configuration = 9.8.1 - 2025-08-15 = * Fix - Remove connection type requirement from PMC sync migration attempt diff --git a/readme.txt b/readme.txt index 779b6f0338..97d45ac276 100644 --- a/readme.txt +++ b/readme.txt @@ -141,5 +141,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Update - Add nightly task and WooCommerce tool to remove stale entries from our database cache * Dev - Make 'Add to cart' more robust in e2e tests * Dev - Ensure e2e tests enable or disable Optimized Checkout during setup +* Add - Implement cache prefetch for payment method configuration [See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt). From 6472b1991ba3f2f41b02e4cc49e13368798a4e3d Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Wed, 17 Sep 2025 22:38:17 +0200 Subject: [PATCH 5/6] Allow cache prefetch window to be adjusted via a filter --- changelog.txt | 1 + ...lass-wc-stripe-database-cache-prefetch.php | 62 ++++++++-- readme.txt | 1 + ...WC_Stripe_Database_Cache_Prefetch_Test.php | 116 ++++++++++++++++-- 4 files changed, 158 insertions(+), 22 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8547d810d7..f3d751a0d5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ * Dev - Consolidate component used for unavailable payment methods * Dev - Update webhook unit tests to be compatible with WooCommerce 10.2 * Add - Implement cache prefetch for payment method configuration +* Add - Allow cache prefetch window to be adjusted via the wc_stripe_database_cache_prefetch_window filter = 9.9.1 - 2025-09-16 = * Add - Allow Klarna to be used for recurring payments and subscriptions diff --git a/includes/class-wc-stripe-database-cache-prefetch.php b/includes/class-wc-stripe-database-cache-prefetch.php index 46c62a81d2..aed9322575 100644 --- a/includes/class-wc-stripe-database-cache-prefetch.php +++ b/includes/class-wc-stripe-database-cache-prefetch.php @@ -60,7 +60,7 @@ public static function get_instance(): WC_Stripe_Database_Cache_Prefetch { * @return bool True if the cache key can be prefetched, false otherwise. */ public function should_prefetch_cache_key( string $key ): bool { - return isset( self::PREFETCH_CONFIG[ $key ] ) && self::PREFETCH_CONFIG[ $key ] > 0; + return $this->get_prefetch_window( $key ) > 0; } /** @@ -70,12 +70,11 @@ public function should_prefetch_cache_key( string $key ): bool { * @param int $expiry_time The expiry time of the cache entry. */ public function maybe_queue_prefetch( string $key, int $expiry_time ): void { - if ( ! $this->should_prefetch_cache_key( $key ) ) { + $prefetch_window = $this->get_prefetch_window( $key ); + if ( 0 === $prefetch_window ) { return; } - $prefetch_window = self::PREFETCH_CONFIG[ $key ]; - // If now plus the prefetch window is before the expiry time, do not trigger a prefetch. if ( ( time() + $prefetch_window ) < $expiry_time ) { return; @@ -107,6 +106,39 @@ public function maybe_queue_prefetch( string $key, int $expiry_time ): void { } } + /** + * Get the prefetch window for a given cache key. + * + * @param string $key The unprefixed cache key to get the prefetch window for. + * @return int The prefetch window for the cache key. 0 indicates that prefetching is disabled for the key. + */ + private function get_prefetch_window( string $cache_key ): int { + if ( ! isset( self::PREFETCH_CONFIG[ $cache_key ] ) ) { + return 0; + } + + $initial_prefetch_window = self::PREFETCH_CONFIG[ $cache_key ]; + + /** + * Filters the cache prefetch window for a given cache key. Return 0 or less to disable prefetching for the key. + * + * @param int $prefetch_window The prefetch window for the cache key. + * @param string $cache_key The unprefixed cache key. + */ + $prefetch_window = apply_filters( 'wc_stripe_database_cache_prefetch_window', $initial_prefetch_window, $cache_key ); + + // If the filter returns a non-integer, use the initial prefetch window. + if ( ! is_int( $prefetch_window ) ) { + return $initial_prefetch_window; + } + + if ( $prefetch_window <= 0 ) { + return 0; + } + + return $prefetch_window; + } + /** * Check if a prefetch is already queued up. * @@ -114,7 +146,8 @@ public function maybe_queue_prefetch( string $key, int $expiry_time ): void { * @return bool True if a prefetch is queued up, false otherwise. */ private function is_prefetch_queued( string $key ): bool { - if ( ! isset( self::PREFETCH_CONFIG[ $key ] ) ) { + $prefetch_window = $this->get_prefetch_window( $key ); + if ( 0 === $prefetch_window ) { return false; } @@ -126,8 +159,7 @@ private function is_prefetch_queued( string $key ): bool { return false; } - $now = time(); - $prefetch_window = self::PREFETCH_CONFIG[ $key ]; + $now = time(); if ( $prefetch_option >= ( $now - $prefetch_window ) ) { // If the prefetch entry expires in the future, or falls within the prefetch window for the key, we should consider the item live and queued. @@ -161,13 +193,13 @@ public function handle_prefetch_action( $key ): void { 'Invalid cache prefetch key', [ 'cache_key' => $key, - 'reason' => 'invalid_key', + 'reason' => 'invalid_cache_key', ] ); return; } - if ( ! $this->should_prefetch_cache_key( $key ) ) { + if ( ! isset( self::PREFETCH_CONFIG[ $key ] ) ) { WC_Stripe_Logger::warning( 'Invalid cache prefetch key', [ @@ -178,6 +210,18 @@ public function handle_prefetch_action( $key ): void { return; } + $prefetch_window = $this->get_prefetch_window( $key ); + if ( 0 === $prefetch_window ) { + WC_Stripe_Logger::warning( + 'Cache prefetch key was disabled', + [ + 'cache_key' => $key, + 'reason' => 'cache_key_disabled', + ] + ); + return; + } + $this->prefetch_cache_key( $key ); // Regardless of whether the prefetch was successful or not, we should remove the prefetch tracking option. diff --git a/readme.txt b/readme.txt index 31241dd953..1e8245340a 100644 --- a/readme.txt +++ b/readme.txt @@ -127,5 +127,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o * Dev - Consolidate component used for unavailable payment methods * Dev - Update webhook unit tests to be compatible with WooCommerce 10.2 * Add - Implement cache prefetch for payment method configuration +* Add - Allow cache prefetch window to be adjusted via the wc_stripe_database_cache_prefetch_window filter [See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt). diff --git a/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php index 9f0770bb6b..d23207d934 100644 --- a/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php +++ b/tests/phpunit/WC_Stripe_Database_Cache_Prefetch_Test.php @@ -18,25 +18,45 @@ class WC_Stripe_Database_Cache_Prefetch_Test extends \WP_UnitTestCase { */ public function provide_handle_prefetch_action_test_cases(): array { return [ - 'pmc_key_exists_and_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true ], - 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', false ], + 'pmc_key_exists_and_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, null, true ], + 'pmc_key_exists_and_should_prefetch_with_20_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 20, true ], + 'pmc_key_exists_and_should_not_prefetch_with_0_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 0, false ], + 'pmc_key_exists_and_should_not_prefetch_with_negative_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, -3, false ], + 'pmc_key_exists_and_should_prefetch_with_invalid_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 'invalid', true ], + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', null, false ], + 'invalid_key_should_not_prefetch_with_filter' => [ 'invalid_test_key', 10, false ], + 'invalid_key_should_not_prefetch_with_0_filter' => [ 'invalid_test_key', 0, false ], + 'invalid_key_should_not_prefetch_with_negative_filter' => [ 'invalid_test_key', -3, false ], + 'invalid_key_should_not_prefetch_with_invalid_filter' => [ 'invalid_test_key', 'invalid', false ], ]; } /** * Test {@see \WC_Stripe_Database_Cache_Prefetch::handle_prefetch_action()}. * - * @param string $key The key to prefetch. - * @param bool $should_prefetch Whether we expect the key to be prefetched. + * @param string $key The key to prefetch. + * @param mixed $prefetch_window_filter_value The value to filter the prefetch window with. Null is no filter value set; other values will be returned as-is. + * @param bool $should_prefetch Whether we expect the key to be prefetched. * * @dataProvider provide_handle_prefetch_action_test_cases */ - public function test_handle_prefetch_action( string $key, bool $should_prefetch ): void { + public function test_handle_prefetch_action( string $key, $prefetch_window_filter_value, bool $should_prefetch ): void { $mock_instance = $this->getMockBuilder( 'WC_Stripe_Database_Cache_Prefetch' ) ->disableOriginalConstructor() ->onlyMethods( [ 'prefetch_cache_key' ] ) ->getMock(); + $filter_callback = null; + if ( null !== $prefetch_window_filter_value ) { + $filter_callback = function ( $prefetch_window, $cache_key ) use ( $key, $prefetch_window_filter_value ) { + if ( $cache_key === $key ) { + return $prefetch_window_filter_value; + } + return $prefetch_window; + }; + add_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10, 2 ); + } + $expected_prefetch_count = $should_prefetch ? $this->once() : $this->never(); $mock_instance->expects( $expected_prefetch_count ) @@ -45,6 +65,10 @@ public function test_handle_prefetch_action( string $key, bool $should_prefetch ->willReturn( true ); $mock_instance->handle_prefetch_action( $key ); + + if ( null !== $filter_callback ) { + remove_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10 ); + } } /** @@ -54,13 +78,18 @@ public function test_handle_prefetch_action( string $key, bool $should_prefetch */ public function provide_maybe_queue_prefetch_test_cases(): array { return [ - 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', 5, false ], - 'pmc_key_expires_in_60_seconds_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 60, false ], - 'pmc_key_expires_in_5_seconds_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true ], - 'pmc_key_expires_in_5_seconds_with_option_set_2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, 2 ], - 'pmc_key_expires_in_5_seconds_with_option_set_-2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -2 ], - 'pmc_key_expires_in_5_seconds_with_option_set_-11s_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, -11 ], - 'pmc_key_expires_in_5_seconds_with_invalid_option_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, 'invalid' ], + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', 5, false ], + 'pmc_key_expires_in_60_seconds_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 60, false ], + 'pmc_key_expires_in_5_seconds_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true ], + 'pmc_key_expires_in_5_seconds_with_option_set_2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, 2 ], + 'pmc_key_expires_in_5_seconds_with_option_set_-2s_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -2 ], + 'pmc_key_expires_in_5_seconds_with_option_set_-11s_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, -11 ], + 'pmc_key_expires_in_5_seconds_with_invalid_option_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, 'invalid' ], + 'pmc_key_expires_in_5_seconds_with_20_filter_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, null, 20 ], + 'pmc_key_expires_in_5_seconds_with_20_filter_option_-11_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, -11, 20 ], + 'pmc_key_expires_in_5_seconds_with_0_filter_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, null, 0 ], + 'pmc_key_expires_in_5_seconds_with_negative_filter_should_not_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, false, null, -3 ], + 'pmc_key_expires_in_5_seconds_with_invalid_filter_should_prefetch' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, 5, true, null, 'invalid' ], ]; } @@ -74,7 +103,7 @@ public function provide_maybe_queue_prefetch_test_cases(): array { * * @dataProvider provide_maybe_queue_prefetch_test_cases */ - public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustment, bool $should_enqueue_action, $option_adjusted_time = null ): void { + public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustment, bool $should_enqueue_action, $option_adjusted_time = null, $prefetch_window_filter_value = null ): void { $instance = \WC_Stripe_Database_Cache_Prefetch::get_instance(); $mock_class = $this->getMockBuilder( \stdClass::class ) @@ -88,6 +117,17 @@ public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustm add_filter( 'pre_as_enqueue_async_action', [ $mock_class, 'test_stub_callback' ], 10, 4 ); + $filter_callback = null; + if ( null !== $prefetch_window_filter_value ) { + $filter_callback = function ( $prefetch_window, $cache_key ) use ( $key, $prefetch_window_filter_value ) { + if ( $cache_key === $key ) { + return $prefetch_window_filter_value; + } + return $prefetch_window; + }; + add_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10, 2 ); + } + $test_args = [ $key, $expiry_time_adjustment, $should_enqueue_action, $option_adjusted_time ]; $option_name = 'wcstripe_prefetch_' . $key; @@ -112,6 +152,9 @@ public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustm $end_time = time(); remove_filter( 'pre_as_enqueue_async_action', [ $mock_class, 'test_stub_callback' ], 10 ); + if ( null !== $filter_callback ) { + remove_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10 ); + } $option_value = get_option( $option_name, false ); @@ -127,4 +170,51 @@ public function test_maybe_queue_prefetch( string $key, int $expiry_time_adjustm $this->assertEquals( $initial_option_value, $option_value ); } } + + /** + * Provide test cases for {@see test_should_prefetch_cache_key()}. + * + * @return array + */ + public function provide_test_should_prefetch_cache_key_test_cases(): array { + return [ + 'invalid_key_should_not_prefetch' => [ 'invalid_test_key', false, null ], + 'pmc_key_should_prefetch_with_no_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true, null ], + 'pmc_key_should_not_prefetch_with_0_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, false, 0 ], + 'pmc_key_should_prefetch_with_20_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true, 20 ], + 'pmc_key_should_not_prefetch_with_negative_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, false, -5 ], + 'pmc_key_should_prefetch_with_invalid_filter' => [ \WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY, true, 'invalid' ], + ]; + } + + /** + * Test {@see \WC_Stripe_Database_Cache_Prefetch::should_prefetch_cache_key()}. + * + * @param string $cache_key The key to prefetch. + * @param bool $expected_result The expected result. + * @param mixed $filter_return_value The value to return from the prefetch window filter. Null is no filter value returned; other values will be returned as-is. + * + * @dataProvider provide_test_should_prefetch_cache_key_test_cases + */ + public function test_should_prefetch_cache_key( string $cache_key, bool $expected_result, $filter_return_value = null ): void { + $instance = \WC_Stripe_Database_Cache_Prefetch::get_instance(); + + $filter_callback = null; + if ( null !== $filter_return_value ) { + $filter_callback = function ( $prefetch_window, $key ) use ( $cache_key, $filter_return_value ) { + if ( $cache_key === $key ) { + return $filter_return_value; + } + return $prefetch_window; + }; + add_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10, 2 ); + } + + $result = $instance->should_prefetch_cache_key( $cache_key ); + if ( null !== $filter_callback ) { + remove_filter( 'wc_stripe_database_cache_prefetch_window', $filter_callback, 10 ); + } + + $this->assertEquals( $expected_result, $result ); + } } From d5035b8d10774e26da01ddd72fee8d66abf71a40 Mon Sep 17 00:00:00 2001 From: Dale du Preez Date: Wed, 15 Oct 2025 15:06:53 +0200 Subject: [PATCH 6/6] Tweak condition to check directly for key support --- includes/class-wc-stripe-database-cache-prefetch.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/class-wc-stripe-database-cache-prefetch.php b/includes/class-wc-stripe-database-cache-prefetch.php index 796759fdd6..a9dc7ac4f1 100644 --- a/includes/class-wc-stripe-database-cache-prefetch.php +++ b/includes/class-wc-stripe-database-cache-prefetch.php @@ -199,7 +199,8 @@ public function handle_prefetch_action( $key ): void { return; } - if ( ! $this->should_prefetch_cache_key( $key ) ) { + // We don't use should_prefetch_cache_key(), as that calls get_prefetch_window(), which we're checking below. + if ( ! isset( self::PREFETCH_CONFIG[ $key ] ) ) { WC_Stripe_Logger::warning( 'Invalid cache prefetch key', [