|
| 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 | +} |
0 commit comments