Merge "mw.Feedback: If the message is posted remotely, link the title correctly"
[lhc/web/wiklou.git] / includes / ForkController.php
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 /**
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.
28 *
29 * This class requires the posix and pcntl extensions.
30 *
31 * @ingroup Maintenance
32 */
33 class ForkController {
34 protected $children = [], $childNumber = 0;
35 protected $termReceived = false;
36 protected $flags = 0, $procsToStart = 0;
37
38 protected static $restartableSignals = [
39 SIGFPE,
40 SIGILL,
41 SIGSEGV,
42 SIGBUS,
43 SIGABRT,
44 SIGSYS,
45 SIGPIPE,
46 SIGXCPU,
47 SIGXFSZ,
48 ];
49
50 /**
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.
53 */
54 const RESTART_ON_ERROR = 1;
55
56 public function __construct( $numProcs, $flags = 0 ) {
57 if ( !wfIsCLI() ) {
58 throw new MWException( "ForkController cannot be used from the web." );
59 }
60 $this->procsToStart = $numProcs;
61 $this->flags = $flags;
62 }
63
64 /**
65 * Start the child processes.
66 *
67 * This should only be called from the command line. It should be called
68 * as early as possible during execution.
69 *
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'.
73 * @return string
74 */
75 public function start() {
76 // Trap SIGTERM
77 pcntl_signal( SIGTERM, [ $this, 'handleTermSignal' ], false );
78
79 do {
80 // Start child processes
81 if ( $this->procsToStart ) {
82 if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) {
83 return 'child';
84 }
85 $this->procsToStart = 0;
86 }
87
88 // Check child status
89 $status = false;
90 $deadPid = pcntl_wait( $status );
91
92 if ( $deadPid > 0 ) {
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++;
103 }
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++;
110 } else {
111 echo "Worker exited normally\n";
112 }
113 }
114 }
115 // Throttle restarts
116 if ( $this->procsToStart ) {
117 usleep( 500000 );
118 }
119 }
120
121 // Run signal handlers
122 if ( function_exists( 'pcntl_signal_dispatch' ) ) {
123 pcntl_signal_dispatch();
124 } else {
125 declare( ticks = 1 ) {
126 $status = $status;
127 }
128 }
129 // Respond to TERM signal
130 if ( $this->termReceived ) {
131 foreach ( $this->children as $childPid => $unused ) {
132 posix_kill( $childPid, SIGTERM );
133 }
134 $this->termReceived = false;
135 }
136 } while ( count( $this->children ) );
137 pcntl_signal( SIGTERM, SIG_DFL );
138 return 'done';
139 }
140
141 /**
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
144 * we are
145 * @return int
146 */
147 public function getChildNumber() {
148 return $this->childNumber;
149 }
150
151 protected function prepareEnvironment() {
152 global $wgMemc;
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();
160 $wgMemc = null;
161 }
162
163 /**
164 * Fork a number of worker processes.
165 *
166 * @param int $numProcs
167 * @return string
168 */
169 protected function forkWorkers( $numProcs ) {
170 $this->prepareEnvironment();
171
172 // Create the child processes
173 for ( $i = 0; $i < $numProcs; $i++ ) {
174 // Do the fork
175 $pid = pcntl_fork();
176 if ( $pid === -1 || $pid === false ) {
177 echo "Error creating child processes\n";
178 exit( 1 );
179 }
180
181 if ( !$pid ) {
182 $this->initChild();
183 $this->childNumber = $i;
184 return 'child';
185 } else {
186 // This is the parent process
187 $this->children[$pid] = true;
188 }
189 }
190
191 return 'parent';
192 }
193
194 protected function initChild() {
195 global $wgMemc, $wgMainCacheType;
196 $wgMemc = wfGetCache( $wgMainCacheType );
197 $this->children = null;
198 pcntl_signal( SIGTERM, SIG_DFL );
199 }
200
201 protected function handleTermSignal( $signal ) {
202 $this->termReceived = true;
203 }
204 }