TripalJob.inc 16 KB

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