background_process_ass.module 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. <?php
  2. /**
  3. * @file
  4. *
  5. * @todo Implement admin interface.
  6. * @todo Fix runtime check of running process.
  7. */
  8. /**
  9. * Default max age before unlock process.
  10. */
  11. define('BACKGROUND_PROCESS_ASS_MAX_AGE', 30);
  12. /**
  13. * Implements hook_menu().
  14. */
  15. function background_process_ass_menu() {
  16. $items = array();
  17. $items['admin/config/system/background-process/ass'] = array(
  18. 'type' => MENU_LOCAL_TASK,
  19. 'title' => 'Apache Server Status',
  20. 'description' => 'Administer background process apache server status',
  21. 'page callback' => 'drupal_get_form',
  22. 'page arguments' => array('background_process_ass_settings_form'),
  23. 'access arguments' => array('administer background process'),
  24. 'file' => 'background_process_ass.admin.inc',
  25. 'weight' => 3,
  26. );
  27. return $items;
  28. }
  29. /**
  30. * Implements hook_cron().
  31. */
  32. function background_process_ass_cron() {
  33. // Don't use more than 30 seconds to unlock
  34. @set_time_limit(30);
  35. background_process_ass_auto_unlock();
  36. }
  37. /**
  38. * Implements hook_cronapi().
  39. */
  40. function background_process_ass_cronapi($op, $job = NULL) {
  41. switch ($op) {
  42. case 'list':
  43. return array('background_process_ass_cron' => t('Unlock dead processes'));
  44. case 'rule':
  45. return '* * * * *';
  46. case 'configure':
  47. return 'admin/config/system/background-process/ass';
  48. }
  49. }
  50. /**
  51. * Implements hook_cron_alter().
  52. */
  53. function background_process_ass_cron_alter(&$items) {
  54. $items['background_process_ass_cron']['override_congestion_protection'] = TRUE;
  55. // Unlock background if too old.
  56. // @todo Move to some access handler or pre-execute?
  57. if ($process = background_process_get_process('uc:background_process_ass_cron')) {
  58. if ($process->start + 30 < time()) {
  59. background_process_unlock($process->handle, t('Self unlocking stale lock'));
  60. }
  61. }
  62. }
  63. /**
  64. * Implements hook_service_group().
  65. */
  66. function background_process_ass_service_group() {
  67. $info = array();
  68. $info['methods']['background_process_ass_service_group_idle'] = t('Idle workers');
  69. return $info;
  70. }
  71. /**
  72. * Determine host with most idle workers and claim it.
  73. *
  74. * @param $service_group
  75. * Service group to check
  76. * @return
  77. * Claimed service host on success, NULL if none found
  78. */
  79. function background_process_ass_service_group_idle($service_group, $reload = FALSE) {
  80. $result = NULL;
  81. $max = 0;
  82. $msg = "";
  83. $workers = &drupal_static('background_process_ass_idle_workers', array());
  84. // Load idle worker status for all hosts
  85. foreach ($service_group['hosts'] as $idx => $host) {
  86. $name = $host . '_ass';
  87. if ($reload || !isset($workers[$name])) {
  88. $workers[$name] = background_process_ass_get_server_status($name, TRUE, $reload);
  89. }
  90. // Reload apache server status for all hosts, if any is fully loaded
  91. if ($workers[$name] <= 0 && !$reload) {
  92. return background_process_ass_service_group_idle($service_group, TRUE);
  93. }
  94. if ($max < $workers[$name]) {
  95. $result = $host;
  96. $max = $workers[$name];
  97. }
  98. }
  99. if (isset($result)) {
  100. // Claim host and tell caller
  101. $workers[$result . '_ass']--;
  102. return $result;
  103. }
  104. else {
  105. // Could not determine most idle host, fallback to pseudo round robin
  106. return background_process_service_group_round_robin($service_group);
  107. }
  108. }
  109. /**
  110. * Unlock locked processes that aren't really running.
  111. */
  112. function background_process_ass_auto_unlock() {
  113. $processes = background_process_get_processes();
  114. $service_hosts = background_process_get_service_hosts();
  115. foreach ($processes as $process) {
  116. // Don't even dare try determining state, if not properly configured.
  117. if (!$process->service_host) {
  118. continue;
  119. }
  120. // Naming convention suffix "_ass" for a given service hosts defines the
  121. // host to use for server-status.
  122. if (!isset($service_hosts[$process->service_host . '_ass'])) {
  123. continue;
  124. }
  125. if (!isset($service_hosts[$process->service_host])) {
  126. continue;
  127. }
  128. list($url, $headers) = background_process_build_request('bgp-start/' . rawurlencode($process->handle), $process->service_host);
  129. $process->http_host = $headers['Host'];
  130. // Locate our connection
  131. $url = parse_url($url);
  132. $path = $url['path'] . (isset($url['query']) ? '?' . $url['query'] : '');
  133. if (strlen("POST $path") > 64) {
  134. // Request is larger than 64 characters, which is the max length of
  135. // requests in the extended Apache Server Status. We cannot determine
  136. // if it's running or not ... skip this process!
  137. continue;
  138. }
  139. if ($process->status != BACKGROUND_PROCESS_STATUS_RUNNING) {
  140. // Not ready for unlock yet
  141. continue;
  142. }
  143. if ($process->start > time() - variable_get('background_process_ass_max_age', BACKGROUND_PROCESS_ASS_MAX_AGE)) {
  144. // Not ready for unlock yet
  145. continue;
  146. }
  147. $server_status = background_process_ass_get_server_status($process->service_host . '_ass');
  148. if ($server_status) {
  149. if (!background_process_ass_check_process($process, $server_status, $path)) {
  150. _background_process_ass_unlock($process);
  151. }
  152. }
  153. }
  154. }
  155. /**
  156. * Check if process is really running.
  157. *
  158. * @param $process
  159. * Process object
  160. * @param $server_status
  161. * Server status data
  162. * @return boolean
  163. * TRUE if running, FALSE if not.
  164. */
  165. function background_process_ass_check_process($process, $server_status, $path) {
  166. $active = TRUE;
  167. // Is status reliable?
  168. if ($server_status && $server_status['status']['Current Timestamp'] > $process->start) {
  169. // Check if process is in the server status
  170. if (!empty($server_status['connections'])) {
  171. $active = FALSE;
  172. foreach ($server_status['connections'] as $conn) {
  173. if ($conn['M'] == 'R') {
  174. // We cannot rely on the server status, assume connection is still
  175. // active, and bail out.
  176. watchdog('bg_process', 'Found reading state ...', array(), WATCHDOG_WARNING);
  177. $active = TRUE;
  178. break;
  179. }
  180. // Empty connections, skip them
  181. if ($conn['M'] == '.' || $conn['M'] == '_') {
  182. continue;
  183. }
  184. if (
  185. $conn['VHost'] == $process->http_host &&
  186. strpos($conn['Request'], 'POST ' . $path) === 0
  187. ) {
  188. $active = TRUE;
  189. break;
  190. }
  191. }
  192. }
  193. }
  194. return $active;
  195. }
  196. function _background_process_ass_unlock($process) {
  197. watchdog('bg_process', 'Unlocking: ' . $process->handle);
  198. if ($process->status == BACKGROUND_PROCESS_STATUS_RUNNING) {
  199. $msg = t('Died unexpectedly (auto unlock due to missing connection)');
  200. // Unlock the process
  201. if (background_process_unlock($process->handle, $msg, $process->start)) {
  202. drupal_set_message(t("%handle unlocked: !msg", array('%handle' => $process->handle, '!msg' => $msg)));
  203. }
  204. }
  205. }
  206. /**
  207. * Get apache extended server status.
  208. *
  209. * @staticvar $server_status
  210. * Cached statically to avoid multiple requests to server-status.
  211. * @param $name
  212. * Name of service host for server-status.
  213. * @param $auto
  214. * Load only idle workers, not entire server status.
  215. * @param $reload
  216. * Don't load from cache.
  217. * @return array
  218. * Server status data.
  219. */
  220. function background_process_ass_get_server_status($name, $auto = FALSE, $reload = FALSE) {
  221. // Sanity check ...
  222. if (!$name) {
  223. return;
  224. }
  225. $service_hosts = variable_get('background_process_service_hosts', array());
  226. if (empty($service_hosts[$name])) {
  227. return;
  228. }
  229. $service_host = $service_hosts[$name];
  230. // Static caching.
  231. $cache = &drupal_static('background_process_ass_server_status', array());
  232. if (!$reload && isset($cache[$name][$auto])) {
  233. return $cache[$name][$auto];
  234. }
  235. $server_status = array();
  236. $options = array();
  237. if ($auto) {
  238. $options['query']['auto'] = 1;
  239. }
  240. list($url, $headers) = background_process_build_request('', $name, $options);
  241. $timestamp = time();
  242. $response = drupal_http_request($url, array('headers' => $headers));
  243. if ($response->code != 200) {
  244. watchdog('bg_process', 'Could not acquire server status from %url - error: %error', array('%url' => $url, '%error' => $response->error), WATCHDOG_ERROR);
  245. return NULL;
  246. }
  247. // If "auto" only collect idle workers
  248. if ($auto) {
  249. preg_match('/IdleWorkers:\s+(\d+)/', $response->data, $matches);
  250. $server_status = $matches[1];
  251. }
  252. else {
  253. $tables = _background_process_ass_parse_table($response->data);
  254. $dls = _background_process_ass_parse_definition_list($response->data);
  255. $server_status = array(
  256. 'response' => $response,
  257. 'connections' => $tables[0],
  258. 'status' => $dls[1],
  259. );
  260. preg_match('/.*?,\s+(\d+-.*?-\d+\s+\d+:\d+:\d+)/', $server_status['status']['Restart Time'], $matches);
  261. // @hack Convert monthly names from Danish to English for strtotime() to work
  262. str_replace('Maj', 'May', $matches[1]);
  263. str_replace('May', 'Oct', $matches[1]);
  264. $server_status['status']['Restart Timestamp'] = strtotime($matches[1]);
  265. $server_status['status']['Current Timestamp'] = $timestamp;
  266. }
  267. $cache[$name][$auto] = $server_status;
  268. return $server_status;
  269. }
  270. /**
  271. * Converts an HTML table into an associative array.
  272. *
  273. * @param $html
  274. * HTML containing table.
  275. * @return array
  276. * Table data.
  277. */
  278. function _background_process_ass_parse_table($html) {
  279. // Find the table
  280. preg_match_all("/<table.*?>.*?<\/[\s]*table>/s", $html, $table_htmls);
  281. $tables = array();
  282. foreach ($table_htmls[0] as $table_html) {
  283. // Get title for each row
  284. preg_match_all("/<th.*?>(.*?)<\/[\s]*th>/s", $table_html, $matches);
  285. $row_headers = $matches[1];
  286. // Iterate each row
  287. preg_match_all("/<tr.*?>(.*?)<\/[\s]*tr>/s", $table_html, $matches);
  288. $table = array();
  289. foreach ($matches[1] as $row_html) {
  290. $row_html = preg_replace("/\r|\n/", '', $row_html);
  291. preg_match_all("/<td.*?>(.*?)<\/[\s]*td>/", $row_html, $td_matches);
  292. $row = array();
  293. for ($i=0; $i<count($td_matches[1]); $i++) {
  294. $td = strip_tags(html_entity_decode($td_matches[1][$i]));
  295. $i2 = isset($row_headers[$i]) ? $row_headers[$i] : $i;
  296. $row[$i2] = $td;
  297. }
  298. if (count($row) > 0) {
  299. $table[] = $row;
  300. }
  301. }
  302. $tables[] = $table;
  303. }
  304. return $tables;
  305. }
  306. /**
  307. * Converts an HTML table into an associative array.
  308. *
  309. * @param $html
  310. * HTML containing table.
  311. * @return array
  312. * Table data.
  313. */
  314. function _background_process_ass_parse_definition_list($html) {
  315. // Find the table
  316. preg_match_all("/<dl.*?>.*?<\/[\s]*dl>/s", $html, $dl_htmls);
  317. $dls = array();
  318. foreach ($dl_htmls[0] as $dl_html) {
  319. // Get title for each row
  320. preg_match_all("/<dl.*?>(.*?)<\/[\s]*dl>/s", $dl_html, $matches);
  321. $dl = array();
  322. foreach ($matches[1] as $row_html) {
  323. $row_html = preg_replace("/\r|\n/", '', $row_html);
  324. preg_match_all("/<dt.*?>(.*?)<\/[\s]*dt>/", $row_html, $dt_matches);
  325. $row = array();
  326. for ($i=0; $i<count($dt_matches[1]); $i++) {
  327. $dt = strip_tags(html_entity_decode($dt_matches[1][$i]));
  328. if (strpos($dt, ':') !== FALSE) {
  329. list($key, $value) = explode(': ', $dt, 2);
  330. $dl[$key] = $value;
  331. }
  332. }
  333. }
  334. $dls[] = $dl;
  335. }
  336. return $dls;
  337. }