Skip to content

Commit bc6035f

Browse files
diegocurbeloannemirasoldaledupreez
committed
Implement custom database cache (#4331)
* Add database cache class * Replace PMC cache transients with custom database cache Co-authored-by: Anne Mirasol <[email protected]> Co-authored-by: Dale du Preez <[email protected]>
1 parent 1dff874 commit bc6035f

6 files changed

+401
-22
lines changed

changelog.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
*** Changelog ***
22

3+
= 9.5.2 - xxxx-xx-xx =
4+
* Add - Implement custom database cache for persistent caching with in-memory optimization.
5+
36
= 9.5.1 - 2025-05-17 =
47
* Fix - Add a fetch cooldown to the payment method configuration retrieval endpoint to prevent excessive requests.
58
* Fix - Prevent further Stripe API calls if API keys are invalid (401 response).
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
<?php
2+
3+
defined( 'ABSPATH' ) || exit; // block direct access.
4+
5+
/**
6+
* Class WC_Stripe_Database_Cache
7+
*/
8+
9+
/**
10+
* A class for caching data as an option in the database.
11+
*
12+
* Based on the WooCommerce Payments Database_Cache class implementation.
13+
*
14+
* @see https://github.com/Automattic/woocommerce-payments/blob/4b084af108cac9c6bd2467e52e5cdc3bc974a951/includes/class-database-cache.php
15+
*/
16+
class WC_Stripe_Database_Cache {
17+
18+
/**
19+
* In-memory cache for the duration of a single request.
20+
*
21+
* This is used to avoid multiple database reads for the same data and as a backstop in case the database write fails.
22+
*
23+
* @var array
24+
*/
25+
private static $in_memory_cache = [];
26+
27+
/**
28+
* Class constructor.
29+
*/
30+
private function __construct() {
31+
}
32+
33+
/**
34+
* Stores a value in the cache.
35+
*
36+
* @param string $key The key to store the value under.
37+
* @param mixed $data The value to store.
38+
* @param int $ttl The TTL of the cache. Dafault 1 hour.
39+
*
40+
* @return void
41+
*/
42+
public static function set( $key, $data, $ttl = HOUR_IN_SECONDS ) {
43+
self::write_to_cache( $key, $data, $ttl );
44+
}
45+
46+
/**
47+
* Gets a value from the cache.
48+
*
49+
* @param string $key The key to look for.
50+
*
51+
* @return mixed|null The cache contents. NULL if the cache value is expired or missing.
52+
*/
53+
public static function get( $key ) {
54+
$cache_contents = self::get_from_cache( $key );
55+
if ( is_array( $cache_contents ) && array_key_exists( 'data', $cache_contents ) ) {
56+
if ( self::is_expired( $key, $cache_contents ) ) {
57+
return null;
58+
}
59+
60+
return $cache_contents['data'];
61+
}
62+
63+
return null;
64+
}
65+
66+
/**
67+
* Deletes a value from the cache.
68+
*
69+
* @param string $key The key to delete.
70+
*
71+
* @return void
72+
*/
73+
public static function delete( $key ) {
74+
// Remove from the in-memory cache.
75+
unset( self::$in_memory_cache[ $key ] );
76+
77+
// Remove from the DB cache.
78+
if ( delete_option( $key ) ) {
79+
// Clear the WP object cache to ensure the new data is fetched by other processes.
80+
wp_cache_delete( $key, 'options' );
81+
}
82+
}
83+
84+
/**
85+
* Wraps the data in the cache metadata and stores it.
86+
*
87+
* @param string $key The key to store the data under.
88+
* @param mixed $data The data to store.
89+
* @param int $ttl The TTL of the cache.
90+
*
91+
* @return void
92+
*/
93+
private static function write_to_cache( $key, $data, $ttl ) {
94+
// Add the data and expiry time to the array we're caching.
95+
$cache_contents = [
96+
'data' => $data,
97+
'ttl' => $ttl,
98+
'updated' => time(),
99+
];
100+
101+
// Write the in-memory cache.
102+
self::$in_memory_cache[ $key ] = $cache_contents;
103+
104+
// Create or update the DB option cache.
105+
// Note: Since we are adding the current time to the option value, WP will ALWAYS write the option because
106+
// the cache contents value is different from the current one, even if the data is the same.
107+
// A `false` result ONLY means that the DB write failed.
108+
// Yes, there is the possibility that we attempt to write the same data multiple times within the SAME second,
109+
// and we will mistakenly think that the DB write failed. We are OK with this false positive,
110+
// since the actual data is the same.
111+
//
112+
// Note 2: Autoloading too many options can lead to performance problems, and we are implementing this as a
113+
// general cache for the plugin, so we set the autoload to false.
114+
$result = update_option( $key, $cache_contents, false );
115+
if ( false !== $result ) {
116+
// If the DB cache write succeeded, clear the WP object cache to ensure the new data is fetched by other processes.
117+
wp_cache_delete( $key, 'options' );
118+
}
119+
}
120+
121+
/**
122+
* Get the cache contents for a certain key.
123+
*
124+
* @param string $key The cache key.
125+
*
126+
* @return array|false The cache contents (array with `data`, `ttl`, and `updated` entries).
127+
* False if there is no cached data.
128+
*/
129+
private static function get_from_cache( $key ) {
130+
// Check the in-memory cache first.
131+
if ( isset( self::$in_memory_cache[ $key ] ) ) {
132+
return self::$in_memory_cache[ $key ];
133+
}
134+
135+
// Read from the DB cache.
136+
$data = get_option( $key );
137+
138+
// Store the data in the in-memory cache, including the case when there is no data cached (`false`).
139+
self::$in_memory_cache[ $key ] = $data;
140+
141+
return $data;
142+
}
143+
144+
/**
145+
* Checks if the cache value is expired.
146+
*
147+
* @param string $key The cache key.
148+
* @param array $cache_contents The cache contents.
149+
*
150+
* @return boolean True if the contents are expired. False otherwise.
151+
*/
152+
private static function is_expired( $key, $cache_contents ) {
153+
if ( ! is_array( $cache_contents ) || ! isset( $cache_contents['updated'] ) || ! isset( $cache_contents['ttl'] ) ) {
154+
// Treat bad/invalid cache contents as expired
155+
return true;
156+
}
157+
158+
// Double-check that we have integers for `updated` and `ttl`.
159+
if ( ! is_int( $cache_contents['updated'] ) || ! is_int( $cache_contents['ttl'] ) ) {
160+
return true;
161+
}
162+
163+
$expires = $cache_contents['updated'] + $cache_contents['ttl'];
164+
$now = time();
165+
166+
return apply_filters( 'wcstripe_database_cache_is_expired', $expires < $now, $key, $cache_contents );
167+
}
168+
169+
/**
170+
* Get all cached keys in memory.
171+
*
172+
* @return array The cached keys.
173+
*/
174+
public static function get_cached_keys() {
175+
return array_keys( self::$in_memory_cache );
176+
}
177+
}

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

Lines changed: 17 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,25 +31,25 @@ class WC_Stripe_Payment_Method_Configurations {
3131
const LIVE_MODE_CONFIGURATION_PARENT_ID = 'pmc_1LEKjAGX8lmJQndTk2ziRchV';
3232

3333
/**
34-
* The test mode payment method configuration transient key (for cache purposes).
34+
* The test mode payment method configuration cache key.
3535
*
3636
* @var string
3737
*/
38-
const TEST_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY = 'wcstripe_test_payment_method_configuration_cache';
38+
const TEST_MODE_CONFIGURATION_CACHE_KEY = 'wcstripe_test_payment_method_configuration_cache';
3939

4040
/**
41-
* The live mode payment method configuration transient key (for cache purposes).
41+
* The live mode payment method configuration cache key.
4242
*
4343
* @var string
4444
*/
45-
const LIVE_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY = 'wcstripe_live_payment_method_configuration_cache';
45+
const LIVE_MODE_CONFIGURATION_CACHE_KEY = 'wcstripe_live_payment_method_configuration_cache';
4646

4747
/**
48-
* The payment method configuration transient expiration (for cache purposes).
48+
* The payment method configuration cache expiration.
4949
*
5050
* @var int
5151
*/
52-
const CONFIGURATION_CACHE_TRANSIENT_EXPIRATION = 10 * MINUTE_IN_SECONDS;
52+
const CONFIGURATION_CACHE_EXPIRATION = 10 * MINUTE_IN_SECONDS;
5353

5454
/**
5555
* The payment method configuration fetch cooldown option key.
@@ -81,7 +81,7 @@ private static function get_primary_configuration( $force_refresh = false ) {
8181
}
8282

8383
// Intentionally fall through to fetching the data from Stripe if we don't have it locally,
84-
// even when $force_refresh = false and/or $is_in_cooldown is true.
84+
// even when $force_refresh == false and/or $is_in_cooldown is true.
8585
// We _need_ the payment method configuration for things to work as expected,
8686
// so we will fetch it if we don't have anything locally.
8787
}
@@ -103,11 +103,11 @@ private static function get_payment_method_configuration_from_cache( $use_fallba
103103
return self::$primary_configuration;
104104
}
105105

106-
$cache_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY : self::LIVE_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY;
107-
$cached_primary_configuration = get_transient( $cache_key );
106+
$cache_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_CONFIGURATION_CACHE_KEY : self::LIVE_MODE_CONFIGURATION_CACHE_KEY;
107+
$cached_primary_configuration = WC_Stripe_Database_Cache::get( $cache_key );
108108
if ( false === $cached_primary_configuration || null === $cached_primary_configuration ) {
109109
if ( $use_fallback ) {
110-
return get_option( $cache_key );
110+
return get_option( $cache_key . '_fallback' );
111111
}
112112
return null;
113113
}
@@ -121,9 +121,9 @@ private static function get_payment_method_configuration_from_cache( $use_fallba
121121
*/
122122
public static function clear_payment_method_configuration_cache() {
123123
self::$primary_configuration = null;
124-
$cache_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY : self::LIVE_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY;
125-
delete_transient( $cache_key );
126-
delete_option( $cache_key );
124+
$cache_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_CONFIGURATION_CACHE_KEY : self::LIVE_MODE_CONFIGURATION_CACHE_KEY;
125+
WC_Stripe_Database_Cache::delete( $cache_key );
126+
delete_option( $cache_key . '_fallback' );
127127
}
128128

129129
/**
@@ -133,11 +133,11 @@ public static function clear_payment_method_configuration_cache() {
133133
*/
134134
private static function set_payment_method_configuration_cache( $configuration ) {
135135
self::$primary_configuration = $configuration;
136-
$cache_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY : self::LIVE_MODE_CONFIGURATION_CACHE_TRANSIENT_KEY;
137-
set_transient( $cache_key, $configuration, self::CONFIGURATION_CACHE_TRANSIENT_EXPIRATION );
136+
$cache_key = WC_Stripe_Mode::is_test() ? self::TEST_MODE_CONFIGURATION_CACHE_KEY : self::LIVE_MODE_CONFIGURATION_CACHE_KEY;
137+
WC_Stripe_Database_Cache::set( $cache_key, $configuration, self::CONFIGURATION_CACHE_EXPIRATION );
138138

139-
// To be used as fallback if we are in API cooldown and the transient is not available.
140-
update_option( $cache_key, $configuration );
139+
// To be used as fallback if we are in API cooldown and the main cache is not available.
140+
update_option( $cache_key . '_fallback', $configuration );
141141
}
142142

143143
/**

readme.txt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,7 @@ If you get stuck, you can ask for help in the [Plugin Forum](https://wordpress.o
110110

111111
== Changelog ==
112112

113-
= 9.5.1 - 2025-05-17 =
114-
* Fix - Add a fetch cooldown to the payment method configuration retrieval endpoint to prevent excessive requests.
115-
* Fix - Prevent further Stripe API calls if API keys are invalid (401 response).
116-
* Fix - Stop checking for detached subscriptions for admin users, as it was slowing down wp-admin.
117-
* Fix - Fix fatal error when checking for a payment method availability using a specific order ID.
113+
= 9.5.2 - xxxx-xx-xx =
114+
* Add - Implement custom database cache for persistent caching with in-memory optimization.
118115

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

0 commit comments

Comments
 (0)