You are here

background_process_ass.module in Background Process 7.2

@todo Implement admin interface. @todo Fix runtime check of running process.

File

background_process_ass/background_process_ass.module
View source
<?php

/**
 * @file
 *
 * @todo Implement admin interface.
 * @todo Fix runtime check of running process.
 */

/**
 * Default max age before unlock process.
 */
define('BACKGROUND_PROCESS_ASS_MAX_AGE', 30);

/**
 * Implements hook_menu().
 */
function background_process_ass_menu() {
  $items = array();
  $items['admin/config/system/background-process/ass'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Apache Server Status',
    'description' => 'Administer background process apache server status',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'background_process_ass_settings_form',
    ),
    'access arguments' => array(
      'administer background process',
    ),
    'file' => 'background_process_ass.admin.inc',
    'weight' => 3,
  );
  return $items;
}

/**
 * Implements hook_init().
 */
function background_process_ass_init() {

  // @todo Automatically determine max clients
  return;
  $cache = cache_get('background_process_ass_max_clients');
  if (!$cache) {
    $cache = (object) array(
      'data' => array(),
    );
    $service_hosts = background_process_get_service_hosts();
    foreach ($service_hosts as $name => $info) {
      $ass = $name . '_ass';
      if (!empty($service_hosts[$ass])) {

        // Get max clients from server status here ...

        #$cache->data[$name] = $max_clients;
      }
    }
  }
  global $conf;
  foreach ($cache->data as $service_host => $max_clients) {
    if (!isset($conf[$service_host]['max_clients'])) {
      $conf[$service_host]['max_clients'] = $max_clients;
    }
  }
}

/**
 * Implements hook_cron().
 */
function background_process_ass_cron() {

  // Don't use more than 30 seconds to unlock
  set_time_limit(30);
  background_process_ass_auto_unlock();
}

/**
 * Implements hook_cronapi().
 */
function background_process_ass_cronapi() {
  $items = array();
  $items['background_process_ass_cron'] = array(
    'description' => t('Unlock dead processes'),
    'rules' => array(
      '* * * * *',
    ),
    'configure' => 'admin/config/system/background-process/ass',
  );
  return $items;
}

/** 
 * Implements hook_cron_schedule_alter().
 */
function background_process_ass_cron_schedule_alter(&$items) {

  // We need to unlock ourselves if we're stale (chicken and the egg...)
  if (!isset($items['background_process_ass_cron'])) {
    $hooks = ultimate_cron_get_hooks();
    if (isset($hooks['background_process_ass_cron']['background_process'])) {
      $process =& $hooks['background_process_ass_cron']['background_process'];

      // Self unlock if too old ...
      if ($process
        ->getStartTime() + 120 < time()) {
        $process
          ->log(t('Self unlocking stale cleanup job'))
          ->unlock();
        $process = NULL;
      }
    }
  }

  // Put our cron job first in the list.
  if (isset($items['background_process_ass_cron'])) {
    $org_items = $items;
    $items = array();
    $items['background_process_ass_cron'] = $org_items['background_process_ass_cron'];
    $items += $org_items;
  }
}

/**
 * Implements hook_service_group().
 */
function background_process_ass_service_group() {
  $info = array();
  $info['methods']['background_process_ass_service_group_idle'] = t('Idle workers');
  return $info;
}

/**
 * Determine host with most idle workers and claim it.
 *
 * @param $service_group
 *   Service group to check
 * @return
 *   Claimed service host on success, NULL if none found
 */
function background_process_ass_service_group_idle($service_group, $reload = FALSE) {
  $result = NULL;
  $max = 0;
  $msg = "";
  $workers =& drupal_static('background_process_ass_idle_workers', array());

  // Load idle worker status for all hosts
  foreach ($service_group['hosts'] as $idx => $host) {
    $name = $host . '_ass';
    if ($reload || !isset($workers[$name])) {
      $workers[$name] = background_process_ass_get_server_status($name, TRUE, $reload);
    }

    // Reload apache server status for all hosts, if any is fully loaded
    if ($workers[$name] <= 0 && !$reload) {
      return background_process_ass_service_group_idle($service_group, TRUE);
    }
    if ($max < $workers[$name]) {
      $result = $host;
      $max = $workers[$name];
    }
  }
  if (isset($result)) {

    // Claim host and tell caller
    $workers[$result . '_ass']--;
    return $result;
  }
  else {

    // Could not determine most idle host, fallback to random
    return background_process_service_group_random($service_group);
  }
}

/**
 * Unlock locked processes that aren't really running.
 */
