123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- <?php
- // Copyright (C) 2015 Combodo SARL
- //
- // This file is part of iTop.
- //
- // iTop is free software; you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // iTop is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with iTop. If not, see <http://www.gnu.org/licenses/>
- /**
- * 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 <boolean, string, DBObjectSet>
- */
- 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 <boolean, string, DBObjectSet>
- */
- 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();
- }
-
- }
- }
|