TripalJob.inc 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. <?php
  2. class TripalJob {
  3. /**
  4. * The ID of the job.
  5. */
  6. private $job_id = NULL;
  7. /**
  8. * Contains the job record for this job.
  9. */
  10. private $job = NULL;
  11. /**
  12. * Instantiates a new TripalJob object.
  13. *
  14. * By default the job object is "empty". It must be associated with
  15. * job details either by calling the load() function or the
  16. * create() function.
  17. */
  18. public function __construct() {
  19. $this->log = '';
  20. }
  21. /**
  22. * Loads a job for this object.
  23. *
  24. * @param $job_id
  25. * The ID of the job.
  26. */
  27. public function load($job_id) {
  28. if (!$job_id or !is_numeric($job_id)) {
  29. throw new Exception("Must provide a numeric \$job_id to the tripal_cancel_job() function.");
  30. }
  31. $sql = 'SELECT j.* FROM {tripal_jobs} j WHERE j.job_id = :job_id';
  32. $args = array(':job_id' => $job_id);
  33. $this->job = db_query($sql, $args)->fetchObject();
  34. if (!$this->job) {
  35. throw new Exception("Cannot find a job with this ID provided.");
  36. }
  37. // Fix the date/time fields.
  38. $this->job->submit_date_string = $this->job->submit_date ? format_date($this->job->submit_date) : '';
  39. $this->job->start_time_string = $this->job->start_time ? format_date($this->job->start_time): '';
  40. $this->job->end_time_string = $this->job->end_time ? format_date($this->job->end_time): '';
  41. // Unserialize the includes.
  42. $this->job->includes = unserialize($this->job->includes);
  43. // Arguments for jobs used to be stored as plain string with a double colon
  44. // separating them. But as of Tripal v2.0 the arguments are stored as
  45. // a serialized array. To be backwards compatible, we should check for
  46. // serialization and if not then we will use the old style
  47. $this->job->arguments = unserialize($this->job->arguments);
  48. if (!is_array($this->job->arguments)) {
  49. $this->job->arguments = explode("::", $this->job->arguments);
  50. }
  51. }
  52. /**
  53. * Creates a new job.
  54. *
  55. * @param $details
  56. * An associative array of the job details or a single job_id. If the
  57. * details are provided then the job is created and added to the database
  58. * otherwise if a job_id is provided then the object is loaded from the
  59. * database. The following keys are allowed:
  60. * - job_name: The human readable name for the job.
  61. * - modulename: The name of the module adding the job.
  62. * - callback: The name of a function to be called when the job is executed.
  63. * - arguments: An array of arguments to be passed on to the callback.
  64. * - uid: The uid of the user adding the job
  65. * - priority: The priority at which to run the job where the highest
  66. * priority is 10 and the lowest priority is 1. The default
  67. * priority is 10.
  68. * - includes: An array of paths to files that should be included in order
  69. * to execute the job. Use the module_load_include function to get a path
  70. * for a given file.
  71. *
  72. * @throws Exception
  73. * On failure an exception is thrown.
  74. */
  75. public function create($details) {
  76. // Set some defaults
  77. if (!array_key_exists('prority', $details)) {
  78. $details['priority'] = 10;
  79. }
  80. if (!array_key_exists('includes', $details)) {
  81. $details['includes'] = array();
  82. }
  83. // Make sure the arguments are correct.
  84. if (!$details['job_name']) {
  85. throw new Exception("Must provide a \$job_name argument to the tripal_add_job() function.");
  86. }
  87. if (!$details['modulename']) {
  88. throw new Exception("Must provide a \$modulename argument to the tripal_add_job() function.");
  89. }
  90. if (!$details['callback']) {
  91. throw new Exception("Must provide a \$callback argument to the tripal_add_job() function.");
  92. }
  93. $includes = $details['includes'];
  94. foreach ($includes as $include) {
  95. require_once($include);
  96. }
  97. if (!function_exists($details['callback'])) {
  98. throw new Exception("Must provide a valid callback function to the tripal_add_job() function.");
  99. }
  100. if (!is_numeric($details['uid'])) {
  101. throw new Exception("Must provide a numeric \$uid argument to the tripal_add_job() function.");
  102. }
  103. $priority = $details['priority'];
  104. if (!$priority or !is_numeric($priority) or $priority < 1 or $priority > 10) {
  105. throw new Exception("Must provide a numeric \$priority argument between 1 and 10 to the tripal_add_job() function.");
  106. }
  107. $arguments = $details['arguments'];
  108. if (!is_array($arguments)) {
  109. throw new Exception("Must provide an array as the \$arguments argument to the tripal_add_job() function.");
  110. }
  111. // convert the arguments into a string for storage in the database
  112. $args = array();
  113. if (is_array($arguments)) {
  114. $args = serialize($arguments);
  115. }
  116. try {
  117. $job_id = db_insert('tripal_jobs')
  118. ->fields(array(
  119. 'job_name' => $details['job_name'],
  120. 'modulename' => $details['modulename'],
  121. 'callback' => $details['callback'],
  122. 'status' => 'Waiting',
  123. 'submit_date' => time(),
  124. 'uid' => $details['uid'],
  125. 'priority' => $priority,
  126. 'arguments' => $args,
  127. 'includes' => serialize($includes),
  128. ))
  129. ->execute();
  130. // Now load the job into this object.
  131. $this->load($job_id);
  132. }
  133. catch (Exception $e) {
  134. throw new Exception('Cannot create job: ' . $e->getMessage());
  135. }
  136. }
  137. /**
  138. * Cancels the job and prevents it from running.
  139. */
  140. public function cancel() {
  141. if (!$this->job) {
  142. throw new Exception("There is no job associated with this object. Cannot cancel");
  143. }
  144. if ($this->job->status == 'Running') {
  145. throw new Exception("Job Cannot be cancelled it is currently running.");
  146. }
  147. if ($this->job->status == 'Completed') {
  148. throw new Exception("Job Cannot be cancelled it has already finished.");
  149. }
  150. if ($this->job->status == 'Error') {
  151. throw new Exception("Job Cannot be cancelled it is in an error state.");
  152. }
  153. if ($this->job->status == 'Cancelled') {
  154. throw new Exception("Job Cannot be cancelled it is already cancelled.");
  155. }
  156. // Set the end time for this job.
  157. try {
  158. if ($this->job->start_time == 0) {
  159. $record = new stdClass();
  160. $record->job_id = $this->job->job_id;
  161. $record->status = 'Cancelled';
  162. $record->progress = '0';
  163. drupal_write_record('tripal_jobs', $record, 'job_id');
  164. }
  165. }
  166. catch (Exception $e) {
  167. throw new Exception('Cannot cancel job: ' . $e->getMessage());
  168. }
  169. }
  170. /**
  171. * Executes the job.
  172. */
  173. public function run() {
  174. if (!$this->job) {
  175. throw new Exception('Cannot launch job as no job is associated with this object.');
  176. }
  177. try {
  178. // Include the necessary files needed to run the job.
  179. foreach ($this->job->includes as $path) {
  180. if ($path) {
  181. require_once $path;
  182. }
  183. }
  184. // Set the start time for this job.
  185. $record = new stdClass();
  186. $record->job_id = $this->job->job_id;
  187. $record->start_time = time();
  188. $record->status = 'Running';
  189. $record->pid = getmypid();
  190. drupal_write_record('tripal_jobs', $record, 'job_id');
  191. // Callback functions need the job in order to update
  192. // progress. But prior to Tripal v3 the job callback functions
  193. // only accepted a $job_id as the final argument. So, we need
  194. // to see if the callback is Tv3 compatible or older. If older
  195. // we want to still support it and pass the job_id.
  196. $arguments = $this->job->arguments;
  197. $callback = $this->job->callback;
  198. $ref = new ReflectionFunction($callback);
  199. $refparams = $ref->getParameters();
  200. if (count($refparams) > 0) {
  201. $lastparam = $refparams[count($refparams)-1];
  202. if ($lastparam->getName() == 'job_id') {
  203. $arguments[] = $this->job->job_id;
  204. }
  205. else {
  206. $arguments[] = $this;
  207. }
  208. }
  209. // Launch the job.
  210. call_user_func_array($callback, $arguments);
  211. // Set the end time for this job.
  212. $record = new stdClass();
  213. $record->job_id = $this->job->job_id;
  214. $record->end_time = time();
  215. $record->error_msg = $this->job->error_msg;
  216. $record->progress = $this->job->progress;
  217. $record->status = 'Completed';
  218. $record->pid = '';
  219. drupal_write_record('tripal_jobs', $record, 'job_id');
  220. $this->load($this->job->job_id);
  221. }
  222. catch (Exception $e) {
  223. $record->end_time = time();
  224. $record->error_msg = $this->job->error_msg;
  225. $record->progress = $this->job->progress;
  226. $record->status = 'Error';
  227. $record->pid = '';
  228. drupal_write_record('tripal_jobs', $record, 'job_id');
  229. drupal_set_message('Job execution failed: ' . $e->getMessage(), 'error');
  230. }
  231. }
  232. /**
  233. * Inidcates if the job is running.
  234. *
  235. * @return
  236. * TRUE if the job is running, FALSE otherwise.
  237. */
  238. public function isRunning() {
  239. if (!$this->job) {
  240. throw new Exception('Cannot check running status as no job is associated with this object.');
  241. }
  242. $status = shell_exec('ps -p ' . escapeshellarg($this->job->pid) . ' -o pid=');
  243. if ($this->job->pid && $status) {
  244. // The job is still running.
  245. return TRUE;
  246. }
  247. // return FALSE to indicate that no jobs are currently running.
  248. return FALSE;
  249. }
  250. /**
  251. * Retrieve the job object as if from a database query.
  252. */
  253. public function getJob(){
  254. return $this->job;
  255. }
  256. /**
  257. * Retrieves the job ID.
  258. */
  259. public function getJobID(){
  260. return $this->job->job_id;
  261. }
  262. /**
  263. * Retrieves the user ID of the user that submitted the job.
  264. */
  265. public function getUID() {
  266. return $this->job->uid;
  267. }
  268. /**
  269. * Retrieves the job name.
  270. */
  271. public function getJobName() {
  272. return $this->job->job_name;
  273. }
  274. /**
  275. * Retrieves the name of the module that submitted the job.
  276. */
  277. public function getModuleName() {
  278. return $this->job->modulename;
  279. }
  280. /**
  281. * Retrieves the callback function for the job.
  282. */
  283. public function getCallback() {
  284. return $this->job->callback;
  285. }
  286. /**
  287. * Retrieves the array of arguments for the job.
  288. */
  289. public function getArguments() {
  290. return $this->job->arguments;
  291. }
  292. /**
  293. * Retrieves the current percent complete (i.e. progress) of the job.
  294. */
  295. public function getProgress() {
  296. return $this->job->progress;
  297. }
  298. /**
  299. * Sets the current percent complete of a job.
  300. *
  301. * @param $percent_done
  302. * A value between 0 and 100 indicating the percentage complete of the job.
  303. */
  304. public function setProgress($percent_done) {
  305. if (!$this->job) {
  306. throw new Exception('Cannot set progress as no job is associated with this object.');
  307. }
  308. $this->job->progress = $percent_done;
  309. $progress = sprintf("%d", $percent_done);
  310. db_update('tripal_jobs')
  311. ->fields(array(
  312. 'progress' => $progress,
  313. ))
  314. ->condition('job_id', $this->job->job_id)
  315. ->execute();
  316. }
  317. /**
  318. * Retrieves the status of the job.
  319. */
  320. public function getStatus() {
  321. return $this->job->status;
  322. }
  323. /**
  324. * Retrieves the time the job was submitted.
  325. */
  326. public function getSubmitTime() {
  327. return $this->job->submit_date;
  328. }
  329. /**
  330. * Retieves the time the job began execution (i.e. the start time).
  331. */
  332. public function getStartTime() {
  333. return $this->job->start_time;
  334. }
  335. /**
  336. * Retieves the time the job completed execution (i.e. the end time).
  337. */
  338. public function getEndTime() {
  339. return $this->job->end_time;
  340. }
  341. /**
  342. * Retieves the log for the job.
  343. *
  344. * @return
  345. * A large string containing the text of the job log. It contains both
  346. * status upates and errors.
  347. */
  348. public function getLog() {
  349. return $this->job->error_msg;
  350. }
  351. /**
  352. * Retrieves the process ID of the job.
  353. */
  354. public function getPID() {
  355. return $this->job->pid;
  356. }
  357. /**
  358. * Retreieves the priority that is currently set for the job.
  359. */
  360. public function getPriority() {
  361. return $this->job->priority;
  362. }
  363. /**
  364. * Get the MLock value of the job.
  365. *
  366. * The MLock value indicates if no other jobs from a give module
  367. * should be executed while this job is running.
  368. */
  369. public function getMLock() {
  370. return $this->job->mlock;
  371. }
  372. /**
  373. * Get the lock value of the job.
  374. *
  375. * The lock value indicates if no other jobs from any module
  376. * should be executed while this job is running.
  377. */
  378. public function getLock() {
  379. return $this->job->lock;
  380. }
  381. /**
  382. * Get the list of files that must be included prior to job execution.
  383. */
  384. public function getIncludes() {
  385. return $this->job->includes;
  386. }
  387. /**
  388. * Logs a message for the job.
  389. *
  390. * There is no distinction between status messages and error logs. Any
  391. * message that is intended for the user to review the status of the job
  392. * can be provided here.
  393. *
  394. * Messages that are are of severity TRIPAL_CRITICAL or TRIPAL_ERROR
  395. * are also logged to the watchdog.
  396. *
  397. * Logging works regardless if the job uses a transaction. If the
  398. * transaction must be rolled back to to an error the error messages will
  399. * persist.
  400. *
  401. * @param $message
  402. * The message to store in the log. Keep $message translatable by not
  403. * concatenating dynamic values into it! Variables in the message should
  404. * be added by using placeholder strings alongside the variables argument
  405. * to declare the value of the placeholders. See t() for documentation on
  406. * how $message and $variables interact.
  407. * @param $variables
  408. * Array of variables to replace in the message on display or NULL if
  409. * message is already translated or not possible to translate.
  410. * @param $severity
  411. * The severity of the message; one of the following values:
  412. * - TRIPAL_CRITICAL: Critical conditions.
  413. * - TRIPAL_ERROR: Error conditions.
  414. * - TRIPAL_WARNING: Warning conditions.
  415. * - TRIPAL_NOTICE: Normal but significant conditions.
  416. * - TRIPAL_INFO: (default) Informational messages.
  417. * - TRIPAL_DEBUG: Debug-level messages.
  418. */
  419. public function logMessage($message, $variables = array(), $severity = TRIPAL_INFO) {
  420. // Generate a translated message.
  421. $tmessage = t($message, $variables);
  422. // Add this message to the job's log.
  423. $this->job->error_msg .= "\n" . $tmessage;
  424. // Report this message to watchdog or set a message.
  425. if ($severity == TRIPAL_CRITICAL or $severity == TRIPAL_ERROR) {
  426. tripal_report_error('tripal_job', $severity, $message, $variables);
  427. }
  428. }
  429. }