Skip to content

Commit 3af29ae

Browse files
authored
Implement cache prefetch for payment method configuration (#4637)
* Initial cache prefetch implementation * Move option update * Fix unit test * Changelog
1 parent 21bacf2 commit 3af29ae

7 files changed

+406
-6
lines changed

changelog.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
* Dev - Upgrades the Babel-related packages
1717
* Dev - Consolidate component used for unavailable payment methods
1818
* Dev - Update webhook unit tests to be compatible with WooCommerce 10.2
19+
* Add - Implement cache prefetch for payment method configuration
1920

2021
= 9.9.1 - 2025-09-16 =
2122
* Add - Allow Klarna to be used for recurring payments and subscriptions
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
<?php
2+
3+
defined( 'ABSPATH' ) || exit; // block direct access.
4+
5+
/**
6+
* Class WC_Stripe_Database_Cache_Prefetch
7+
*
8+
* This class is responsible for prefetching cache keys.
9+
*/
10+
class WC_Stripe_Database_Cache_Prefetch {
11+
/**
12+
* The action used for the asynchronous cache prefetch code.
13+
*
14+
* @var string
15+
*/
16+
public const ASYNC_PREFETCH_ACTION = 'wc_stripe_database_cache_prefetch_async';
17+
18+
/**
19+
* Configuration for cache prefetching.
20+
*
21+
* @var int[]
22+
*/
23+
protected const PREFETCH_CONFIG = [
24+
WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY => 10,
25+
];
26+
27+
/**
28+
* The prefix used for prefetch tracking options.
29+
*
30+
* @var string
31+
*/
32+
private const PREFETCH_OPTION_PREFIX = 'wcstripe_prefetch_';
33+
34+
/**
35+
* The singleton instance.
36+
*/
37+
private static ?WC_Stripe_Database_Cache_Prefetch $instance = null;
38+
39+
/**
40+
* Protected constructor to support singleton pattern.
41+
*/
42+
protected function __construct() {}
43+
44+
/**
45+
* Get the singleton instance.
46+
*
47+
* @return WC_Stripe_Database_Cache_Prefetch The singleton instance.
48+
*/
49+
public static function get_instance(): WC_Stripe_Database_Cache_Prefetch {
50+
if ( null === self::$instance ) {
51+
self::$instance = new self();
52+
}
53+
return self::$instance;
54+
}
55+
56+
/**
57+
* Check if the unprefixed cache key has prefetch enabled.
58+
*
59+
* @param string $key The unprefixed cache key to check.
60+
* @return bool True if the cache key can be prefetched, false otherwise.
61+
*/
62+
public function should_prefetch_cache_key( string $key ): bool {
63+
return isset( self::PREFETCH_CONFIG[ $key ] ) && self::PREFETCH_CONFIG[ $key ] > 0;
64+
}
65+
66+
/**
67+
* Maybe queue a prefetch for a cache key.
68+
*
69+
* @param string $key The unprefixed cache key to prefetch.
70+
* @param int $expiry_time The expiry time of the cache entry.
71+
*/
72+
public function maybe_queue_prefetch( string $key, int $expiry_time ): void {
73+
if ( ! $this->should_prefetch_cache_key( $key ) ) {
74+
return;
75+
}
76+
77+
$prefetch_window = self::PREFETCH_CONFIG[ $key ];
78+
79+
// If now plus the prefetch window is before the expiry time, do not trigger a prefetch.
80+
if ( ( time() + $prefetch_window ) < $expiry_time ) {
81+
return;
82+
}
83+
84+
$logging_context = [
85+
'cache_key' => $key,
86+
'expiry_time' => $expiry_time,
87+
];
88+
89+
if ( $this->is_prefetch_queued( $key ) ) {
90+
WC_Stripe_Logger::debug( 'Cache prefetch already pending', $logging_context );
91+
return;
92+
}
93+
94+
if ( ! did_action( 'action_scheduler_init' ) || ! function_exists( 'as_enqueue_async_action' ) ) {
95+
WC_Stripe_Logger::debug( 'Unable to enqueue cache prefetch: Action Scheduler is not initialized or available', $logging_context );
96+
return;
97+
}
98+
99+
$prefetch_option_key = $this->get_prefetch_option_name( $key );
100+
101+
$result = as_enqueue_async_action( self::ASYNC_PREFETCH_ACTION, [ $key ], 'woocommerce-gateway-stripe' );
102+
if ( 0 === $result ) {
103+
WC_Stripe_Logger::warning( 'Failed to enqueue cache prefetch', $logging_context );
104+
} else {
105+
update_option( $prefetch_option_key, time() );
106+
WC_Stripe_Logger::debug( 'Enqueued cache prefetch', $logging_context );
107+
}
108+
}
109+
110+
/**
111+
* Check if a prefetch is already queued up.
112+
*
113+
* @param string $key The unprefixed cache key to check.
114+
* @return bool True if a prefetch is queued up, false otherwise.
115+
*/
116+
private function is_prefetch_queued( string $key ): bool {
117+
if ( ! isset( self::PREFETCH_CONFIG[ $key ] ) ) {
118+
return false;
119+
}
120+
121+
$prefetch_option_key = $this->get_prefetch_option_name( $key );
122+
123+
$prefetch_option = get_option( $prefetch_option_key, false );
124+
// We use ctype_digit() and the (string) cast to ensure we handle the option value being returned as a string.
125+
if ( ! ctype_digit( (string) $prefetch_option ) ) {
126+
return false;
127+
}
128+
129+
$now = time();
130+
$prefetch_window = self::PREFETCH_CONFIG[ $key ];
131+
132+
if ( $prefetch_option >= ( $now - $prefetch_window ) ) {
133+
// 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.
134+
// 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.
135+
return true;
136+
}
137+
138+
return false;
139+
}
140+
141+
/**
142+
* Get the name of the prefetch tracking option for a given cache key.
143+
*
144+
* @param string $key The unprefixed cache key to get the option name for.
145+
* @return string The name of the prefetch option.
146+
*/
147+
private function get_prefetch_option_name( string $key ): string {
148+
return self::PREFETCH_OPTION_PREFIX . $key;
149+
}
150+
151+
/**
152+
* Handle the prefetch action. We are generally expecting this to be queued up by Action Scheduler using
153+
* the action from {@see ASYNC_PREFETCH_ACTION}.
154+
*
155+
* @param string $key The unprefixed cache key to prefetch.
156+
* @return void
157+
*/
158+
public function handle_prefetch_action( $key ): void {
159+
if ( ! is_string( $key ) || empty( $key ) ) {
160+
WC_Stripe_Logger::warning(
161+
'Invalid cache prefetch key',
162+
[
163+
'cache_key' => $key,
164+
'reason' => 'invalid_key',
165+
]
166+
);
167+
return;
168+
}
169+
170+
if ( ! $this->should_prefetch_cache_key( $key ) ) {
171+
WC_Stripe_Logger::warning(
172+
'Invalid cache prefetch key',
173+
[
174+
'cache_key' => $key,
175+
'reason' => 'unsupported_cache_key',
176+
]
177+
);
178+
return;
179+
}
180+
181+
$this->prefetch_cache_key( $key );
182+
183+
// Regardless of whether the prefetch was successful or not, we should remove the prefetch tracking option.
184+
delete_option( $this->get_prefetch_option_name( $key ) );
185+
}
186+
187+
/**
188+
* Helper method to implement prefetch/repopulation for supported cache entries.
189+
*
190+
* @param string $key The unprefixed cache key to prefetch.
191+
* @return bool|null True if the prefetch was successful, false if the prefetch failed, or null if the prefetch was not attempted.
192+
*/
193+
protected function prefetch_cache_key( string $key ): ?bool {
194+
$prefetched = null;
195+
196+
switch ( $key ) {
197+
case WC_Stripe_Payment_Method_Configurations::CONFIGURATION_CACHE_KEY:
198+
if ( WC_Stripe_Payment_Method_Configurations::is_enabled() ) {
199+
WC_Stripe_Payment_Method_Configurations::get_upe_enabled_payment_method_ids( true );
200+
$prefetched = true;
201+
} else {
202+
$prefetched = false;
203+
WC_Stripe_Logger::debug( 'Unable to prefetch PMC cache as settings sync is disabled', [ 'cache_key' => $key ] );
204+
}
205+
break;
206+
default:
207+
break;
208+
}
209+
210+
if ( true === $prefetched ) {
211+
WC_Stripe_Logger::debug( 'Successfully prefetched cache key', [ 'cache_key' => $key ] );
212+
} elseif ( null === $prefetched ) {
213+
WC_Stripe_Logger::warning( 'Prefetch cache key not handled', [ 'cache_key' => $key ] );
214+
} else {
215+
WC_Stripe_Logger::debug( 'Failed to prefetch cache key', [ 'cache_key' => $key ] );
216+
}
217+
218+
return $prefetched;
219+
}
220+
}

includes/class-wc-stripe-database-cache.php

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ public static function get( $key ) {
101101
return null;
102102
}
103103

104+
self::maybe_trigger_prefetch( $key, $cache_contents );
105+
104106
return $cache_contents['data'];
105107
}
106108

