was acquired or not. */ protected function acquire_db_lock_w_mysql_functions( $lock_key, $timeout ) { /* * On MySQL 5.6 if a session (a db connection) fires two requests of `GET_LOCK`, the lock is * implicitly released and re-acquired. * While this will not cause issues in the context of different db sessions (e.g. two diff. PHP * processes competing for a lock), it would cause issues when the lock acquisition is attempted * in the context of the same PHP process. * To avoid a read-what-you-write issue in the context of the same request, we check if the lock is * free, using `IS_FREE_LOCK` first. */ global $wpdb; $free = $wpdb->get_var( $wpdb->prepare( 'SELECT IS_FREE_LOCK( SHA1( %s ) )', $lock_key ) ); if ( ! $free ) { return false; } $acquired = $wpdb->get_var( $wpdb->prepare( 'SELECT GET_LOCK( SHA1( %s ),%d )', $lock_key, $timeout ) ); if ( false === $acquired ) { // Only log errors, a failure to acquire lock is not an error. $log_data = [ 'message' => 'Error while trying to acquire lock.', 'key' => $lock_key, 'error' => $wpdb->last_error ]; do_action( 'tribe_log', 'error', __CLASS__, $log_data ); return false; } return true; } /** * Tries to acquire the lock using SQL queries. * * This kind of lock does not support timeout to avoid sieging the MySQL server during processes * that are most likely already stressing it. Either the lock is available the moment it's required or not. * The method leverages `INSERT IGNORE` that it's available on MySQL 5.6 and is atomic provided one of the values * we're trying to insert is UNIQUE or PRIMARY: `option_name` is UNIQUE in the `options` table. * * @since 4.12.6 * * @param string $lock_key The lock key to try and acquire the lock for. * * @return bool Whether the lock was acquired or not. */ protected function acquire_db_lock_w_queries( $lock_key ) { global $wpdb; $option_name = $this->get_db_lock_option_name( $lock_key ); $lock_time = microtime( true ); //phpcs:disable $rows_affected = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')", $option_name, $lock_time ) ); //phpcs:enable if ( false === $rows_affected ) { $log_data = [ 'message' => 'Error while trying to acquire lock with database.', 'key' => $lock_key, 'option_name' => $option_name, 'error' => $wpdb->last_error, ]; do_action( 'tribe_log', 'error', __CLASS__, $log_data ); return false; } /* * The `wpdb::query()` method will return the number of affected rows when using `INSERT`. * 1 row affected means we could INSERT and have the lock, 0 rows affected means we could not INSERT * and have not the lock. */ if ( $rows_affected ) { self::$held_db_locks[ $lock_key ] = $lock_time; } return (bool) $rows_affected; } /** * Returns the option name used to manage the lock for a key in the options table. * * @since 4.12.6 * * @param string $lock_key The lock key to build the option name for. * * @return string The name of the option that will be used to manage the lock for the specified key in the * options table. */ public function get_db_lock_option_name( $lock_key ) { return self::$db_lock_option_prefix . $lock_key; } /** * Releases the database lock of the record. * * Release a not held db lock will return `null`, not `false`. * * @since 4.12.6 * * @param string $lock_key The name of the lock to release. * * @return bool Whether the lock was correctly released or not. */ public function release_db_lock( $lock_key ) { if ( $this->manage_db_lock_w_mysql_functions() ) { return $this->release_db_lock_w_mysql_functions( $lock_key ); } return $this->release_db_lock_w_queries( $lock_key ); } /** * Releases a DB lock held by the current database session (`$wpdb` instance) by * using the MySQL `RELEASE_LOCK` function. * * @since 4.12.6 * * @param string $lock_key The lock key to release the lock for. * * @return bool Whether the lock was correctly released or not. */ protected function release_db_lock_w_mysql_functions( $lock_key ) { global $wpdb; $released = $wpdb->query( $wpdb->prepare( "SELECT RELEASE_LOCK( SHA1( %s ) )", $lock_key ) ); if ( false === $released ) { $log_data = [ 'message' => 'Error while trying to release lock.', 'key' => $lock_key, 'error' => $wpdb->last_error ]; do_action( 'tribe_log', 'error', __CLASS__, $log_data ); return false; } return true; } /** * Releases a lock using SQL queries. * * Note: differently from the `release_db_lock_w_mysql_functions`, this method will release the lock * even if the current session is not the one holding the lock. * To protect from this the trait uses a map of registered locks and when the locks where registered. * * @since 4.12.6 * * @param string $lock_key The lock key to release the lock for. * * @return bool Whether the lock was released or not, errors will be logged, a `false` value is returned if * the lock was not held to begin with. */ protected function release_db_lock_w_queries( $lock_key ) { if ( ! isset( self::$held_db_locks[ $lock_key ] ) ) { // Avoid sessions that do nothold the lock to release it. return false; } global $wpdb; $option_name = $this->get_db_lock_option_name( $lock_key ); //phpcs:disable $rows_affected = $wpdb->delete( $wpdb->options, [ 'option_name' => $option_name ], [ '%s' ] ); //phpcs:enable if ( false === $rows_affected ) { $log_data = [ 'message' => 'Error while trying to release lock with database.', 'key' => $lock_key, 'option_name' => $option_name, 'error' => $wpdb->last_error, ]; do_action( 'tribe_log', 'error', __CLASS__, $log_data ); return false; } if ( $rows_affected ) { // Lock successfully released. unset( self::$held_db_locks[ $lock_key ] ); } return (bool) $rows_affected; } }