name = $name; $this->value = $value; $this->table = $table; $this->blog = $blog; } /** * Takes a lock, including conditional takeover of an expired value. * * @param string $name Option name. * @param int $ttl Lifetime in seconds. * @param int|null $now Current time override for deterministic tests. * @return self|null Null when busy or storage fails. */ public static function acquire( $name, $ttl, $now = null ) { global $wpdb; $now = null === $now ? time() : (int) $now; $value = ( $now + max( 1, (int) $ttl ) ) . '|' . wp_generate_uuid4(); $table = $wpdb->options; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Atomic INSERT; caching cannot establish ownership. $won = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO %i (option_name, option_value, autoload) VALUES (%s, %s, 'no')", $table, $name, $value ) ); if ( 0 === $won ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Read the exact stored value for compare-and-swap, bypassing stale cache entries. $held = $wpdb->get_var( $wpdb->prepare( 'SELECT option_value FROM %i WHERE option_name = %s', $table, $name ) ); if ( ! is_string( $held ) || ! preg_match( '/^(\d+)\|[a-f0-9-]+$/D', $held, $parts ) || (int) $parts[1] > $now ) { return null; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Exactly one contender can replace the observed expired owner. $won = $wpdb->query( $wpdb->prepare( "UPDATE %i SET option_value = %s, autoload = 'no' WHERE option_name = %s AND BINARY option_value = %s", $table, $value, $name, $held ) ); } if ( 1 !== $won ) { return null; } $lock = new self( $name, $value, $table, get_current_blog_id() ); $lock->invalidate(); return $lock; } /** * Extends the lifetime only if this worker still owns the lock. * * @param int $ttl Lifetime in seconds. * @param int|null $now Time override. * @return bool */ public function renew( $ttl, $now = null ) { global $wpdb; $now = null === $now ? time() : (int) $now; $value = ( $now + max( 1, (int) $ttl ) ) . '|' . wp_generate_uuid4(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Ownership-aware renewal, including when expiry times are equal. $won = $wpdb->query( $wpdb->prepare( 'UPDATE %i SET option_value = %s WHERE option_name = %s AND BINARY option_value = %s', $this->table, $value, $this->name, $this->value ) ); if ( 1 !== $won ) { return false; } $this->value = $value; $this->invalidate(); return true; } /** Releases only the value owned by this instance. @return void */ public function release() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Conditional deletion cannot release a replacement owner. $wpdb->query( $wpdb->prepare( 'DELETE FROM %i WHERE option_name = %s AND BINARY option_value = %s', $this->table, $this->name, $this->value ) ); $this->invalidate(); } /** Clears Core's option caches in the owning blog. @return void */ private function invalidate() { $switched = get_current_blog_id() !== $this->blog; if ( $switched ) { switch_to_blog( $this->blog ); } try { wp_cache_delete( $this->name, 'options' ); wp_cache_delete( 'notoptions', 'options' ); } finally { if ( $switched ) { restore_current_blog(); } } } }