@@ -209,18 +211,17 @@ private static function get_from_cache( $prefixed_key ) {
209211
* @return boolean True if the contents are expired. False otherwise.
210212
*/
211213
private static function is_expired( $prefixed_key, $cache_contents ) {
212-
if ( ! is_array( $cache_contents ) || ! isset( $cache_contents['updated'] ) || ! isset( $cache_contents['ttl'] ) ) {
214+
if ( ! is_array( $cache_contents ) ) {
213215
// Treat bad/invalid cache contents as expired
214216
return true;
215217
}
216218

217-
// Double-check that we have integers for `updated` and `ttl`.
218-
if ( ! is_int( $cache_contents['updated'] ) || ! is_int( $cache_contents['ttl'] ) ) {
219+
$expires = self::get_expiry_time( $cache_contents );
220+
if ( null === $expires ) {
219221
return true;
220222
}
221223

222-
$expires = $cache_contents['updated'] + $cache_contents['ttl'];
223-
$now = time();
224+
$now = time();
224225

225226
/**
226227
* Filters the result of the database cache entry expiration check.
@@ -236,6 +237,49 @@ private static function is_expired( $prefixed_key, $cache_contents ) {
236237
return apply_filters( 'wc_stripe_database_cache_is_expired', $expires < $now, $prefixed_key, $cache_contents );
237238
}
238239

240+
/**
241+
* Get the expiry time for a cache entry. Includes validation for time-related fields in the array.
242+
*
243+
* @param array $cache_contents The cache contents.
244+
*
245+
* @return int|null The expiry time as a timestamp. Null if the expiry time can't be determined.
246+
*/
247+
private static function get_expiry_time( array $cache_contents ): ?int {
248+
// If we don't have updated and ttl keys, expiry time is unknown.
249+
if ( ! isset( $cache_contents['updated'], $cache_contents['ttl'] ) ) {
250+
return null;
251+
}
252+
253+
// If we don't have integers for updated and ttl, expiry time is unknown.
254+
if ( ! is_int( $cache_contents['updated'] ) || ! is_int( $cache_contents['ttl'] ) ) {
255+
return null;
256+
}
257+
258+
return $cache_contents['updated'] + $cache_contents['ttl'];
259+
}
260+
261+
/**
262+
* Maybe trigger a cache prefetch.
263+
*
264+
* @param string $key The unprefixed cache key.
265+
* @param array $cache_contents The cache contents.
266+
*
267+
* @return void
268+
*/
269+
private static function maybe_trigger_prefetch( string $key, array $cache_contents ): void {
270+
$prefetch = WC_Stripe_Database_Cache_Prefetch::get_instance();
271+
if ( ! $prefetch->should_prefetch_cache_key( $key ) ) {
272+
return;
273+
}
274+
275+
$expires = self::get_expiry_time( $cache_contents );
276+
if ( null === $expires ) {
277+
return;
278+
}
279+
280+
$prefetch->maybe_queue_prefetch( $key, $expires );
281+
}
282+
239283
/**
240284
* Adds the CACHE_KEY_PREFIX + plugin mode prefix to the key.
241285
* Ex: "wcstripe_cache_[mode]_[key].

includes/class-wc-stripe-payment-method-configurations.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ class WC_Stripe_Payment_Method_Configurations {
3535
*
3636
* @var string
3737
*/
38-
const CONFIGURATION_CACHE_KEY = 'payment_method_configuration';
38+
public const CONFIGURATION_CACHE_KEY = 'payment_method_configuration';
3939

4040
/**
4141
* The payment method configuration cache expiration (TTL).

includes/class-wc-stripe.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ public function init() {
127127
require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-helper.php';
128128
require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-database-cache.php';
129129
require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-payment-method-configurations.php';
130+
require_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-database-cache-prefetch.php';
130131
include_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-api.php';
131132
include_once WC_STRIPE_PLUGIN_PATH . '/includes/class-wc-stripe-mode.php';
132133
require_once WC_STRIPE_PLUGIN_PATH . '/includes/compat/class-wc-stripe-subscriptions-helper.php';
@@ -293,6 +294,9 @@ public function init() {
293294

294295
add_action( WC_Stripe_Database_Cache::ASYNC_CLEANUP_ACTION, [ WC_Stripe_Database_Cache::class, 'delete_all_stale_entries_async' ], 10, 2 );
295296
add_action( 'action_scheduler_run_recurring_actions_schedule_hook', [ WC_Stripe_Database_Cache::class, 'maybe_schedule_daily_async_cleanup' ], 10, 0 );
297+
298+
// Handle the async cache prefetch action.
299+
add_action( WC_Stripe_Database_Cache_Prefetch::ASYNC_PREFETCH_ACTION, [ WC_Stripe_Database_Cache_Prefetch::get_instance(), 'handle_prefetch_action' ], 10, 1 );
296300
}
297301

298302
/**

readme.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,5 +126,6 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
126126
* Dev - Upgrades the Babel-related packages
127127
* Dev - Consolidate component used for unavailable payment methods
128128
* Dev - Update webhook unit tests to be compatible with WooCommerce 10.2
129+
* Add - Implement cache prefetch for payment method configuration
129130

130131
[See changelog for full details across versions](https://raw.githubusercontent.com/woocommerce/woocommerce-gateway-stripe/trunk/changelog.txt).

0 commit comments

Comments
 (0)