function background_process_ass_auto_unlock() {
  $processes = BackgroundProcess::loadAll();
  $service_hosts = background_process_get_service_hosts();
  foreach ($processes as $process) {

    // Don't even dare try determining state, if not properly configured.
    if (!$process->service_host) {
      continue;
    }

    // Naming convention suffix "_ass" for a given service hosts defines the
    // host to use for server-status.
    if (!isset($service_hosts[$process->service_host . '_ass'])) {
      continue;
    }
    if (!isset($service_hosts[$process->service_host])) {
      continue;
    }
    list($url, $headers) = background_process_build_request('bgp-start/' . $process->pid, $process->service_host);
    $process->http_host = $headers['Host'];

    // Locate our connection
    $url = parse_url($url);
    $path = $url['path'] . (isset($url['query']) ? '?' . $url['query'] : '');
    if (strlen("POST {$path}") > 64) {

      // Request is larger than 64 characters, which is the max length of
      // requests in the extended Apache Server Status. We cannot determine
      // if it's running or not ... skip this process!
      continue;
    }
    if ($process
      ->getStatus() != BACKGROUND_PROCESS_STATUS_RUNNING) {

      // Not ready for unlock yet
      continue;
    }
    if ($process
      ->getStartTime() > time() - variable_get('background_process_ass_max_age', BACKGROUND_PROCESS_ASS_MAX_AGE)) {

      // Not ready for unlock yet
      continue;
    }
    $server_status = background_process_ass_get_server_status($process->service_host . '_ass');
    if ($server_status) {
      if (!background_process_ass_check_process($process, $server_status, $path)) {
        _background_process_ass_unlock($process);
      }
    }
  }
}

/**
 * Check if process is really running.
 *
 * @param $process
 *   Process object
 * @param $server_status
 *   Server status data
 * @return boolean
 *   TRUE if running, FALSE if not.
 */
function background_process_ass_check_process($process, $server_status, $path) {
  $active = TRUE;

  // Is status reliable?
  if ($server_status && $server_status['status']['Current Timestamp'] > $process
    ->getStartTime()) {

    // Check if process is in the server status
    if (!empty($server_status['connections'])) {
      $active = FALSE;
      foreach ($server_status['connections'] as $conn) {
        if ($conn['M'] == 'R') {

          // We cannot rely on the server status, assume connection is still
          // active, and bail out.
          watchdog('bg_process', 'Found reading state ...', array(), WATCHDOG_WARNING);
          $active = TRUE;
          break;
        }

        // Empty connections, skip them
        if ($conn['M'] == '.' || $conn['M'] == '_') {
          continue;
        }
        if ($conn['VHost'] == $process->http_host && strpos($conn['Request'], 'POST ' . $path) === 0) {
          $active = TRUE;
          break;
        }
      }
    }
  }
  return $active;
}
function _background_process_ass_unlock($process) {
  if ($process
    ->getStatus() == BACKGROUND_PROCESS_STATUS_RUNNING) {
    $msg = t('Died unexpectedly (auto unlock due to missing connection)');

    // Unlock the process
    if ($process
      ->log($msg)
      ->unlock()) {
      watchdog('bg_process_ass', 'Unlocked @handle', array(
        '@handle' => $process
          ->getHandle(),
      ), WATCHDOG_INFO);
    }
  }
}

/**
 * Get apache extended server status.
 *
 * @staticvar $server_status
 *   Cached statically to avoid multiple requests to server-status.
 * @param $name
 *   Name of service host for server-status.
 * @param $auto
 *   Load only idle workers, not entire server status.
 * @param $reload
 *   Don't load from cache.
 * @return array
 *   Server status data.
 */
