3 * Class for managing forking command line scripts.
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.
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.
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
22 use MediaWiki\MediaWikiServices
;
25 * Class for managing forking command line scripts.
26 * Currently just does forking and process control, but it could easily be extended
27 * to provide IPC and job dispatch.
29 * This class requires the posix and pcntl extensions.
31 * @ingroup Maintenance
33 class ForkController
{
34 protected $children = [], $childNumber = 0;
35 protected $termReceived = false;
36 protected $flags = 0, $procsToStart = 0;
38 protected static $restartableSignals = [
51 * Pass this flag to __construct() to cause the class to automatically restart
52 * workers that exit with non-zero exit status or a signal such as SIGSEGV.
54 const RESTART_ON_ERROR
= 1;
56 public function __construct( $numProcs, $flags = 0 ) {
57 if ( PHP_SAPI
!= 'cli' ) {
58 throw new MWException( "ForkController cannot be used from the web." );
60 $this->procsToStart
= $numProcs;
61 $this->flags
= $flags;
65 * Start the child processes.
67 * This should only be called from the command line. It should be called
68 * as early as possible during execution.
70 * This will return 'child' in the child processes. In the parent process,
71 * it will run until all the child processes exit or a TERM signal is
72 * received. It will then return 'done'.
75 public function start() {
77 pcntl_signal( SIGTERM
, [ $this, 'handleTermSignal' ], false );
80 // Start child processes
81 if ( $this->procsToStart
) {
82 if ( $this->forkWorkers( $this->procsToStart
) == 'child' ) {
85 $this->procsToStart
= 0;
90 $deadPid = pcntl_wait( $status );
93 // Respond to child process termination
94 unset( $this->children
[$deadPid] );
95 if ( $this->flags
& self
::RESTART_ON_ERROR
) {
96 if ( pcntl_wifsignaled( $status ) ) {
97 // Restart if the signal was abnormal termination
98 // Don't restart if it was deliberately killed
99 $signal = pcntl_wtermsig( $status );
100 if ( in_array( $signal, self
::$restartableSignals ) ) {
101 echo "Worker exited with signal $signal, restarting\n";
102 $this->procsToStart++
;
104 } elseif ( pcntl_wifexited( $status ) ) {
105 // Restart on non-zero exit status
106 $exitStatus = pcntl_wexitstatus( $status );
107 if ( $exitStatus != 0 ) {
108 echo "Worker exited with status $exitStatus, restarting\n";
109 $this->procsToStart++
;
111 echo "Worker exited normally\n";
116 if ( $this->procsToStart
) {
121 // Run signal handlers
122 if ( function_exists( 'pcntl_signal_dispatch' ) ) {
123 pcntl_signal_dispatch();
125 declare( ticks
= 1 ) {
129 // Respond to TERM signal
130 if ( $this->termReceived
) {
131 foreach ( $this->children
as $childPid => $unused ) {
132 posix_kill( $childPid, SIGTERM
);
134 $this->termReceived
= false;
136 } while ( count( $this->children
) );
137 pcntl_signal( SIGTERM
, SIG_DFL
);
142 * Get the number of the child currently running. Note, this
143 * is not the pid, but rather which of the total number of children
147 public function getChildNumber() {
148 return $this->childNumber
;
151 protected function prepareEnvironment() {
153 // Don't share DB, storage, or memcached connections
154 MediaWikiServices
::resetChildProcessServices();
155 FileBackendGroup
::destroySingleton();
156 LockManagerGroup
::destroySingletons();
157 JobQueueGroup
::destroySingletons();
158 ObjectCache
::clear();
159 RedisConnectionPool
::destroySingletons();
164 * Fork a number of worker processes.
166 * @param int $numProcs
169 protected function forkWorkers( $numProcs ) {
170 $this->prepareEnvironment();
172 // Create the child processes
173 for ( $i = 0; $i < $numProcs; $i++
) {
176 if ( $pid === -1 ||
$pid === false ) {
177 echo "Error creating child processes\n";
183 $this->childNumber
= $i;
186 // This is the parent process
187 $this->children
[$pid] = true;
194 protected function initChild() {
195 global $wgMemc, $wgMainCacheType;
196 $wgMemc = wfGetCache( $wgMainCacheType );
197 $this->children
= null;
198 pcntl_signal( SIGTERM
, SIG_DFL
);
201 protected function handleTermSignal( $signal ) {
202 $this->termReceived
= true;