ForkController.php 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. <?php
  2. /**
  3. * Class for managing forking command line scripts.
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. */
  22. use MediaWiki\MediaWikiServices;
  23. /**
  24. * Class for managing forking command line scripts.
  25. * Currently just does forking and process control, but it could easily be extended
  26. * to provide IPC and job dispatch.
  27. *
  28. * This class requires the posix and pcntl extensions.
  29. *
  30. * @ingroup Maintenance
  31. */
  32. class ForkController {
  33. protected $children = [], $childNumber = 0;
  34. protected $termReceived = false;
  35. protected $flags = 0, $procsToStart = 0;
  36. protected static $restartableSignals = [
  37. SIGFPE,
  38. SIGILL,
  39. SIGSEGV,
  40. SIGBUS,
  41. SIGABRT,
  42. SIGSYS,
  43. SIGPIPE,
  44. SIGXCPU,
  45. SIGXFSZ,
  46. ];
  47. /**
  48. * Pass this flag to __construct() to cause the class to automatically restart
  49. * workers that exit with non-zero exit status or a signal such as SIGSEGV.
  50. */
  51. const RESTART_ON_ERROR = 1;
  52. public function __construct( $numProcs, $flags = 0 ) {
  53. if ( !wfIsCLI() ) {
  54. throw new MWException( "ForkController cannot be used from the web." );
  55. }
  56. $this->procsToStart = $numProcs;
  57. $this->flags = $flags;
  58. }
  59. /**
  60. * Start the child processes.
  61. *
  62. * This should only be called from the command line. It should be called
  63. * as early as possible during execution.
  64. *
  65. * This will return 'child' in the child processes. In the parent process,
  66. * it will run until all the child processes exit or a TERM signal is
  67. * received. It will then return 'done'.
  68. * @return string
  69. */
  70. public function start() {
  71. // Trap SIGTERM
  72. pcntl_signal( SIGTERM, [ $this, 'handleTermSignal' ], false );
  73. do {
  74. // Start child processes
  75. if ( $this->procsToStart ) {
  76. if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) {
  77. return 'child';
  78. }
  79. $this->procsToStart = 0;
  80. }
  81. // Check child status
  82. $status = false;
  83. $deadPid = pcntl_wait( $status );
  84. if ( $deadPid > 0 ) {
  85. // Respond to child process termination
  86. unset( $this->children[$deadPid] );
  87. if ( $this->flags & self::RESTART_ON_ERROR ) {
  88. if ( pcntl_wifsignaled( $status ) ) {
  89. // Restart if the signal was abnormal termination
  90. // Don't restart if it was deliberately killed
  91. $signal = pcntl_wtermsig( $status );
  92. if ( in_array( $signal, self::$restartableSignals ) ) {
  93. echo "Worker exited with signal $signal, restarting\n";
  94. $this->procsToStart++;
  95. }
  96. } elseif ( pcntl_wifexited( $status ) ) {
  97. // Restart on non-zero exit status
  98. $exitStatus = pcntl_wexitstatus( $status );
  99. if ( $exitStatus != 0 ) {
  100. echo "Worker exited with status $exitStatus, restarting\n";
  101. $this->procsToStart++;
  102. } else {
  103. echo "Worker exited normally\n";
  104. }
  105. }
  106. }
  107. // Throttle restarts
  108. if ( $this->procsToStart ) {
  109. usleep( 500000 );
  110. }
  111. }
  112. // Run signal handlers
  113. if ( function_exists( 'pcntl_signal_dispatch' ) ) {
  114. pcntl_signal_dispatch();
  115. } else {
  116. declare( ticks = 1 ) {
  117. // @phan-suppress-next-line PhanPluginDuplicateExpressionAssignment
  118. $status = $status;
  119. }
  120. }
  121. // Respond to TERM signal
  122. if ( $this->termReceived ) {
  123. foreach ( $this->children as $childPid => $unused ) {
  124. posix_kill( $childPid, SIGTERM );
  125. }
  126. $this->termReceived = false;
  127. }
  128. } while ( count( $this->children ) );
  129. pcntl_signal( SIGTERM, SIG_DFL );
  130. return 'done';
  131. }
  132. /**
  133. * Get the number of the child currently running. Note, this
  134. * is not the pid, but rather which of the total number of children
  135. * we are
  136. * @return int
  137. */
  138. public function getChildNumber() {
  139. return $this->childNumber;
  140. }
  141. protected function prepareEnvironment() {
  142. global $wgMemc;
  143. // Don't share DB, storage, or memcached connections
  144. MediaWikiServices::resetChildProcessServices();
  145. FileBackendGroup::destroySingleton();
  146. JobQueueGroup::destroySingletons();
  147. ObjectCache::clear();
  148. RedisConnectionPool::destroySingletons();
  149. $wgMemc = null;
  150. }
  151. /**
  152. * Fork a number of worker processes.
  153. *
  154. * @param int $numProcs
  155. * @return string
  156. */
  157. protected function forkWorkers( $numProcs ) {
  158. $this->prepareEnvironment();
  159. // Create the child processes
  160. for ( $i = 0; $i < $numProcs; $i++ ) {
  161. // Do the fork
  162. $pid = pcntl_fork();
  163. if ( $pid === -1 || $pid === false ) {
  164. echo "Error creating child processes\n";
  165. exit( 1 );
  166. }
  167. if ( !$pid ) {
  168. $this->initChild();
  169. $this->childNumber = $i;
  170. return 'child';
  171. } else {
  172. // This is the parent process
  173. $this->children[$pid] = true;
  174. }
  175. }
  176. return 'parent';
  177. }
  178. protected function initChild() {
  179. global $wgMemc, $wgMainCacheType;
  180. $wgMemc = wfGetCache( $wgMainCacheType );
  181. $this->children = null;
  182. pcntl_signal( SIGTERM, SIG_DFL );
  183. }
  184. protected function handleTermSignal( $signal ) {
  185. $this->termReceived = true;
  186. }
  187. }