function background_process_ass_get_server_status($name, $auto = FALSE, $reload = FALSE) {

  // Sanity check ...
  if (!$name) {
    return;
  }
  $service_hosts = variable_get('background_process_service_hosts', array());
  if (empty($service_hosts[$name])) {
    return;
  }
  $service_host = $service_hosts[$name];

  // Static caching.
  $cache =& drupal_static('background_process_ass_server_status', array());
  if (!$reload && isset($cache[$name][$auto])) {
    return $cache[$name][$auto];
  }
  $server_status = array();
  $options = array();
  if ($auto) {
    $options['query']['auto'] = 1;
  }
  list($url, $headers) = background_process_build_request('', $name, $options);
  $timestamp = time();
  $response = drupal_http_request($url, array(
    'headers' => $headers,
  ));
  if ($response->code != 200) {
    watchdog('bg_process', 'Could not acquire server status from %url - error: %error', array(
      '%url' => $url,
      '%error' => $response->error,
    ), WATCHDOG_ERROR);
    return NULL;
  }

  // If "auto" only collect idle workers
  if ($auto) {
    preg_match('/IdleWorkers:\\s+(\\d+)/', $response->data, $matches);
    $server_status = $matches[1];
  }
  else {
    $tables = _background_process_ass_parse_table($response->data);
    $dls = _background_process_ass_parse_definition_list($response->data);
    $server_status = array(
      'response' => $response,
      'connections' => $tables[0],
      'status' => $dls[1],
    );
    preg_match('/.*?,\\s+(\\d+-.*?-\\d+\\s+\\d+:\\d+:\\d+)/', $server_status['status']['Restart Time'], $matches);

    // @hack Convert monthly names from Danish to English for strtotime() to work
    str_replace('Maj', 'May', $matches[1]);
    str_replace('May', 'Oct', $matches[1]);
    $server_status['status']['Restart Timestamp'] = strtotime($matches[1]);
    $server_status['status']['Current Timestamp'] = $timestamp;
  }
  $cache[$name][$auto] = $server_status;
  return $server_status;
}

/**
 * Converts an HTML table into an associative array.
 *
 * @param $html
 *   HTML containing table.
 * @return array
 *   Table data.
 */
function _background_process_ass_parse_table($html) {

  // Find the table
  preg_match_all("/<table.*?>.*?<\\/[\\s]*table>/s", $html, $table_htmls);
  $tables = array();
  foreach ($table_htmls[0] as $table_html) {

    // Get title for each row
    preg_match_all("/<th.*?>(.*?)<\\/[\\s]*th>/s", $table_html, $matches);
    $row_headers = $matches[1];

    // Iterate each row
    preg_match_all("/<tr.*?>(.*?)<\\/[\\s]*tr>/s", $table_html, $matches);
    $table = array();
    foreach ($matches[1] as $row_html) {
      $row_html = preg_replace("/\r|\n/", '', $row_html);
      preg_match_all("/<td.*?>(.*?)<\\/[\\s]*td>/", $row_html, $td_matches);
      $row = array();
      for ($i = 0; $i < count($td_matches[1]); $i++) {
        $td = strip_tags(html_entity_decode($td_matches[1][$i]));
        $i2 = isset($row_headers[$i]) ? $row_headers[$i] : $i;
        $row[$i2] = $td;
      }
      if (count($row) > 0) {
        $table[] = $row;
      }
    }
    $tables[] = $table;
  }
  return $tables;
}

/**
 * Converts an HTML table into an associative array.
 *
 * @param $html
 *   HTML containing table.
 * @return array
 *   Table data.
 */
function _background_process_ass_parse_definition_list($html) {

  // Find the table
  preg_match_all("/<dl.*?>.*?<\\/[\\s]*dl>/s", $html, $dl_htmls);
  $dls = array();
  foreach ($dl_htmls[0] as $dl_html) {

    // Get title for each row
    preg_match_all("/<dl.*?>(.*?)<\\/[\\s]*dl>/s", $dl_html, $matches);
    $dl = array();
    foreach ($matches[1] as $row_html) {
      $row_html = preg_replace("/\r|\n/", '', $row_html);
      preg_match_all("/<dt.*?>(.*?)<\\/[\\s]*dt>/", $row_html, $dt_matches);
      $row = array();
      for ($i = 0; $i < count($dt_matches[1]); $i++) {
        $dt = strip_tags(html_entity_decode($dt_matches[1][$i]));
        if (strpos($dt, ':') !== FALSE) {
          list($key, $value) = explode(': ', $dt, 2);
          $dl[$key] = $value;
        }
      }
    }
    $dls[] = $dl;
  }
  return $dls;
}

Functions

Namesort descending Description
background_process_ass_auto_unlock Unlock locked processes that aren't really running.
background_process_ass_check_process Check if process is really running.
background_process_ass_cron Implements hook_cron().
background_process_ass_cronapi Implements hook_cronapi().
background_process_ass_cron_schedule_alter Implements hook_cron_schedule_alter().
background_process_ass_get_server_status Get apache extended server status.
background_process_ass_init Implements hook_init().
background_process_ass_menu Implements hook_menu().
background_process_ass_service_group Implements hook_service_group().
background_process_ass_service_group_idle Determine host with most idle workers and claim it.
_background_process_ass_parse_definition_list Converts an HTML table into an associative array.
_background_process_ass_parse_table Converts an HTML table into an associative array.
_background_process_ass_unlock

Constants

Namesort descending Description
BACKGROUND_PROCESS_ASS_MAX_AGE Default max age before unlock process.