asynctask.class.inc.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <?php
  2. // Copyright (C) 2010-2012 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. * Persistent classes (internal): user defined actions
  20. *
  21. * @copyright Copyright (C) 2010-2012 Combodo SARL
  22. * @license http://opensource.org/licenses/AGPL-3.0
  23. */
  24. class ExecAsyncTask implements iBackgroundProcess
  25. {
  26. public function GetPeriodicity()
  27. {
  28. return 2; // seconds
  29. }
  30. public function Process($iTimeLimit)
  31. {
  32. $sNow = date('Y-m-d H:i:s');
  33. // Criteria: planned, and expected to occur... ASAP or in the past
  34. $sOQL = "SELECT AsyncTask WHERE (status = 'planned') AND (ISNULL(planned) OR (planned < '$sNow'))";
  35. $iProcessed = 0;
  36. while (time() < $iTimeLimit)
  37. {
  38. // Next one ?
  39. $oSet = new CMDBObjectSet(DBObjectSearch::FromOQL($sOQL), array('created' => true) /* order by*/, array(), null, 1 /* limit count */);
  40. $oTask = $oSet->Fetch();
  41. if (is_null($oTask))
  42. {
  43. // Nothing to be done
  44. break;
  45. }
  46. $iProcessed++;
  47. if ($oTask->Process())
  48. {
  49. $oTask->DBDelete();
  50. }
  51. }
  52. return "processed $iProcessed tasks";
  53. }
  54. }
  55. /**
  56. * A
  57. *
  58. * @package iTopORM
  59. */
  60. abstract class AsyncTask extends DBObject
  61. {
  62. public static function Init()
  63. {
  64. $aParams = array
  65. (
  66. "category" => "core/cmdb",
  67. "key_type" => "autoincrement",
  68. "name_attcode" => array('created'),
  69. "state_attcode" => "",
  70. "reconc_keys" => array(),
  71. "db_table" => "priv_async_task",
  72. "db_key_field" => "id",
  73. "db_finalclass_field" => "realclass",
  74. "display_template" => "",
  75. );
  76. MetaModel::Init_Params($aParams);
  77. // Null is allowed to ease the migration from iTop 2.0.2 and earlier, when the status did not exist, and because the default value is not taken into account in the SQL definition
  78. // The value is set from null to planned in the setup program
  79. MetaModel::Init_AddAttribute(new AttributeEnum("status", array("allowed_values"=>new ValueSetEnum('planned,running,idle,error'), "sql"=>"status", "default_value"=>"planned", "is_null_allowed"=>true, "depends_on"=>array())));
  80. MetaModel::Init_AddAttribute(new AttributeDateTime("created", array("allowed_values"=>null, "sql"=>"created", "default_value"=>"", "is_null_allowed"=>false, "depends_on"=>array())));
  81. MetaModel::Init_AddAttribute(new AttributeDateTime("started", array("allowed_values"=>null, "sql"=>"started", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  82. MetaModel::Init_AddAttribute(new AttributeDateTime("planned", array("allowed_values"=>null, "sql"=>"planned", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  83. MetaModel::Init_AddAttribute(new AttributeExternalKey("event_id", array("targetclass"=>"Event", "jointype"=> "", "allowed_values"=>null, "sql"=>"event_id", "is_null_allowed"=>true, "on_target_delete"=>DEL_SILENT, "depends_on"=>array())));
  84. MetaModel::Init_AddAttribute(new AttributeInteger("remaining_retries", array("allowed_values"=>null, "sql"=>"remaining_retries", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
  85. MetaModel::Init_AddAttribute(new AttributeInteger("last_error_code", array("allowed_values"=>null, "sql"=>"last_error_code", "default_value"=>0, "is_null_allowed"=>true, "depends_on"=>array())));
  86. MetaModel::Init_AddAttribute(new AttributeString("last_error", array("allowed_values"=>null, "sql"=>"last_error", "default_value"=>'', "is_null_allowed"=>true, "depends_on"=>array())));
  87. MetaModel::Init_AddAttribute(new AttributeDateTime("last_attempt", array("allowed_values"=>null, "sql"=>"last_attempt", "default_value"=>"", "is_null_allowed"=>true, "depends_on"=>array())));
  88. }
  89. /**
  90. * Every is fine
  91. */
  92. const OK = 0;
  93. /**
  94. * The task no longer exists
  95. */
  96. const DELETED = 1;
  97. /**
  98. * The task is already being executed
  99. */
  100. const ALREADY_RUNNING = 2;
  101. /**
  102. * The current process requests the ownership on the task.
  103. * In case the task can be accessed concurrently, this function can be overloaded to add a critical section.
  104. * The function must not block the caller if another process is already owning the task
  105. *
  106. * @return integer A code among OK/DELETED/ALREADY_RUNNING.
  107. */
  108. public function MarkAsRunning()
  109. {
  110. try
  111. {
  112. if ($this->Get('status') == 'running')
  113. {
  114. return self::ALREADY_RUNNING;
  115. }
  116. else
  117. {
  118. $this->Set('status', 'running');
  119. $this->Set('started', time());
  120. $this->DBUpdate();
  121. return self::OK;
  122. }
  123. }
  124. catch(Exception $e)
  125. {
  126. // Corrupted task !! (for example: "Failed to reload object")
  127. IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$e->getMessage().' - fatal error, deleting the task.');
  128. if ($this->Get('event_id') != 0)
  129. {
  130. $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
  131. $oEventLog->Set('message', 'Failed, corrupted data: '.$e->getMessage());
  132. $oEventLog->DBUpdate();
  133. }
  134. $this->DBDelete();
  135. return self::DELETED;
  136. }
  137. }
  138. public function GetRetryDelay($iErrorCode = null)
  139. {
  140. $iRetryDelay = 600;
  141. $aRetries = MetaModel::GetConfig()->Get('async_task_retries', array());
  142. if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries))
  143. {
  144. $aConfig = $aRetries[get_class($this)];
  145. $iRetryDelay = $aConfig['retry_delay'];
  146. }
  147. return $iRetryDelay;
  148. }
  149. public function GetMaxRetries($iErrorCode = null)
  150. {
  151. $iMaxRetries = 0;
  152. $aRetries = MetaModel::GetConfig()->Get('async_task_retries', array());
  153. if (is_array($aRetries) && array_key_exists(get_class($this), $aRetries))
  154. {
  155. $aConfig = $aRetries[get_class($this)];
  156. $iMaxRetries = $aConfig['max_retries'];
  157. }
  158. }
  159. /**
  160. * Override to notify people that a task cannot be performed
  161. */
  162. protected function OnDefinitiveFailure()
  163. {
  164. }
  165. protected function OnInsert()
  166. {
  167. $this->Set('created', time());
  168. }
  169. /**
  170. * @return boolean True if the task record can be deleted
  171. */
  172. public function Process()
  173. {
  174. // By default: consider that the task is not completed
  175. $bRet = false;
  176. // Attempt to take the ownership
  177. $iStatus = $this->MarkAsRunning();
  178. if ($iStatus == self::OK)
  179. {
  180. try
  181. {
  182. $sStatus = $this->DoProcess();
  183. if ($this->Get('event_id') != 0)
  184. {
  185. $oEventLog = MetaModel::GetObject('Event', $this->Get('event_id'));
  186. $oEventLog->Set('message', $sStatus);
  187. $oEventLog->DBUpdate();
  188. }
  189. $bRet = true;
  190. }
  191. catch(Exception $e)
  192. {
  193. $this->HandleError($e->getMessage(), $e->getCode());
  194. }
  195. }
  196. else
  197. {
  198. // Already done or being handled by another process... skip...
  199. $bRet = false;
  200. }
  201. return $bRet;
  202. }
  203. /**
  204. * Overridable to extend the behavior in case of error (logging)
  205. */
  206. protected function HandleError($sErrorMessage, $iErrorCode)
  207. {
  208. if ($this->Get('last_attempt') == '')
  209. {
  210. // First attempt
  211. $this->Set('remaining_retries', $this->GetMaxRetries($iErrorCode));
  212. }
  213. $this->Set('last_error', $sErrorMessage);
  214. $this->Set('last_error_code', $iErrorCode); // Note: can be ZERO !!!
  215. $this->Set('last_attempt', time());
  216. $iRemaining = $this->Get('remaining_retries');
  217. if ($iRemaining > 0)
  218. {
  219. $iRetryDelay = $this->GetRetryDelay($iErrorCode);
  220. IssueLog::Info('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage.' - remaining retries: '.$iRemaining.' - next retry in '.$iRetryDelay.'s');
  221. $this->Set('remaining_retries', $iRemaining - 1);
  222. $this->Set('status', 'planned');
  223. $this->Set('started', null);
  224. $this->Set('planned', time() + $iRetryDelay);
  225. }
  226. else
  227. {
  228. IssueLog::Error('Failed to process async task #'.$this->GetKey().' - reason: '.$sErrorMessage);
  229. $this->Set('status', 'error');
  230. $this->Set('started', null);
  231. $this->Set('planned', null);
  232. $this->OnDefinitiveFailure();
  233. }
  234. $this->DBUpdate();
  235. }
  236. /**
  237. * Throws an exception (message and code)
  238. */
  239. abstract public function DoProcess();
  240. /**
  241. * Describes the error codes that DoProcess can return by the mean of exceptions
  242. */
  243. static public function EnumErrorCodes()
  244. {
  245. return array();
  246. }
  247. }
  248. /**
  249. * An email notification
  250. *
  251. * @package iTopORM
  252. */
  253. class AsyncSendEmail extends AsyncTask
  254. {
  255. public static function Init()
  256. {
  257. $aParams = array
  258. (
  259. "category" => "core/cmdb",
  260. "key_type" => "autoincrement",
  261. "name_attcode" => "created",
  262. "state_attcode" => "",
  263. "reconc_keys" => array(),
  264. "db_table" => "priv_async_send_email",
  265. "db_key_field" => "id",
  266. "db_finalclass_field" => "",
  267. "display_template" => "",
  268. );
  269. MetaModel::Init_Params($aParams);
  270. MetaModel::Init_InheritAttributes();
  271. MetaModel::Init_AddAttribute(new AttributeInteger("version", array("allowed_values"=>null, "sql"=>"version", "default_value"=>Email::ORIGINAL_FORMAT, "is_null_allowed"=>false, "depends_on"=>array())));
  272. MetaModel::Init_AddAttribute(new AttributeText("to", array("allowed_values"=>null, "sql"=>"to", "default_value"=>null, "is_null_allowed"=>true, "depends_on"=>array())));
  273. MetaModel::Init_AddAttribute(new AttributeText("subject", array("allowed_values"=>null, "sql"=>"subject", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  274. MetaModel::Init_AddAttribute(new AttributeLongText("message", array("allowed_values"=>null, "sql"=>"message", "default_value"=>null, "is_null_allowed"=>false, "depends_on"=>array())));
  275. // Display lists
  276. // MetaModel::Init_SetZListItems('details', array('name', 'description', 'status', 'test_recipient', 'from', 'reply_to', 'to', 'cc', 'bcc', 'subject', 'body', 'importance', 'trigger_list')); // Attributes to be displayed for the complete details
  277. // MetaModel::Init_SetZListItems('list', array('name', 'status', 'to', 'subject')); // Attributes to be displayed for a list
  278. // Search criteria
  279. // MetaModel::Init_SetZListItems('standard_search', array('name')); // Criteria of the std search form
  280. // MetaModel::Init_SetZListItems('advanced_search', array('name')); // Criteria of the advanced search form
  281. }
  282. static public function AddToQueue(EMail $oEMail, $oLog)
  283. {
  284. $oNew = MetaModel::NewObject(__class__);
  285. if ($oLog)
  286. {
  287. $oNew->Set('event_id', $oLog->GetKey());
  288. }
  289. $oNew->Set('to', $oEMail->GetRecipientTO(true /* string */));
  290. $oNew->Set('subject', $oEMail->GetSubject());
  291. // $oNew->Set('version', 1);
  292. // $sMessage = serialize($oEMail);
  293. $oNew->Set('version', 2);
  294. $sMessage = $oEMail->SerializeV2();
  295. $oNew->Set('message', $sMessage);
  296. $oNew->DBInsert();
  297. }
  298. public function DoProcess()
  299. {
  300. $sMessage = $this->Get('message');
  301. $iVersion = (int) $this->Get('version');
  302. switch($iVersion)
  303. {
  304. case Email::FORMAT_V2:
  305. $oEMail = Email::UnSerializeV2($sMessage);
  306. break;
  307. case Email::ORIGINAL_FORMAT:
  308. $oEMail = unserialize($sMessage);
  309. break;
  310. default:
  311. return 'Unknown version of the serialization format: '.$iVersion;
  312. }
  313. $iRes = $oEMail->Send($aIssues, true /* force synchro !!!!! */);
  314. switch ($iRes)
  315. {
  316. case EMAIL_SEND_OK:
  317. return "Sent";
  318. case EMAIL_SEND_PENDING:
  319. return "Bug - the email should be sent in synchronous mode";
  320. case EMAIL_SEND_ERROR:
  321. return "Failed: ".implode(', ', $aIssues);
  322. }
  323. }
  324. }
  325. ?>