Changeset View
Changeset View
Standalone View
Standalone View
src/filesystem/PhutilLock.php
- This file was added.
| <?php | |||||
| /** | |||||
| * Base class for locks, like file locks. | |||||
| * | |||||
| * libphutil provides a concrete lock in @{class:PhutilFileLock}. | |||||
| * | |||||
| * $lock->lock(); | |||||
| * do_contentious_things(); | |||||
| * $lock->unlock(); | |||||
| * | |||||
| * If the lock can't be acquired because it is already held, | |||||
| * @{class:PhutilLockException} is thrown. Other exceptions indicate | |||||
| * permanent failure unrelated to locking. | |||||
| * | |||||
| * When extending this class, you should call @{method:getLock} to look up | |||||
| * an existing lock object, and @{method:registerLock} when objects are | |||||
| * constructed to register for automatic unlock on shutdown. | |||||
| * | |||||
| * @task impl Lock Implementation | |||||
| * @task registry Lock Registry | |||||
| * @task construct Constructing Locks | |||||
| * @task status Determining Lock Status | |||||
| * @task lock Locking | |||||
| * @task internal Internals | |||||
| */ | |||||
| abstract class PhutilLock extends Phobject { | |||||
| private static $registeredShutdownFunction = false; | |||||
| private static $locks = array(); | |||||
| private $locked = false; | |||||
| private $profilerID; | |||||
| private $name; | |||||
| /* -( Constructing Locks )------------------------------------------------- */ | |||||
| /** | |||||
| * Build a new lock, given a lock name. The name should be globally unique | |||||
| * across all locks. | |||||
| * | |||||
| * @param string Globally unique lock name. | |||||
| * @task construct | |||||
| */ | |||||
| protected function __construct($name) { | |||||
| $this->name = $name; | |||||
| } | |||||
| /* -( Lock Implementation )------------------------------------------------ */ | |||||
| /** | |||||
| * Acquires the lock, or throws @{class:PhutilLockException} if it fails. | |||||
| * | |||||
| * @param float Seconds to block waiting for the lock. | |||||
| * @return void | |||||
| * @task impl | |||||
| */ | |||||
| abstract protected function doLock($wait); | |||||
| /** | |||||
| * Releases the lock. | |||||
| * | |||||
| * @return void | |||||
| * @task impl | |||||
| */ | |||||
| abstract protected function doUnlock(); | |||||
| /* -( Lock Registry )------------------------------------------------------ */ | |||||
| /** | |||||
| * Returns a globally unique name for this lock. | |||||
| * | |||||
| * @return string Globally unique lock name, across all locks. | |||||
| * @task registry | |||||
| */ | |||||
| final public function getName() { | |||||
| return $this->name; | |||||
| } | |||||
| /** | |||||
| * Get a named lock, if it has been registered. | |||||
| * | |||||
| * @param string Lock name. | |||||
| * @task registry | |||||
| */ | |||||
| protected static function getLock($name) { | |||||
| return idx(self::$locks, $name); | |||||
| } | |||||
| /** | |||||
| * Register a lock for cleanup when the process exits. | |||||
| * | |||||
| * @param PhutilLock Lock to register. | |||||
| * @task registry | |||||
| */ | |||||
| protected static function registerLock(PhutilLock $lock) { | |||||
| if (!self::$registeredShutdownFunction) { | |||||
| register_shutdown_function(array(__CLASS__, 'unlockAll')); | |||||
| self::$registeredShutdownFunction = true; | |||||
| } | |||||
| $name = $lock->getName(); | |||||
| if (self::getLock($name)) { | |||||
| throw new Exception( | |||||
| pht("Lock '%s' is already registered!", $name)); | |||||
| } | |||||
| self::$locks[$name] = $lock; | |||||
| } | |||||
| /* -( Determining Lock Status )-------------------------------------------- */ | |||||
| /** | |||||
| * Determine if the lock is currently held. | |||||
| * | |||||
| * @return bool True if the lock is held. | |||||
| * | |||||
| * @task status | |||||
| */ | |||||
| final public function isLocked() { | |||||
| return $this->locked; | |||||
| } | |||||
| /* -( Locking )------------------------------------------------------------ */ | |||||
| /** | |||||
| * Acquire the lock. If lock acquisition fails because the lock is held by | |||||
| * another process, throws @{class:PhutilLockException}. Other exceptions | |||||
| * indicate that lock acquisition has failed for reasons unrelated to locking. | |||||
| * | |||||
| * If the lock is already held by this process, this method throws. You can | |||||
| * test the lock status with @{method:isLocked}. | |||||
| * | |||||
| * @param float Seconds to block waiting for the lock. By default, do not | |||||
| * block. | |||||
| * @return this | |||||
| * | |||||
| * @task lock | |||||
| */ | |||||
| final public function lock($wait = 0) { | |||||
| if ($this->locked) { | |||||
| $name = $this->getName(); | |||||
| throw new Exception( | |||||
| pht("Lock '%s' has already been locked by this process.", $name)); | |||||
| } | |||||
| $profiler = PhutilServiceProfiler::getInstance(); | |||||
| $profiler_id = $profiler->beginServiceCall( | |||||
| array( | |||||
| 'type' => 'lock', | |||||
| 'name' => $this->getName(), | |||||
| )); | |||||
| try { | |||||
| $this->doLock((float)$wait); | |||||
| } catch (Exception $ex) { | |||||
| $profiler->endServiceCall( | |||||
| $profiler_id, | |||||
| array( | |||||
| 'lock' => false, | |||||
| )); | |||||
| throw $ex; | |||||
| } | |||||
| $this->profilerID = $profiler_id; | |||||
| $this->locked = true; | |||||
| return $this; | |||||
| } | |||||
| /** | |||||
| * Release the lock. Throws an exception on failure, e.g. if the lock is not | |||||
| * currently held. | |||||
| * | |||||
| * @return this | |||||
| * | |||||
| * @task lock | |||||
| */ | |||||
| final public function unlock() { | |||||
| if (!$this->locked) { | |||||
| $name = $this->getName(); | |||||
| throw new Exception( | |||||
| pht("Lock '%s is not locked by this process!", $name)); | |||||
| } | |||||
| $this->doUnlock(); | |||||
| $profiler = PhutilServiceProfiler::getInstance(); | |||||
| $profiler->endServiceCall( | |||||
| $this->profilerID, | |||||
| array( | |||||
| 'lock' => true, | |||||
| )); | |||||
| $this->profilerID = null; | |||||
| $this->locked = false; | |||||
| return $this; | |||||
| } | |||||
| /* -( Internals )---------------------------------------------------------- */ | |||||
| /** | |||||
| * On shutdown, we release all the locks. You should not call this method | |||||
| * directly. Use @{method:unlock} to release individual locks. | |||||
| * | |||||
| * @return void | |||||
| * | |||||
| * @task internal | |||||
| */ | |||||
| public static function unlockAll() { | |||||
| foreach (self::$locks as $key => $lock) { | |||||
| if ($lock->locked) { | |||||
| $lock->unlock(); | |||||
| } | |||||
| } | |||||
| } | |||||
| } | |||||