Merge "maintenance: Script to rename titles for Unicode uppercasing changes"
[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 // @phan-suppress-next-line PhanPluginDuplicateExpressionAssignment
127 $status = $status;
128 }
129 }
130 // Respond to TERM signal
131 if ( $this->termReceived ) {
132 foreach ( $this->children as $childPid => $unused ) {
133 posix_kill( $childPid, SIGTERM );
134 }
135 $this->termReceived = false;
136 }
137 } while ( count( $this->children ) );
138 pcntl_signal( SIGTERM, SIG_DFL );
139 return 'done';
140 }
141
142 /**
143 * Get the number of the child currently running. Note, this
144 * is not the pid, but rather which of the total number of children
145 * we are
146 * @return int
147 */
148 public function getChildNumber() {
149 return $this->childNumber;
150 }
151
152 protected function prepareEnvironment() {
153 global $wgMemc;
154 // Don't share DB, storage, or memcached connections
155 MediaWikiServices::resetChildProcessServices();
156 FileBackendGroup::destroySingleton();
157 LockManagerGroup::destroySingletons();
158 JobQueueGroup::destroySingletons();
159 ObjectCache::clear();
160 RedisConnectionPool::destroySingletons();
161 $wgMemc = null;
162 }
163
164 /**
165 * Fork a number of worker processes.
166 *
167 * @param int $numProcs
168 * @return string
169 */
170 protected function forkWorkers( $numProcs ) {
171 $this->prepareEnvironment();
172
173 // Create the child processes
174 for ( $i = 0; $i < $numProcs; $i++ ) {
175 // Do the fork
176 $pid = pcntl_fork();
177 if ( $pid === -1 || $pid === false ) {
178 echo "Error creating child processes\n";
179 exit( 1 );
180 }
181
182 if ( !$pid ) {
183 $this->initChild();
184 $this->childNumber = $i;
185 return 'child';
186 } else {
187 // This is the parent process
188 $this->children[$pid] = true;
189 }
190 }
191
192 return 'parent';
193 }
194
195 protected function initChild() {
196 global $wgMemc, $wgMainCacheType;
197 $wgMemc = wfGetCache( $wgMainCacheType );
198 $this->children = null;
199 pcntl_signal( SIGTERM, SIG_DFL );
200 }
201
202 protected function handleTermSignal( $signal ) {
203 $this->termReceived = true;
204 }
205 }