ownershiplock.class.inc.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. <?php
  2. // Copyright (C) 2015 Combodo SARL
  3. //
  4. // This file is part of iTop.
  5. //
  6. // iTop is free software; you can redistribute it and/or modify
  7. // it under the terms of the GNU Affero General Public License as published by
  8. // the Free Software Foundation, either version 3 of the License, or
  9. // (at your option) any later version.
  10. //
  11. // iTop is distributed in the hope that it will be useful,
  12. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  13. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  14. // GNU Affero General Public License for more details.
  15. //
  16. // You should have received a copy of the GNU Affero General Public License
  17. // along with iTop. If not, see <http://www.gnu.org/licenses/>
  18. /**
  19. * Mechanism to obtain an exclusive lock while editing an object
  20. *
  21. * @package iTopORM
  22. */
  23. /**
  24. * Persistent storage (in the database) for remembering that an object is locked
  25. */
  26. class iTopOwnershipToken extends DBObject
  27. {
  28. public static function Init()
  29. {
  30. $aParams = array
  31. (
  32. 'category' => 'application',
  33. 'key_type' => 'autoincrement',
  34. 'name_attcode' => array('obj_class', 'obj_key'),
  35. 'state_attcode' => '',
  36. 'reconc_keys' => array(''),
  37. 'db_table' => 'priv_ownership_token',
  38. 'db_key_field' => 'id',
  39. 'db_finalclass_field' => '',
  40. );
  41. MetaModel::Init_Params($aParams);
  42. MetaModel::Init_InheritAttributes();
  43. MetaModel::Init_AddAttribute(new AttributeDateTime("acquired", array("allowed_values"=>null, "sql"=>'acquired', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
  44. MetaModel::Init_AddAttribute(new AttributeDateTime("last_seen", array("allowed_values"=>null, "sql"=>'last_seen', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
  45. MetaModel::Init_AddAttribute(new AttributeString("obj_class", array("allowed_values"=>null, "sql"=>'obj_class', "default_value"=>'', "is_null_allowed"=>false, "depends_on"=>array())));
  46. MetaModel::Init_AddAttribute(new AttributeInteger("obj_key", array("allowed_values"=>null, "sql"=>'obj_key', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
  47. MetaModel::Init_AddAttribute(new AttributeString("token", array("allowed_values"=>null, "sql"=>'token', "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
  48. 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())));
  49. MetaModel::Init_SetZListItems('details', array ('obj_class', 'obj_key', 'last_seen', 'token'));
  50. MetaModel::Init_SetZListItems('standard_search', array ('obj_class', 'obj_key', 'last_seen', 'token'));
  51. MetaModel::Init_SetZListItems('list', array ('obj_class', 'obj_key', 'last_seen', 'token'));
  52. }
  53. }
  54. /**
  55. * Utility class to acquire/extend/release/kill an exclusive lock on a given persistent object,
  56. * for example to prevent concurrent edition of the same object.
  57. * Each lock has an expiration delay of 120 seconds (tunable via the configuration parameter 'concurrent_lock_expiration_delay')
  58. * A watchdog (called twice during this delay) is in charge of keeping the lock "alive" while an object is being edited.
  59. */
  60. class iTopOwnershipLock
  61. {
  62. protected $sObjClass;
  63. protected $iObjKey;
  64. protected $oToken;
  65. /**
  66. * Acquires an exclusive lock on the specified DBObject. Once acquired, the lock is identified
  67. * by a unique "token" string.
  68. * @param string $sObjClass The class of the object for which to acquire the lock
  69. * @param integer $iObjKey The identifier of the object for which to acquire the lock
  70. * @return multitype:boolean iTopOwnershipLock Ambigous <boolean, string, DBObjectSet>
  71. */
  72. public static function AcquireLock($sObjClass, $iObjKey)
  73. {
  74. $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
  75. $oMutex->Lock();
  76. $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
  77. $token = $oOwnershipLock->Acquire();
  78. $oMutex->Unlock();
  79. return array('success' => $token !== false, 'token' => $token, 'lock' => $oOwnershipLock, 'acquired' => $oOwnershipLock->oToken->Get('acquired'));
  80. }
  81. /**
  82. * Extends the ownership lock or acquires it if none exists
  83. * Returns a hash array with 3 elements:
  84. * 'status': either true or false, tells if the lock is still owned
  85. * 'owner': is status is false, the User object currently owning the lock
  86. * '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)
  87. * @param string $sToken
  88. * @return multitype:boolean string User
  89. */
  90. public static function ExtendLock($sObjClass, $iObjKey, $sToken)
  91. {
  92. $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
  93. $oMutex->Lock();
  94. $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
  95. $aResult = $oOwnershipLock->Extend($sToken);
  96. $oMutex->Unlock();
  97. return $aResult;
  98. }
  99. /**
  100. * Releases the given lock for the specified object
  101. *
  102. * @param string $sObjClass The class of the object
  103. * @param int $iObjKey The identifier of the object
  104. * @param string $sToken The string identifying the lock
  105. * @return boolean
  106. */
  107. public static function ReleaseLock($sObjClass, $iObjKey, $sToken)
  108. {
  109. $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
  110. $oMutex->Lock();
  111. $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
  112. $bResult = $oOwnershipLock->Release($sToken);
  113. self::DeleteExpiredLocks(); // Cleanup orphan locks
  114. $oMutex->Unlock();
  115. return $bResult;
  116. }
  117. /**
  118. * Kills the lock for the specified object
  119. *
  120. * @param string $sObjClass The class of the object
  121. * @param int $iObjKey The identifier of the object
  122. * @return boolean
  123. */
  124. public static function KillLock($sObjClass, $iObjKey)
  125. {
  126. $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
  127. $oMutex->Lock();
  128. $sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key";
  129. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('obj_class' => $sObjClass, 'obj_key' => $iObjKey)));
  130. while($oLock = $oSet->Fetch())
  131. {
  132. $oLock->DBDelete();
  133. }
  134. $oMutex->Unlock();
  135. }
  136. /**
  137. * Checks if an exclusive lock exists on the specified DBObject.
  138. * @param string $sObjClass The class of the object for which to acquire the lock
  139. * @param integer $iObjKey The identifier of the object for which to acquire the lock
  140. * @return multitype:boolean iTopOwnershipLock Ambigous <boolean, string, DBObjectSet>
  141. */
  142. public static function IsLocked($sObjClass, $iObjKey)
  143. {
  144. $bLocked = false;
  145. $oMutex = new iTopMutex('lock_'.$sObjClass.'::'.$iObjKey);
  146. $oMutex->Lock();
  147. $oOwnershipLock = new iTopOwnershipLock($sObjClass, $iObjKey);
  148. if ($oOwnershipLock->IsOwned())
  149. {
  150. $bLocked = true;
  151. }
  152. $oMutex->Unlock();
  153. return array('locked' =>$bLocked, 'owner' => $oOwnershipLock->GetOwner());
  154. }
  155. /**
  156. * Get the current owner of the lock
  157. * @return User
  158. */
  159. public function GetOwner()
  160. {
  161. if ($this->IsTokenValid())
  162. {
  163. return MetaModel::GetObject('User', $this->oToken->Get('user_id'), false);
  164. }
  165. return null;
  166. }
  167. /**
  168. * The constructor is protected. Use the static methods AcquireLock / ExtendLock / ReleaseLock / KillLock
  169. * which are protected against concurrent access by a Mutex.
  170. * @param string $sObjClass The class of the object for which to create a lock
  171. * @param integer $iObjKey The identifier of the object for which to create a lock
  172. */
  173. protected function __construct($sObjClass, $iObjKey)
  174. {
  175. $sOQL = "SELECT iTopOwnershipToken WHERE obj_class = :obj_class AND obj_key = :obj_key";
  176. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('obj_class' => $sObjClass, 'obj_key' => $iObjKey)));
  177. $this->oToken = $oSet->Fetch();
  178. $this->sObjClass = $sObjClass;
  179. $this->iObjKey = $iObjKey;
  180. // IssueLog::Info("iTopOwnershipLock::__construct($sObjClass, $iObjKey) oToken::".($this->oToken ? $this->oToken->GetKey() : 'null'));
  181. }
  182. protected function IsOwned()
  183. {
  184. return $this->IsTokenValid();
  185. }
  186. protected function Acquire($sToken = null)
  187. {
  188. if ($this->IsTokenValid())
  189. {
  190. // IssueLog::Info("Acquire($sToken) returns false");
  191. return false;
  192. }
  193. else
  194. {
  195. $sToken = $this->TakeOwnership($sToken);
  196. // IssueLog::Info("Acquire($sToken) returns $sToken");
  197. return $sToken;
  198. }
  199. }
  200. /**
  201. * Extends the ownership lock or acquires it if none exists
  202. * Returns a hash array with 3 elements:
  203. * 'status': either true or false, tells if the lock is still owned
  204. * 'owner': is status is false, the User object currently owning the lock
  205. * '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)
  206. * 'acquired': date at which the lock was initially acquired
  207. * @param string $sToken
  208. * @return multitype:boolean string User
  209. */
  210. protected function Extend($sToken)
  211. {
  212. $aResult = array('status' => true, 'owner' => '', 'operation' => 'renewed');
  213. if ($this->IsTokenValid())
  214. {
  215. if ($sToken === $this->oToken->Get('token'))
  216. {
  217. $this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat()));
  218. $this->oToken->DBUpdate();
  219. $aResult['acquired'] = $this->oToken->Get('acquired');
  220. }
  221. else
  222. {
  223. // IssueLog::Info("Extend($sToken) returns false");
  224. $aResult['status'] = false;
  225. $aResult['operation'] = 'lost';
  226. $aResult['owner'] = $this->GetOwner();
  227. $aResult['acquired'] = $this->oToken->Get('acquired');
  228. }
  229. }
  230. else
  231. {
  232. $aResult['status'] = false;
  233. $aResult['operation'] = 'expired';
  234. }
  235. // IssueLog::Info("Extend($sToken) returns true");
  236. return $aResult;
  237. }
  238. protected function HasOwnership($sToken)
  239. {
  240. $bRet = false;
  241. if ($this->IsTokenValid())
  242. {
  243. if ($sToken === $this->oToken->Get('token'))
  244. {
  245. $bRet = true;
  246. }
  247. }
  248. // IssueLog::Info("HasOwnership($sToken) return $bRet");
  249. return $bRet;
  250. }
  251. protected function Release($sToken)
  252. {
  253. $bRet = false;
  254. // IssueLog::Info("Release... begin [$sToken]");
  255. if (($this->oToken) && ($sToken === $this->oToken->Get('token')))
  256. {
  257. // IssueLog::Info("oToken::".$this->oToken->GetKey().' ('.$sToken.') to be deleted');
  258. $this->oToken->DBDelete();
  259. // IssueLog::Info("oToken deleted");
  260. $this->oToken = null;
  261. $bRet = true;
  262. }
  263. else if ($this->oToken == null)
  264. {
  265. // IssueLog::Info("Release FAILED oToken == null !!!");
  266. }
  267. else
  268. {
  269. // IssueLog::Info("Release FAILED inconsistent tokens: sToken=\"".$sToken.'", oToken->Get(\'token\')="'.$this->oToken->Get('token').'"');
  270. }
  271. // IssueLog::Info("Release... end");
  272. return $bRet;
  273. }
  274. protected function IsTokenValid()
  275. {
  276. $bRet = false;
  277. if ($this->oToken != null)
  278. {
  279. $sToken = $this->oToken->Get('token');
  280. $sDate = $this->oToken->Get('last_seen');
  281. if (($sDate != '') && ($sToken != ''))
  282. {
  283. $oLastSeenTime = new DateTime($sDate);
  284. $iNow = date('U');
  285. if (($iNow - $oLastSeenTime->format('U')) < MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay'))
  286. {
  287. $bRet = true;
  288. }
  289. }
  290. }
  291. return $bRet;
  292. }
  293. protected function TakeOwnership($sToken = null)
  294. {
  295. if ($this->oToken == null)
  296. {
  297. $this->oToken = new iTopOwnershipToken();
  298. $this->oToken->Set('obj_class', $this->sObjClass);
  299. $this->oToken->Set('obj_key', $this->iObjKey);
  300. }
  301. $this->oToken->Set('acquired', date(AttributeDateTime::GetSQLFormat()));
  302. $this->oToken->Set('user_id', UserRights::GetUserId());
  303. $this->oToken->Set('last_seen', date(AttributeDateTime::GetSQLFormat()));
  304. if ($sToken === null)
  305. {
  306. $sToken = sprintf('%X', microtime(true));
  307. }
  308. $this->oToken->Set('token', $sToken);
  309. $this->oToken->DBWrite();
  310. return $this->oToken->Get('token');
  311. }
  312. protected static function DeleteExpiredLocks()
  313. {
  314. $sOQL = "SELECT iTopOwnershipToken WHERE last_seen < :last_seen_limit";
  315. $oSet = new DBObjectSet(DBObjectSearch::FromOQL($sOQL, array('last_seen_limit' => date(AttributeDateTime::GetSQLFormat(), time() - MetaModel::GetConfig()->Get('concurrent_lock_expiration_delay')))));
  316. while($oToken = $oSet->Fetch())
  317. {
  318. $oToken->DBDelete();
  319. }
  320. }
  321. }