/** * Mechanism to obtain an exclusive lock while editing an object * * @package iTopORM */ /** * Persistent storage (in the database) for remembering that an object is locked */ class iTopOwnershipToken extends DBObject { public static function Init() { $aParams = array ( 'category' => 'application', 'key_type' => 'autoincrement', 'name_attcode' => array('obj_class', 'obj_key'), 'state_attcode' => '', 'reconc_keys' => array(''), 'db_table' => 'priv_ownership_token', 'db_key_field' => 'id', 'db_finalclass_field' => '', ); MetaModel::Init_Params($aParams); MetaModel::Init_InheritAttributes(); MetaModel::Init_AddAttribute(new AttributeDateTime("acquired", array("allowed_values"=>null, "sql"=>'acquired', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeDateTime("last_seen", array("allowed_values"=>null, "sql"=>'last_seen', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeString("obj_class", array("allowed_values"=>null, "sql"=>'obj_class', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeInteger("obj_key", array("allowed_values"=>null, "sql"=>'obj_key', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeString("token", array("allowed_values"=>null, "sql"=>'token', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array()))); MetaModel::Init_AddAttribute(new AttributeExternalKey("user_id", array("targetclass"=>"User", "jointype"=> '', "allowed_values"=>null, "sql"=>"user_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array()))); MetaModel::Init_SetZListItems('details', array ('obj_class', 'obj_key', 'last_seen', 'token')); MetaModel::Init_SetZListItems('standard_search', array ('obj_class', 'obj_key', 'last_seen', 'token')); MetaModel::Init_SetZListItems('list', array ('obj_class', 'obj_key', 'last_seen', 'token')); } } /** * Utility class to acquire/extend/release/kill an exclusive lock on a given persistent object, * for example to prevent concurrent edition of the same object. * Each lock has an expiration delay of 120 seconds (tunable via the configuration parameter 'concurrent_lock_expiration_delay') * A watchdog (called twice during this delay) is in charge of keeping the lock "alive" while an object is being edited. */ class iTopOwnershipLock { protected $sObjClass; protected $iObjKey; protected $oToken; /** * Acquires an exclusive lock on the specified DBObject. Once acquired, the lock is identified * by a unique "token" string. * @param string $sObjClass The class of the object for which to acquire the lock * @param integer $iObjKey The identifier of the object for which to acquire the lock * @return multitype:boolean iTopOwnershipLock Ambigous */ public static function AcquireLock($sObjClass, $iObjKey) { $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey); $oMutex->Lock(); $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey); $token = $oOwnershipLock->Acquire(); $oMutex->Unlock(); return array('success' => $token !== false, 'token' => $token, 'lock' => $oOwnershipLock, 'acquired' => $oOwnershipLock->oToken->Get('acquired')); } /** * Extends the ownership lock or acquires it if none exists * Returns a hash array with 3 elements: * 'status': either true or false, tells if the lock is still owned * 'owner': is status is false, the User object currently owning the lock * 'operation': whether the lock was 'renewed' (i.e. the lock was valid, its duration has been extended) or 'acquired' (there was no valid lock for this object and a new one was created) * @param string $sToken * @return multitype:boolean string User */ public static function ExtendLock($sObjClass, $iObjKey, $sToken) { $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey); $oMutex->Lock(); $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey); $aResult = $oOwnershipLock->Extend($sToken); $oMutex->Unlock(); return $aResult; } /** * Releases the given lock for the specified object * * @param string $sObjClass The class of the object * @param int $iObjKey The identifier of the object * @param string $sToken The string identifying the lock * @return boolean */ public static function ReleaseLock($sObjClass, $iObjKey, $sToken) { $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey); $oMutex->Lock(); $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey); $bResult = $oOwnershipLock->Release($sToken); self::DeleteExpiredLocks(); // Cleanup orphan locks $oMutex->Unlock(); return $bResult; } /** * Kills the lock for the specified object * * @param string $sObjClass The class of the object * @param int $iObjKey The identifier of the object * @return boolean */ public static function KillLock($sObjClass, $iObjKey) { $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey); $oMutex->Lock(); $sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key"; $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('obj_class' => $sObjClass, 'obj_key' => $iObjKey))); while($oLock = $oSet->Fetch()) { $oLock->DBDelete(); } $oMutex->Unlock(); } /** * Checks if an exclusive lock exists on the specified DBObject. * @param string $sObjClass The class of the object for which to acquire the lock * @param integer $iObjKey The identifier of the object for which to acquire the lock * @return multitype:boolean iTopOwnershipLock Ambigous */ public static function IsLocked($sObjClass, $iObjKey) { $bLocked = false; $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey); $oMutex->Lock(); $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey); if ($oOwnershipLock->IsOwned()) { $bLocked = true; } $oMutex->Unlock(); return array('locked' =>$bLocked, 'owner' => $oOwnershipLock->GetOwner()); } /** * Get the current owner of the lock * @return User */ public function GetOwner() { if ($this->IsTokenValid()) { return MetaModel::GetObject('User', $this->oToken->Get('user_id'), false); } return null; } /** * The constructor is protected. Use the static methods AcquireLock / ExtendLock / ReleaseLock / KillLock * which are protected against concurrent access by a Mutex. * @param string $sObjClass The class of the object for which to create a lock * @param integer $iObjKey The identifier of the object for which to create a lock */ protected function __construct($sObjClass, $iObjKey) { $sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key"; $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('obj_class' => $sObjClass, 'obj_key' => $iObjKey))); $this->oToken = $oSet->Fetch(); $this->sObjClass = $sObjClass; $this->iObjKey = $iObjKey; // IssueLog::Info("iTopOwnershipLock::__construct($sObjClass, $iObjKey) oToken::".($this->oToken ? $this->oToken->GetKey() : 'null')); } protected function IsOwned() { return $this->IsTokenValid(); } protected function Acquire($sToken = null) { if ($this->IsTokenValid()) { // IssueLog::Info("Acquire($sToken) returns false"); return false; } else { $sToken = $this->TakeOwnership($sToken); // IssueLog::Info("Acquire($sToken) returns $sToken"); return $sToken; } } /** * Extends the ownership lock or acquires it if none exists * Returns a hash array with 3 elements: * 'status': either true or false, tells if the lock is still owned * 'owner': is status is false, the User object currently owning the lock * 'operation': whether the lock was 'renewed' (i.e. the lock was valid, its duration was extended) or 'expired' (there was no valid lock for this object) or 'lost' (someone else grabbed it) * 'acquired': date at which the lock was initially acquired * @param string $sToken * @return multitype:boolean string User */ protected function Extend($sToken) { $aResult = array('status' => true, 'owner' => '', 'operation' => 'renewed'); if ($this->IsTokenValid()) { if ($sToken === $this->oToken->Get('token')) { $this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat())); $this->oToken->DBUpdate(); $aResult['acquired'] = $this->oToken->Get('acquired'); } else { // IssueLog::Info("Extend($sToken) returns false"); $aResult['status'] = false; $aResult['operation'] = 'lost'; $aResult['owner'] = $this->GetOwner(); $aResult['acquired'] = $this->oToken->Get('acquired'); } } else { $aResult['status'] = false; $aResult['operation'] = 'expired'; } // IssueLog::Info("Extend($sToken) returns true"); return $aResult; } protected function HasOwnership($sToken) { $bRet = false; if ($this->IsTokenValid()) { if ($sToken === $this->oToken->Get('token')) { $bRet = true; } } // IssueLog::Info("HasOwnership($sToken) return $bRet"); return $bRet; } protected function Release($sToken) { $bRet = false; // IssueLog::Info("Release... begin [$sToken]"); if (($this->oToken) && ($sToken === $this->oToken->Get('token'))) { // IssueLog::Info("oToken::".$this->oToken->GetKey().' ('.$sToken.') to be deleted'); $this->oToken->DBDelete(); // IssueLog::Info("oToken deleted"); $this->oToken = null; $bRet = true; } else if ($this->oToken == null) { // IssueLog::Info("Release FAILED oToken == null !!!"); } else { // IssueLog::Info("Release FAILED inconsistent tokens: sToken=\"".$sToken.'", oToken->Get(\'token\')="'.$this->oToken->Get('token').'"'); } // IssueLog::Info("Release... end"); return $bRet; } protected function IsTokenValid() { $bRet = false; if ($this->oToken != null) { $sToken = $this->oToken->Get('token'); $sDate = $this->oToken->Get('last_seen'); if (($sDate != '') && ($sToken != '')) { $oLastSeenTime = new DateTime($sDate); $iNow = date('U'); if (($iNow - $oLastSeenTime->format('U')) < MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay')) { $bRet = true; } } } return $bRet; } protected function TakeOwnership($sToken = null) { if ($this->oToken == null) { $this->oToken = new iTopOwnershipToken(); $this->oToken->Set('obj_class', $this->sObjClass); $this->oToken->Set('obj_key', $this->iObjKey); } $this->oToken->Set('acquired', date(AttributeDateTime::GetSQLFormat())); $this->oToken->Set('user_id', UserRights::GetUserId()); $this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat())); if ($sToken === null) { $sToken = sprintf('%X', microtime(true)); } $this->oToken->Set('token', $sToken); $this->oToken->DBWrite(); return $this->oToken->Get('token'); } protected static function DeleteExpiredLocks() { $sOQL = "SELECT iTopOwnershipToken WHERE last_seen < :last_seen_limit"; $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('last_seen_limit' => date(AttributeDateTime::GetSQLFormat(), time() - MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay'))))); while($oToken = $oSet->Fetch()) { $oToken->DBDelete(); } } }