1<?php 2/** 3 * @defgroup LockManager Lock management 4 * @ingroup FileBackend 5 */ 6use Psr\Log\LoggerInterface; 7use Psr\Log\NullLogger; 8use Wikimedia\WaitConditionLoop; 9 10/** 11 * Resource locking handling. 12 * 13 * This program is free software; you can redistribute it and/or modify 14 * it under the terms of the GNU General Public License as published by 15 * the Free Software Foundation; either version 2 of the License, or 16 * (at your option) any later version. 17 * 18 * This program is distributed in the hope that it will be useful, 19 * but WITHOUT ANY WARRANTY; without even the implied warranty of 20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21 * GNU General Public License for more details. 22 * 23 * You should have received a copy of the GNU General Public License along 24 * with this program; if not, write to the Free Software Foundation, Inc., 25 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 26 * http://www.gnu.org/copyleft/gpl.html 27 * 28 * @file 29 * @ingroup LockManager 30 */ 31 32/** 33 * @brief Class for handling resource locking. 34 * 35 * Locks on resource keys can either be shared or exclusive. 36 * 37 * Implementations must keep track of what is locked by this process 38 * in-memory and support nested locking calls (using reference counting). 39 * At least LOCK_UW and LOCK_EX must be implemented. LOCK_SH can be a no-op. 40 * Locks should either be non-blocking or have low wait timeouts. 41 * 42 * Subclasses should avoid throwing exceptions at all costs. 43 * 44 * @stable to extend 45 * @ingroup LockManager 46 * @since 1.19 47 */ 48abstract class LockManager { 49 /** @var LoggerInterface */ 50 protected $logger; 51 52 /** @var array Mapping of lock types to the type actually used */ 53 protected $lockTypeMap = [ 54 self::LOCK_SH => self::LOCK_SH, 55 self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH 56 self::LOCK_EX => self::LOCK_EX 57 ]; 58 59 /** @var array Map of (resource path => lock type => count) */ 60 protected $locksHeld = []; 61 62 protected $domain; // string; domain (usually wiki ID) 63 protected $lockTTL; // integer; maximum time locks can be held 64 65 /** @var string Random 32-char hex number */ 66 protected $session; 67 68 /** Lock types; stronger locks have higher values */ 69 public const LOCK_SH = 1; // shared lock (for reads) 70 public const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) 71 public const LOCK_EX = 3; // exclusive lock (for writes) 72 73 /** @var int Max expected lock expiry in any context */ 74 protected const MAX_LOCK_TTL = 7200; // 2 hours 75 76 /** 77 * Construct a new instance from configuration 78 * @stable to call 79 * 80 * @param array $config Parameters include: 81 * - domain : Domain (usually wiki ID) that all resources are relative to [optional] 82 * - lockTTL : Age (in seconds) at which resource locks should expire. 83 * This only applies if locks are not tied to a connection/process. 84 */ 85 public function __construct( array $config ) { 86 $this->domain = $config['domain'] ?? 'global'; 87 if ( isset( $config['lockTTL'] ) ) { 88 $this->lockTTL = max( 5, $config['lockTTL'] ); 89 } elseif ( PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg' ) { 90 $this->lockTTL = 3600; 91 } else { 92 $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode 93 $this->lockTTL = max( 5 * 60, 2 * (int)$met ); 94 } 95 96 // Upper bound on how long to keep lock structures around. This is useful when setting 97 // TTLs, as the "lockTTL" value may vary based on CLI mode and app server group. This is 98 // a "safe" value that can be used to avoid clobbering other locks that use high TTLs. 99 $this->lockTTL = min( $this->lockTTL, self::MAX_LOCK_TTL ); 100 101 $random = []; 102 for ( $i = 1; $i <= 5; ++$i ) { 103 $random[] = mt_rand( 0, 0xFFFFFFF ); 104 } 105 $this->session = md5( implode( '-', $random ) ); 106 107 $this->logger = $config['logger'] ?? new NullLogger(); 108 } 109 110 /** 111 * Lock the resources at the given abstract paths 112 * 113 * @param array $paths List of resource names 114 * @param int $type LockManager::LOCK_* constant 115 * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) 116 * @return StatusValue 117 */ 118 final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { 119 return $this->lockByType( [ $type => $paths ], $timeout ); 120 } 121 122 /** 123 * Lock the resources at the given abstract paths 124 * 125 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths 126 * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) 127 * @return StatusValue 128 * @since 1.22 129 */ 130 final public function lockByType( array $pathsByType, $timeout = 0 ) { 131 $pathsByType = $this->normalizePathsByType( $pathsByType ); 132 133 $status = null; 134 $loop = new WaitConditionLoop( 135 function () use ( &$status, $pathsByType ) { 136 $status = $this->doLockByType( $pathsByType ); 137 138 return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; 139 }, 140 $timeout 141 ); 142 $loop->invoke(); 143 144 return $status; 145 } 146 147 /** 148 * Unlock the resources at the given abstract paths 149 * 150 * @param array $paths List of paths 151 * @param int $type LockManager::LOCK_* constant 152 * @return StatusValue 153 */ 154 final public function unlock( array $paths, $type = self::LOCK_EX ) { 155 return $this->unlockByType( [ $type => $paths ] ); 156 } 157 158 /** 159 * Unlock the resources at the given abstract paths 160 * 161 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths 162 * @return StatusValue 163 * @since 1.22 164 */ 165 final public function unlockByType( array $pathsByType ) { 166 $pathsByType = $this->normalizePathsByType( $pathsByType ); 167 $status = $this->doUnlockByType( $pathsByType ); 168 169 return $status; 170 } 171 172 /** 173 * Get the base 36 SHA-1 of a string, padded to 31 digits. 174 * Before hashing, the path will be prefixed with the domain ID. 175 * This should be used internally for lock key or file names. 176 * 177 * @param string $path 178 * @return string 179 */ 180 final protected function sha1Base36Absolute( $path ) { 181 return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); 182 } 183 184 /** 185 * Get the base 16 SHA-1 of a string, padded to 31 digits. 186 * Before hashing, the path will be prefixed with the domain ID. 187 * This should be used internally for lock key or file names. 188 * 189 * @param string $path 190 * @return string 191 */ 192 final protected function sha1Base16Absolute( $path ) { 193 return sha1( "{$this->domain}:{$path}" ); 194 } 195 196 /** 197 * Normalize the $paths array by converting LOCK_UW locks into the 198 * appropriate type and removing any duplicated paths for each lock type. 199 * 200 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths 201 * @return array 202 * @since 1.22 203 */ 204 final protected function normalizePathsByType( array $pathsByType ) { 205 $res = []; 206 foreach ( $pathsByType as $type => $paths ) { 207 foreach ( $paths as $path ) { 208 if ( (string)$path === '' ) { 209 throw new InvalidArgumentException( __METHOD__ . ": got empty path." ); 210 } 211 } 212 $res[$this->lockTypeMap[$type]] = array_unique( $paths ); 213 } 214 215 return $res; 216 } 217 218 /** 219 * @see LockManager::lockByType() 220 * @stable to override 221 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths 222 * @return StatusValue 223 * @since 1.22 224 */ 225 protected function doLockByType( array $pathsByType ) { 226 $status = StatusValue::newGood(); 227 $lockedByType = []; // map of (type => paths) 228 foreach ( $pathsByType as $type => $paths ) { 229 $status->merge( $this->doLock( $paths, $type ) ); 230 if ( $status->isOK() ) { 231 $lockedByType[$type] = $paths; 232 } else { 233 // Release the subset of locks that were acquired 234 foreach ( $lockedByType as $lType => $lPaths ) { 235 $status->merge( $this->doUnlock( $lPaths, $lType ) ); 236 } 237 break; 238 } 239 } 240 241 return $status; 242 } 243 244 /** 245 * Lock resources with the given keys and lock type 246 * 247 * @param array $paths List of paths 248 * @param int $type LockManager::LOCK_* constant 249 * @return StatusValue 250 */ 251 abstract protected function doLock( array $paths, $type ); 252 253 /** 254 * @see LockManager::unlockByType() 255 * @stable to override 256 * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths 257 * @return StatusValue 258 * @since 1.22 259 */ 260 protected function doUnlockByType( array $pathsByType ) { 261 $status = StatusValue::newGood(); 262 foreach ( $pathsByType as $type => $paths ) { 263 $status->merge( $this->doUnlock( $paths, $type ) ); 264 } 265 266 return $status; 267 } 268 269 /** 270 * Unlock resources with the given keys and lock type 271 * 272 * @param array $paths List of paths 273 * @param int $type LockManager::LOCK_* constant 274 * @return StatusValue 275 */ 276 abstract protected function doUnlock( array $paths, $type ); 277} 278