X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/includes/libs/lockmanager/LockManager.php diff --git a/includes/libs/lockmanager/LockManager.php b/includes/libs/lockmanager/LockManager.php new file mode 100644 index 00000000..a6257bfd --- /dev/null +++ b/includes/libs/lockmanager/LockManager.php @@ -0,0 +1,267 @@ + self::LOCK_SH, + self::LOCK_UW => self::LOCK_EX, // subclasses may use self::LOCK_SH + self::LOCK_EX => self::LOCK_EX + ]; + + /** @var array Map of (resource path => lock type => count) */ + protected $locksHeld = []; + + protected $domain; // string; domain (usually wiki ID) + protected $lockTTL; // integer; maximum time locks can be held + + /** @var string Random 32-char hex number */ + protected $session; + + /** Lock types; stronger locks have higher values */ + const LOCK_SH = 1; // shared lock (for reads) + const LOCK_UW = 2; // shared lock (for reads used to write elsewhere) + const LOCK_EX = 3; // exclusive lock (for writes) + + /** @var int Max expected lock expiry in any context */ + const MAX_LOCK_TTL = 7200; // 2 hours + + /** + * Construct a new instance from configuration + * + * @param array $config Parameters include: + * - domain : Domain (usually wiki ID) that all resources are relative to [optional] + * - lockTTL : Age (in seconds) at which resource locks should expire. + * This only applies if locks are not tied to a connection/process. + */ + public function __construct( array $config ) { + $this->domain = isset( $config['domain'] ) ? $config['domain'] : 'global'; + if ( isset( $config['lockTTL'] ) ) { + $this->lockTTL = max( 5, $config['lockTTL'] ); + } elseif ( PHP_SAPI === 'cli' ) { + $this->lockTTL = 3600; + } else { + $met = ini_get( 'max_execution_time' ); // this is 0 in CLI mode + $this->lockTTL = max( 5 * 60, 2 * (int)$met ); + } + + // Upper bound on how long to keep lock structures around. This is useful when setting + // TTLs, as the "lockTTL" value may vary based on CLI mode and app server group. This is + // a "safe" value that can be used to avoid clobbering other locks that use high TTLs. + $this->lockTTL = min( $this->lockTTL, self::MAX_LOCK_TTL ); + + $random = []; + for ( $i = 1; $i <= 5; ++$i ) { + $random[] = mt_rand( 0, 0xFFFFFFF ); + } + $this->session = md5( implode( '-', $random ) ); + + $this->logger = isset( $config['logger'] ) ? $config['logger'] : new \Psr\Log\NullLogger(); + } + + /** + * Lock the resources at the given abstract paths + * + * @param array $paths List of resource names + * @param int $type LockManager::LOCK_* constant + * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) + * @return StatusValue + */ + final public function lock( array $paths, $type = self::LOCK_EX, $timeout = 0 ) { + return $this->lockByType( [ $type => $paths ], $timeout ); + } + + /** + * Lock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @param int $timeout Timeout in seconds (0 means non-blocking) (since 1.21) + * @return StatusValue + * @since 1.22 + */ + final public function lockByType( array $pathsByType, $timeout = 0 ) { + $pathsByType = $this->normalizePathsByType( $pathsByType ); + + $status = null; + $loop = new WaitConditionLoop( + function () use ( &$status, $pathsByType ) { + $status = $this->doLockByType( $pathsByType ); + + return $status->isOK() ?: WaitConditionLoop::CONDITION_CONTINUE; + }, + $timeout + ); + $loop->invoke(); + + return $status; + } + + /** + * Unlock the resources at the given abstract paths + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + final public function unlock( array $paths, $type = self::LOCK_EX ) { + return $this->unlockByType( [ $type => $paths ] ); + } + + /** + * Unlock the resources at the given abstract paths + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + final public function unlockByType( array $pathsByType ) { + $pathsByType = $this->normalizePathsByType( $pathsByType ); + $status = $this->doUnlockByType( $pathsByType ); + + return $status; + } + + /** + * Get the base 36 SHA-1 of a string, padded to 31 digits. + * Before hashing, the path will be prefixed with the domain ID. + * This should be used internally for lock key or file names. + * + * @param string $path + * @return string + */ + final protected function sha1Base36Absolute( $path ) { + return Wikimedia\base_convert( sha1( "{$this->domain}:{$path}" ), 16, 36, 31 ); + } + + /** + * Get the base 16 SHA-1 of a string, padded to 31 digits. + * Before hashing, the path will be prefixed with the domain ID. + * This should be used internally for lock key or file names. + * + * @param string $path + * @return string + */ + final protected function sha1Base16Absolute( $path ) { + return sha1( "{$this->domain}:{$path}" ); + } + + /** + * Normalize the $paths array by converting LOCK_UW locks into the + * appropriate type and removing any duplicated paths for each lock type. + * + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return array + * @since 1.22 + */ + final protected function normalizePathsByType( array $pathsByType ) { + $res = []; + foreach ( $pathsByType as $type => $paths ) { + $res[$this->lockTypeMap[$type]] = array_unique( $paths ); + } + + return $res; + } + + /** + * @see LockManager::lockByType() + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + protected function doLockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + $lockedByType = []; // map of (type => paths) + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doLock( $paths, $type ) ); + if ( $status->isOK() ) { + $lockedByType[$type] = $paths; + } else { + // Release the subset of locks that were acquired + foreach ( $lockedByType as $lType => $lPaths ) { + $status->merge( $this->doUnlock( $lPaths, $lType ) ); + } + break; + } + } + + return $status; + } + + /** + * Lock resources with the given keys and lock type + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + abstract protected function doLock( array $paths, $type ); + + /** + * @see LockManager::unlockByType() + * @param array $pathsByType Map of LockManager::LOCK_* constants to lists of paths + * @return StatusValue + * @since 1.22 + */ + protected function doUnlockByType( array $pathsByType ) { + $status = StatusValue::newGood(); + foreach ( $pathsByType as $type => $paths ) { + $status->merge( $this->doUnlock( $paths, $type ) ); + } + + return $status; + } + + /** + * Unlock resources with the given keys and lock type + * + * @param array $paths List of paths + * @param int $type LockManager::LOCK_* constant + * @return StatusValue + */ + abstract protected function doUnlock( array $paths, $type ); +}