1

When importing content using Feeds using an importer associated with a node type, the mapping can be configured to assign taxonomy terms from the Feed Node to the imported Feed Items. On a project, we need to extend this feature by also updating all the already imported Feed Items when a Feed Node is updated.

This can be done using hook_node_update($node). But since a single Feed Node can have a lot of Feed Items, they cannot be all updated in a single HTTP request otherwise we may encounter a timeout. The obvious way to avoid timeouts, is to use the Batch operations API.

Most of the time, hook_node_update() implementations will be executed because node_save has been called from a form submit handler, therefore, most of the time, there is no need to call batch_process from a hook_node_update() since it will be called automatically at the end of the form processing. Calling batch_progress from hook_node_update() may also produce unexpected result since it will prevent other implementation of the same hook or others hooks to be executed.

But what about the times when the hook_node_update() implementations are not executed from a form submit handler. For instance, when node_save is called from an implementation of hook_cron? Is there a way to detect these cases to either run all the update at once or delay them to a dedicated queue?

1

As pointed in kiamlaluno's answer, there is no execution context information passed to hook_node_update() implementations. So we ended up using debug_backtrace() to look up for the currently processed form (if any). If a form is processed, drupal_process_form() will handle batch processing for us. If not, we can't safely start a progressive processing of the batch. But we can run the whole batch in a single pass. This can lead to timeout and should really be replaced. Either by delaying operations execution (queued operations) or by starting progressive processing of the batch after all update hook have been run (but how ?).

function _MODULE_get_currently_processed_form() {
  $backtrace = debug_backtrace();
  while ($frame = next($backtrace)) {
    if (isset($frame['function']) && $frame['function'] == 'drupal_process_form') {
      return array(
        'form_id' => $frame['args'][0],
        'form' => &$frame['args'][1],
        'form_state' => &$frame['args'][2],
      );
    }
  }
  return FALSE;
}

function MODULE_node_update($node) {
  if (some_condition_on($node)) {
    $batch = array(
      'operations' => array(
        // ...
      ),
    );
    batch_set($batch);
    $currently_processed_form = _MODULE_get_currently_processed_form();
    if (!$currently_processed_form) {
      // We are not currently processing a form, the batch processing won't be
      // automatically started. But we cannot start processing it because it
      // will end the request and prevent any other post node update code to
      // run. So we process the batch in a single pass and hope for the best.
      // FIXME: Start progressive processing of the batch after node update
      // processing (using hook_exit?) or use other delayed execution (using
      // queued operation?)
      $batch =& batch_get();
      $batch['progressive'] = FALSE;
      batch_process();
    }
  }
}
0

In short, it is not possible to know which function invoked a hook implementation; a hook doesn't get an argument that, for example, is TRUE when the hook is called from a form submission handler and FALSE when the hook invocation is caused from another hook implementation.

Generally, it is better to start batch operations from a menu callback, not from a function that is invoked, for example, during cron tasks, where Drupal is not outputting a page to a browser. In fact, to avoid PHP timeouts, Drupal causes the browser to invoke the batch menu callback every X seconds; if it is not involved a browser (as it happens with cron tasks executed from a cron utility), then the batch operations don't work, or work as a normal function call that could cause PHP timeouts.
This rules out the possibility to use batch operation in your case.

If you really needs to use batch operations, then you should do what done by Drupal core code. When the module needs to use batch operations, it reports that in a hook_requirements() implementation which gives to the user the link of a page that starts the batch operations. Alternatively, a similar warning message can be placed in an administrative settings page; also in this case, the user will start the batch operations clicking on a link provided in the message.

What I reported before about batch operations is still valid: batch operations rely on the browser. If there are connection problems with the server, or the browser crashes, the batch operations will be interrupted; that is what also happens if the computer running the browser freezes, or it switches off/resets for any cause.
Batch operations should be used for relatively long operations, but not operations that last 20-30 or more minutes.

  • I known how to handle long running operation happening outside a standard page request (using the Queue API). Relaying on a debug function to decide which solution (Batch vs. Queue) seems ugly to me. Detecting my non-processed batch operations at the end of a request/process to turn them into queued ones seems better. I'm not sure how to achieve this (hook_exit?). – Pierre Buyle Apr 27 '11 at 6:29
  • @mongolito404 What I mean is that a hook doesn't get an argument that tells it which function called it; therefore, hook_node_update() will not know when it is called from node_save(), which is then called from some hook_cron() implementations. The second part of my answer rules out using batch operations outside a menu callback/form submission handler. – kiamlaluno Apr 27 '11 at 11:35

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.