diff --git a/inc/class-cachestorageprovider.php b/inc/class-cachestorageprovider.php new file mode 100644 index 0000000..526bebd --- /dev/null +++ b/inc/class-cachestorageprovider.php @@ -0,0 +1,109 @@ +redis_hash_groups[ $group ] ); + } + + return false; + } +} diff --git a/inc/class-storageprovider.php b/inc/class-storageprovider.php new file mode 100644 index 0000000..c9a090f --- /dev/null +++ b/inc/class-storageprovider.php @@ -0,0 +1,104 @@ +registered_groups[ $cache_group ] ) ) { + return false; + } + + $cache_group = $this->dashit( $cache_group ); + $affected = 0; + // Deleting all options and transients with shared group name + $affected += (int) $wpdb->query( + $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", "$cache_group%" ) + ); + $affected += (int) $wpdb->query( + $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", "_transient_$cache_group%" ) + ); + return $affected > 0; + } + + /** + * Registers a cache group for flushing. + * + * @param string $group The cache group to be registered. + * + * @return bool Whether the cache group was successfully registered. + */ + public function register_group( string $group ) : bool { + $this->registered_groups[ $group ] = $group; + return true; + } + + /** + * Retrieves a cached form with its expiry time. + * + * @param string $cache_key The key of the cache to retrieve. + * @param string $cache_group Optional. The cache group. Default is empty string. + * + * @return array An array containing the cached contents (or false) and its expiry timestamp. + */ + public function get_with_expiry( string $cache_key, string $cache_group = '' ) : array { + $data = get_option( $this->dashit( $cache_group ) . $cache_key ); + $expiry_timestamp = (int) get_option( $this->dashit( $cache_group ) . $cache_key . '_expiry' ); + + return [ $data, $expiry_timestamp ]; + } + + /** + * Add a dash to the end of a string if it is not empty. + * + * @param string $str The string to append the dash to. + * + * @return string The modified string. + */ + protected function dashit( $str ) : string { + return $str ? $str . '-' : $str; + } + + /** + * Set data in cache with expiry and delete the lock transient. + * + * @param string $lock_key The name of the lock. + * @param mixed $data The data to be cached. + * @param int $cache_duration The expiry time for the cache in seconds. + * @param string $cache_key The key for the cache. + * @param string $cache_group Optional. The group for the cache. Default value is empty string. + */ + public function set_with_expiry( string $lock_key, mixed $data, int $cache_duration, string $cache_key, string $cache_group = '' ) : void { + update_option( $this->dashit( $cache_group ) . $cache_key, $data, false ); // Don't autoload. + set_transient( $this->dashit( $cache_group ) . $cache_key . '_expiry', time() + $cache_duration, $cache_duration ); + delete_transient( $this->dashit( $cache_group ) . $lock_key ); + } + + /** + * Adds a lock key in the cache to avoid race conditions and enables synchronization between processes. + * It's only stored if the lock doesn't exist (or is expired). + * + * @param string $lock_key The unique key for the lock. + * @param string $cache_group The cache group where the lock key will be stored + * @param int $lock_time The duration in seconds for which the lock will be held. Default is MINUTE_IN_SECONDS constant. + * + * @return string The generated lock value, unique per invocation, regardless whether the lock is added. + */ + public function lock_add( string $lock_key, string $cache_group = '', int $lock_time = MINUTE_IN_SECONDS ) : string { + $lock_value = wp_generate_uuid4(); + // Add will fail if it already exists. + $this->add_transient( $lock_key, $lock_value, $cache_group, $lock_time ); + + return $lock_value; + } + + /** + * Verifies the lock against the cached lock. + * + * @param string $lock_key The transient key used to lock the group. + * @param string $lock_value The lock value to be verified. + * @param string $cache_group The cache group in which the lock value is stored. Default is an empty string. + * + * @return bool Whether the lock value matches the cached lock value in the cache group. + */ + function lock_verify( string $lock_key, string $lock_value, string $cache_group = '' ) : bool { + $cached_lock = get_transient( $this->dashit( $cache_group ) . $lock_key ); + $found = $cached_lock !== false; + + return $found && $cached_lock === $lock_value; + } + + /** + * Adds a transient value to the cache. + * + * @param string $key The key of the transient value. + * @param mixed $data The data to be stored in the transient value. + * @param string $group The cache group where the transient value will be stored. Default is an empty string. + * @param int $expire The duration in seconds for which the transient value will be stored. Default is 0, which means no expiration. + * + * @return bool True if the transient value was successfully added to the cache, false if the key and group already exists. + */ + protected function add_transient( string $key, mixed $data, string $group = '', int $expire = 0 ) : bool { + $group = $this->dashit( $group ); + if ( get_transient( $group . $key ) ) { + return false; + } + $transient = $group . $key; + + return set_transient( $transient, $data, $expire ); + } +} diff --git a/inc/namespace.php b/inc/namespace.php index 5f19235..9a606e8 100644 --- a/inc/namespace.php +++ b/inc/namespace.php @@ -23,14 +23,16 @@ use RuntimeException; const CRON_ACTION = 'hm.swrCache.cron'; - /** * Bootstrapping. * * @return void */ function bootstrap() : void { - add_action( CRON_ACTION, __NAMESPACE__ . '\\do_cron', 10, 6 ); + global $storage; + + add_action( CRON_ACTION, __NAMESPACE__ . '\\do_cron', 10, 7 ); + $storage = StorageProvider::get_instance( StorageProvider::CACHE ); } /** @@ -41,43 +43,11 @@ function bootstrap() : void { * @return bool Whether the cache group was successfully deleted. */ function cache_delete_group( string $cache_group ) : bool { - // Requires cache group registration. - // @see \HM\SwrCache\register_cache_group - if ( function_exists( 'wp_cache_supports' ) && wp_cache_supports( 'flush_group' ) ) { - return wp_cache_flush_group( $cache_group ); - } - return false; -} - -/** - * Retrieves a cached form with its expiry time. - * - * @param string $cache_key The key of the cache to retrieve. - * @param string $cache_group Optional. The cache group. Default is empty string. - * - * @return array An array containing the cached contents (or false) and its expiry timestamp. - */ -function cache_get_with_expiry( string $cache_key, string $cache_group = '' ) : array { - $data = wp_cache_get( $cache_key, $cache_group ); - $expiry_timestamp = (int) wp_cache_get( $cache_key . '_expiry', $cache_group ); + global $storage; - return [ $data, $expiry_timestamp ]; + return $storage->delete_group( $cache_group ); } -/** - * Set data in cache with expiry and delete the lock transient. - * - * @param string $lock_key The name of the lock. - * @param mixed $data The data to be cached. - * @param int $cache_duration The expiry time for the cache in seconds. - * @param string $cache_key The key for the cache. - * @param string $cache_group Optional. The group for the cache. Default value is empty string. - */ -function cache_set_with_expiry( string $lock_key, mixed $data, int $cache_duration, string $cache_key, string $cache_group = '' ) : void { - wp_cache_set( $cache_key, $data, $cache_group ); - wp_cache_set( $cache_key . '_expiry', time() + $cache_duration, $cache_group, $cache_duration ); - wp_cache_delete( $lock_key, $cache_group ); -} /** * Check if the cache is warm. @@ -106,22 +76,25 @@ function cache_is_warm( mixed $data, int $expiry_time ) : bool { * @throws InvalidArgumentException If a closure is provided as a callback. * @throws RuntimeException If an error occurs during the execution of the callback function. */ -function do_cron( string $lock_value, callable $callback, array $callback_args, int $expiry_duration, string $cache_key, string $cache_group = '' ) : void { - if ($callback instanceof Closure) { - throw new InvalidArgumentException("Closures are not allowed as callbacks."); - } +function do_cron( string $lock_value, callable $callback, array $callback_args, int $expiry_duration, string $cache_key, string $cache_group = '') : void { + global $storage; + + if ( $callback instanceof Closure ) { + throw new InvalidArgumentException( 'Closures are not allowed as callbacks.' ); + } + $lock_key = "lock_$cache_key"; - if ( ! lock_verify( $lock_key, $lock_value, $cache_group ) ) { + if ( ! $storage->lock_verify( $lock_key, $lock_value, $cache_group ) ) { // Another invocation already reserved this cron job. return; } - $data = $callback( $callback_args ); + $data = $callback( $callback_args ); if ( is_wp_error( $data ) ) { throw new RuntimeException( $data->get_error_message(), $data->get_error_code() ); } - cache_set_with_expiry( $lock_key, $data, $expiry_duration, $cache_key, $cache_group ); + $storage->set_with_expiry( $lock_key, $data, $expiry_duration, $cache_key, $cache_group ); } /** @@ -138,11 +111,13 @@ function do_cron( string $lock_value, callable $callback, array $callback_args, * @throws InvalidArgumentException If a closure is provided as a callback. */ function get( string $cache_key, string $cache_group, callable $callback, array $callback_args, int $cache_duration ) : mixed { - if ($callback instanceof Closure) { - throw new InvalidArgumentException("Closures are not allowed as callbacks."); - } + global $storage; + + if ( $callback instanceof Closure ) { + throw new InvalidArgumentException( 'Closures are not allowed as callbacks.' ); + } - [ $data, $expiry_timestamp ] = cache_get_with_expiry( $cache_key, $cache_group ); + [ $data, $expiry_timestamp ] = $storage->get_with_expiry( $cache_key, $cache_group ); if ( cache_is_warm( $data, $expiry_timestamp ) ) { // Cache is warm @@ -150,7 +125,7 @@ function get( string $cache_key, string $cache_group, callable $callback, array } wp_schedule_single_event( time(), CRON_ACTION, [ - lock_add( "lock_$cache_key", $cache_group ), + $storage->lock_add( "lock_$cache_key", $cache_group ), $callback, $callback_args, $cache_duration, @@ -162,40 +137,6 @@ function get( string $cache_key, string $cache_group, callable $callback, array return $data; } -/** - * Adds a lock key in the cache to avoid race conditions and enables synchronization between processes. - * It's only stored if the lock doesn't exist (or is expired). - * - * @param string $lock_key The unique key for the lock. - * @param string $cache_group The cache group where the lock key will be stored - * @param int $lock_time The duration in seconds for which the lock will be held. Default is MINUTE_IN_SECONDS constant. - * - * @return string The generated lock value, unique per invocation, regardless whether the lock is added. - */ -function lock_add( string $lock_key, string $cache_group = '', int $lock_time = MINUTE_IN_SECONDS ) : string { - $lock_value = wp_generate_uuid4(); - // Add will fail if it already exists. - wp_cache_add( $lock_key, $lock_value, $cache_group, $lock_time ); - - return $lock_value; -} - -/** - * Verifies the lock against the cached lock. - * - * @param string $lock_key The transient key used to lock the group. - * @param string $lock_value The lock value to be verified. - * @param string $cache_group The cache group in which the lock value is stored. Default is an empty string. - * - * @return bool Whether the lock value matches the cached lock value in the cache group. - */ -function lock_verify( string $lock_key, string $lock_value, string $cache_group = '' ) : bool { - $found = null; - $cached_lock = wp_cache_get( $lock_key, $cache_group, false, $found ); - - return $found && $cached_lock === $lock_value; -} - /** * Registers a cache group for flushing. * @@ -203,12 +144,8 @@ function lock_verify( string $lock_key, string $lock_value, string $cache_group * * @return bool Whether the cache group was successfully registered. */ -function register_cache_group( string $cache_group ) { - global $wp_object_cache; - if ( function_exists( 'wp_cache_add_redis_hash_groups' ) ) { - // Enable cache group flushing for this group - wp_cache_add_redis_hash_groups( $cache_group ); - return $wp_object_cache && isset( $wp_object_cache->redis_hash_groups[ $cache_group ] ); - } - return false; +function register_cache_group( string $cache_group ) : bool { + global $storage; + + return $storage->register_group( $cache_group ); } diff --git a/plugin.php b/plugin.php index 2e38363..9c65edb 100644 --- a/plugin.php +++ b/plugin.php @@ -5,6 +5,9 @@ namespace HM\SwrCache; +require_once __DIR__ . '/inc/class-storageprovider.php'; +require_once __DIR__ . '/inc/class-cachestorageprovider.php'; +require_once __DIR__ . '/inc/class-transoptionstorageprovider.php'; require_once __DIR__ . '/inc/namespace.php'; bootstrap();