background_batch.module 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377
  1. <?php
  2. /**
  3. * @file
  4. * This module adds background processing to Drupals batch API
  5. *
  6. * @todo Add option to stop a running batch job.
  7. */
  8. /**
  9. * Default value for delay (in microseconds).
  10. */
  11. define('BACKGROUND_BATCH_DELAY', 1000000);
  12. /**
  13. * Default value for process lifespan (in miliseconds).
  14. */
  15. define('BACKGROUND_BATCH_PROCESS_LIFESPAN', 10000);
  16. /**
  17. * Default value wether ETA information should be shown.
  18. */
  19. define('BACKGROUND_BATCH_PROCESS_ETA', TRUE);
  20. /**
  21. * Implements hook_menu().
  22. */
  23. function background_batch_menu() {
  24. $items = array();
  25. $items['admin/config/system/batch/settings'] = array(
  26. 'type' => MENU_DEFAULT_LOCAL_TASK,
  27. 'title' => 'Settings',
  28. 'weight' => 1,
  29. );
  30. $items['admin/config/system/batch'] = array(
  31. 'title' => 'Batch',
  32. 'description' => 'Administer batch jobs',
  33. 'page callback' => 'drupal_get_form',
  34. 'page arguments' => array('background_batch_settings_form'),
  35. 'access arguments' => array('administer site'),
  36. 'file' => 'background_batch.pages.inc',
  37. );
  38. $items['admin/config/system/batch/overview'] = array(
  39. 'type' => MENU_LOCAL_TASK,
  40. 'title' => 'Overview',
  41. 'description' => 'Batch job overview',
  42. 'page callback' => 'background_batch_overview_page',
  43. 'access arguments' => array('administer site'),
  44. 'file' => 'background_batch.pages.inc',
  45. 'weight' => 3,
  46. );
  47. return $items;
  48. }
  49. /**
  50. * Implements hook_menu_alter().
  51. */
  52. function background_batch_menu_alter(&$items) {
  53. $items['batch'] = array(
  54. 'page callback' => 'background_batch_page',
  55. 'access callback' => TRUE,
  56. 'theme callback' => '_system_batch_theme',
  57. 'type' => MENU_CALLBACK,
  58. 'file' => 'background_batch.pages.inc',
  59. 'module' => 'background_batch',
  60. );
  61. }
  62. /**
  63. * Implements hook_batch_alter().
  64. * Steal the operation and hook into context data.
  65. */
  66. function background_batch_batch_alter(&$batch) {
  67. if ($batch['progressive'] && $batch['url'] == 'batch') {
  68. foreach ($batch['sets'] as &$set) {
  69. if (!empty($set['operations'])) {
  70. foreach ($set['operations'] as &$operation) {
  71. $operation = array('_background_batch_operation', array($operation));
  72. }
  73. }
  74. }
  75. $batch['timestamp'] = microtime(TRUE);
  76. }
  77. // In order to make this batch session independend we save the owner UID.
  78. global $user;
  79. $batch['uid'] = $user->uid;
  80. }
  81. /**
  82. * Implements hook_library().
  83. */
  84. function background_batch_library() {
  85. $libraries = array();
  86. $libraries['background-process.batch'] = array(
  87. 'title' => 'Background batch API',
  88. 'version' => '1.0.0',
  89. 'js' => array(
  90. drupal_get_path('module', 'background_batch') . '/js/batch.js' => array('group' => JS_DEFAULT, 'cache' => FALSE),
  91. ),
  92. 'dependencies' => array(
  93. array('background_batch', 'background-process.progress'),
  94. ),
  95. );
  96. $libraries['background-process.progress'] = array(
  97. 'title' => 'Background batch progress',
  98. 'version' => VERSION,
  99. 'js' => array(
  100. drupal_get_path('module', 'background_batch') . '/js/progress.js' => array('group' => JS_DEFAULT, 'cache' => FALSE),
  101. ),
  102. );
  103. return $libraries;
  104. }
  105. /**
  106. * Run a batch operation with "listening" context.
  107. * @param $operation
  108. * Batch operation definition.
  109. * @param &$context
  110. * Context for the batch operation.
  111. */
  112. function _background_batch_operation($operation, &$context) {
  113. // Steal context and trap finished variable
  114. $fine_progress = !empty($context['sandbox']['background_batch_fine_progress']);
  115. if ($fine_progress) {
  116. $batch_context = new BackgroundBatchContext($context);
  117. }
  118. else {
  119. $batch_context = $context;
  120. }
  121. // Call the original operation
  122. $operation[1][] = &$batch_context;
  123. call_user_func_array($operation[0], $operation[1]);
  124. if ($fine_progress) {
  125. // Transfer back context result to batch api
  126. $batch_context = (array)$batch_context;
  127. foreach (array_keys($batch_context) as $key) {
  128. $context[$key] = $batch_context[$key];
  129. }
  130. }
  131. else {
  132. $batch_context = new BackgroundBatchContext($context);
  133. $batch_context['finished'] = $context['finished'];
  134. }
  135. }
  136. /**
  137. * Process a batch step
  138. * @param type $id
  139. * @return type
  140. */
  141. function _background_batch_process($id = NULL) {
  142. if (!$id) {
  143. return;
  144. }
  145. // Retrieve the current state of batch from db.
  146. $data = db_query("SELECT batch FROM {batch} WHERE bid = :bid", array(':bid' => $id))->fetchColumn();
  147. if (!$data) {
  148. return;
  149. }
  150. require_once('includes/batch.inc');
  151. $batch =& batch_get();
  152. $batch = unserialize($data);
  153. // Check if the current user owns (has access to) this batch.
  154. global $user;
  155. if ($batch['uid'] != $user->uid) {
  156. return drupal_access_denied();
  157. }
  158. // Register database update for the end of processing.
  159. drupal_register_shutdown_function('_batch_shutdown');
  160. timer_start('background_batch_processing');
  161. $percentage = 0;
  162. $mem_max_used = 0;
  163. $mem_last_used = memory_get_usage();
  164. $mem_limit = ini_get('memory_limit');
  165. preg_match('/(\d+)(\w)/', $mem_limit, $matches);
  166. switch ($matches[2]) {
  167. case 'M':
  168. default:
  169. $mem_limit = $matches[1] * 1024 * 1024;
  170. break;
  171. }
  172. while ($percentage < 100) {
  173. list ($percentage, $message) = _batch_process();
  174. $mem_used = memory_get_usage();
  175. // If we memory usage of last run will exceed the memory limit in next run
  176. // then bail out
  177. if ($mem_limit < $mem_used + $mem_last_used) {
  178. break;
  179. }
  180. $mem_last_used = $mem_used - $mem_last_used;
  181. // If we maximum memory usage of previous runs will exceed the memory limit in next run
  182. // then bail out
  183. $mem_max_used = $mem_max_used < $mem_last_used ? $mem_last_used : $mem_max_used;
  184. if ($mem_limit < $mem_used + $mem_max_used) {
  185. break;
  186. }
  187. // Restart background process after X miliseconds
  188. if (timer_read('background_batch_processing') > variable_get('background_batch_process_lifespan', BACKGROUND_BATCH_PROCESS_LIFESPAN)) {
  189. break;
  190. }
  191. }
  192. if ($percentage < 100) {
  193. background_process_keepalive($id);
  194. }
  195. }
  196. /**
  197. * Processes the batch.
  198. *
  199. * Unless the batch has been marked with 'progressive' = FALSE, the function
  200. * issues a drupal_goto and thus ends page execution.
  201. *
  202. * This function is not needed in form submit handlers; Form API takes care
  203. * of batches that were set during form submission.
  204. *
  205. * @param $redirect
  206. * (optional) Path to redirect to when the batch has finished processing.
  207. * @param $url
  208. * (optional - should only be used for separate scripts like update.php)
  209. * URL of the batch processing page.
  210. */
  211. function background_batch_process_batch($redirect = NULL, $url = 'batch', $redirect_callback = 'drupal_goto') {
  212. $batch =& batch_get();
  213. drupal_theme_initialize();
  214. if (isset($batch)) {
  215. // Add process information
  216. $process_info = array(
  217. 'current_set' => 0,
  218. 'progressive' => TRUE,
  219. 'url' => $url,
  220. 'url_options' => array(),
  221. 'source_url' => $_GET['q'],
  222. 'redirect' => $redirect,
  223. 'theme' => $GLOBALS['theme_key'],
  224. 'redirect_callback' => $redirect_callback,
  225. );
  226. $batch += $process_info;
  227. // The batch is now completely built. Allow other modules to make changes
  228. // to the batch so that it is easier to reuse batch processes in other
  229. // environments.
  230. drupal_alter('batch', $batch);
  231. // Assign an arbitrary id: don't rely on a serial column in the 'batch'
  232. // table, since non-progressive batches skip database storage completely.
  233. $batch['id'] = db_next_id();
  234. // Move operations to a job queue. Non-progressive batches will use a
  235. // memory-based queue.
  236. foreach ($batch['sets'] as $key => $batch_set) {
  237. _batch_populate_queue($batch, $key);
  238. }
  239. // Initiate processing.
  240. // Now that we have a batch id, we can generate the redirection link in
  241. // the generic error message.
  242. $t = get_t();
  243. $batch['error_message'] = $t('Please continue to <a href="@error_url">the error page</a>', array('@error_url' => url($url, array('query' => array('id' => $batch['id'], 'op' => 'finished')))));
  244. // Clear the way for the drupal_goto() redirection to the batch processing
  245. // page, by saving and unsetting the 'destination', if there is any.
  246. if (isset($_GET['destination'])) {
  247. $batch['destination'] = $_GET['destination'];
  248. unset($_GET['destination']);
  249. }
  250. // Store the batch.
  251. db_insert('batch')
  252. ->fields(array(
  253. 'bid' => $batch['id'],
  254. 'timestamp' => REQUEST_TIME,
  255. 'token' => drupal_get_token($batch['id']),
  256. 'batch' => serialize($batch),
  257. ))
  258. ->execute();
  259. // Set the batch number in the session to guarantee that it will stay alive.
  260. $_SESSION['batches'][$batch['id']] = TRUE;
  261. // Redirect for processing.
  262. $function = $batch['redirect_callback'];
  263. if (function_exists($function)) {
  264. // $function($batch['url'], array('query' => array('op' => 'start', 'id' => $batch['id'])));
  265. }
  266. }
  267. background_process_start('_background_batch_process_callback', $batch);
  268. }
  269. function _background_batch_process_callback($batch) {
  270. $rbatch =& batch_get();
  271. $rbatch = $batch;
  272. require_once('background_batch.pages.inc');
  273. _background_batch_page_start();
  274. }
  275. /**
  276. * Class batch context.
  277. * Automatically updates progress when 'finished' index is changed.
  278. */
  279. class BackgroundBatchContext extends ArrayObject {
  280. private $batch = NULL;
  281. private $interval = NULL;
  282. private $progress = NULL;
  283. public function __construct() {
  284. $this->interval = variable_get('background_batch_delay', BACKGROUND_BATCH_DELAY) / 1000000;
  285. $args = func_get_args();
  286. return call_user_func_array(array('parent', '__construct'), $args);
  287. }
  288. /**
  289. * Set progress update interval in seconds (float).
  290. */
  291. public function setInterval($interval) {
  292. $this->interval = $interval;
  293. }
  294. /**
  295. * Override offsetSet().
  296. * Update progress if needed.
  297. */
  298. public function offsetSet($name, $value) {
  299. if ($name == 'finished') {
  300. if (!isset($this->batch)) {
  301. $this->batch =& batch_get();
  302. $this->progress = progress_get_progress('_background_batch:' . $this->batch['id']);
  303. }
  304. if ($this->batch) {
  305. $total = $this->batch['sets'][$this->batch['current_set']]['total'];
  306. $count = $this->batch['sets'][$this->batch['current_set']]['count'];
  307. $elapsed = $this->batch['sets'][$this->batch['current_set']]['elapsed'];
  308. $progress_message = $this->batch['sets'][$this->batch['current_set']]['progress_message'];
  309. $current = $total - $count;
  310. $step = 1 / $total;
  311. $base = $current * $step;
  312. $progress = $base + $value * $step;
  313. progress_estimate_completion($this->progress);
  314. $elapsed = floor($this->progress->current - $this->progress->start);
  315. $values = array(
  316. '@remaining' => $count,
  317. '@total' => $total,
  318. '@current' => $current,
  319. '@percentage' => $progress * 100,
  320. '@elapsed' => format_interval($elapsed),
  321. // If possible, estimate remaining processing time.
  322. '@estimate' => format_interval(floor($this->progress->estimate) - floor($this->progress->current)),
  323. );
  324. $message = strtr($progress_message, $values);
  325. $message .= $message && $this['message'] ? '<br/>' : '';
  326. $message .= $this['message'];
  327. progress_set_intervalled_progress('_background_batch:' . $this->batch['id'], $message ? $message : $this->progress->message, $progress, $this->interval);
  328. }
  329. }
  330. return parent::offsetSet($name, $value);
  331. }
  332. }