Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 78 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
ForkController | |
0.00% |
0 / 77 |
|
0.00% |
0 / 8 |
812 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
start | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
210 | |||
allSuccessful | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
getChildNumber | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
prepareEnvironment | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
forkWorkers | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
20 | |||
initChild | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
handleTermSignal | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * This program is free software; you can redistribute it and/or modify |
4 | * it under the terms of the GNU General Public License as published by |
5 | * the Free Software Foundation; either version 2 of the License, or |
6 | * (at your option) any later version. |
7 | * |
8 | * This program is distributed in the hope that it will be useful, |
9 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
10 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
11 | * GNU General Public License for more details. |
12 | * |
13 | * You should have received a copy of the GNU General Public License along |
14 | * with this program; if not, write to the Free Software Foundation, Inc., |
15 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
16 | * http://www.gnu.org/copyleft/gpl.html |
17 | * |
18 | * @file |
19 | */ |
20 | |
21 | namespace MediaWiki\Maintenance; |
22 | |
23 | use MediaWiki\MediaWikiServices; |
24 | use RuntimeException; |
25 | use Wikimedia\ObjectCache\RedisConnectionPool; |
26 | |
27 | /** |
28 | * Manage forking inside CLI maintenance scripts. |
29 | * |
30 | * Only handles forking and process control. In the future, this could |
31 | * be extended to provide IPC and job dispatch. |
32 | * |
33 | * This class requires the posix and pcntl extensions. |
34 | * |
35 | * @ingroup Maintenance |
36 | */ |
37 | class ForkController { |
38 | /** @var array|null */ |
39 | protected $children = []; |
40 | |
41 | /** @var int */ |
42 | protected $childNumber = 0; |
43 | |
44 | /** @var bool */ |
45 | protected $termReceived = false; |
46 | |
47 | /** @var int */ |
48 | protected $flags = 0; |
49 | |
50 | /** @var int */ |
51 | protected $procsToStart = 0; |
52 | |
53 | /** @var int[] */ |
54 | protected static $RESTARTABLE_SIGNALS = []; |
55 | |
56 | /** @var int[] */ |
57 | protected $exitStatuses = []; |
58 | |
59 | /** |
60 | * Pass this flag to __construct() to cause the class to automatically restart |
61 | * workers that exit with non-zero exit status or a signal such as SIGSEGV. |
62 | */ |
63 | private const RESTART_ON_ERROR = 1; |
64 | |
65 | /** |
66 | * @param int $numProcs The number of worker processes to fork |
67 | * @param int $flags |
68 | */ |
69 | public function __construct( $numProcs, $flags = 0 ) { |
70 | if ( !wfIsCLI() ) { |
71 | throw new RuntimeException( "MediaWiki\Maintenance\ForkController cannot be used from the web." ); |
72 | } elseif ( !extension_loaded( 'pcntl' ) ) { |
73 | throw new RuntimeException( |
74 | 'MediaWiki\Maintenance\ForkController requires pcntl extension to be installed.' |
75 | ); |
76 | } elseif ( !extension_loaded( 'posix' ) ) { |
77 | throw new RuntimeException( |
78 | 'MediaWiki\Maintenance\ForkController requires posix extension to be installed.' |
79 | ); |
80 | } |
81 | $this->procsToStart = $numProcs; |
82 | $this->flags = $flags; |
83 | |
84 | // Define this only after confirming PCNTL support |
85 | self::$RESTARTABLE_SIGNALS = [ |
86 | SIGFPE, SIGILL, SIGSEGV, SIGBUS, |
87 | SIGABRT, SIGSYS, SIGPIPE, SIGXCPU, SIGXFSZ, |
88 | ]; |
89 | } |
90 | |
91 | /** |
92 | * Start the child processes. |
93 | * |
94 | * This should only be called from the command line. It should be called |
95 | * as early as possible during execution. |
96 | * |
97 | * This will return 'child' in the child processes. In the parent process, |
98 | * it will run until all the child processes exit or a TERM signal is |
99 | * received. It will then return 'done'. |
100 | * |
101 | * @return string |
102 | */ |
103 | public function start() { |
104 | // Trap SIGTERM |
105 | pcntl_signal( SIGTERM, [ $this, 'handleTermSignal' ], false ); |
106 | |
107 | do { |
108 | // Start child processes |
109 | if ( $this->procsToStart ) { |
110 | if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) { |
111 | return 'child'; |
112 | } |
113 | $this->procsToStart = 0; |
114 | } |
115 | |
116 | // Check child status |
117 | $status = false; |
118 | $deadPid = pcntl_wait( $status ); |
119 | |
120 | if ( $deadPid > 0 ) { |
121 | // Respond to child process termination |
122 | unset( $this->children[$deadPid] ); |
123 | if ( $this->flags & self::RESTART_ON_ERROR ) { |
124 | if ( pcntl_wifsignaled( $status ) ) { |
125 | // Restart if the signal was abnormal termination |
126 | // Don't restart if it was deliberately killed |
127 | $signal = pcntl_wtermsig( $status ); |
128 | if ( in_array( $signal, self::$RESTARTABLE_SIGNALS ) ) { |
129 | echo "Worker exited with signal $signal, restarting\n"; |
130 | $this->procsToStart++; |
131 | } |
132 | } elseif ( pcntl_wifexited( $status ) ) { |
133 | // Restart on non-zero exit status |
134 | $exitStatus = pcntl_wexitstatus( $status ); |
135 | if ( $exitStatus != 0 ) { |
136 | echo "Worker exited with status $exitStatus, restarting\n"; |
137 | $this->procsToStart++; |
138 | } else { |
139 | echo "Worker exited normally\n"; |
140 | } |
141 | } |
142 | } |
143 | |
144 | if ( pcntl_wifexited( $status ) ) { |
145 | $exitStatus = pcntl_wexitstatus( $status ); |
146 | echo "Worker exited with status $exitStatus\n"; |
147 | $this->exitStatuses[] = $exitStatus; |
148 | } |
149 | |
150 | // Throttle restarts |
151 | if ( $this->procsToStart ) { |
152 | usleep( 500_000 ); |
153 | } |
154 | } |
155 | |
156 | // Run signal handlers |
157 | if ( function_exists( 'pcntl_signal_dispatch' ) ) { |
158 | pcntl_signal_dispatch(); |
159 | } else { |
160 | declare( ticks=1 ) { |
161 | // @phan-suppress-next-line PhanPluginDuplicateExpressionAssignment |
162 | $status = $status; |
163 | } |
164 | } |
165 | // Respond to TERM signal |
166 | if ( $this->termReceived ) { |
167 | foreach ( $this->children as $childPid => $unused ) { |
168 | posix_kill( $childPid, SIGTERM ); |
169 | } |
170 | $this->termReceived = false; |
171 | } |
172 | } while ( count( $this->children ) ); |
173 | pcntl_signal( SIGTERM, SIG_DFL ); |
174 | return 'done'; |
175 | } |
176 | |
177 | /** |
178 | * Return true if all completed child processes exited with an exit |
179 | * status / return code of 0. |
180 | * |
181 | * @return bool |
182 | */ |
183 | public function allSuccessful(): bool { |
184 | return array_reduce( |
185 | $this->exitStatuses, |
186 | static fn ( $acc, $status ) => $acc && ( $status === 0 ), |
187 | true |
188 | ); |
189 | } |
190 | |
191 | /** |
192 | * Get the number of the child currently running. Note, this |
193 | * is not the pid, but rather which of the total number of children |
194 | * we are |
195 | * @return int |
196 | */ |
197 | public function getChildNumber() { |
198 | return $this->childNumber; |
199 | } |
200 | |
201 | protected function prepareEnvironment() { |
202 | // Don't share DB, storage, or memcached connections |
203 | MediaWikiServices::resetChildProcessServices(); |
204 | MediaWikiServices::getInstance()->getObjectCacheFactory()->clear(); |
205 | RedisConnectionPool::destroySingletons(); |
206 | } |
207 | |
208 | /** |
209 | * Fork a number of worker processes. |
210 | * |
211 | * @param int $numProcs |
212 | * @return string |
213 | */ |
214 | protected function forkWorkers( $numProcs ) { |
215 | $this->prepareEnvironment(); |
216 | |
217 | // Create the child processes |
218 | for ( $i = 0; $i < $numProcs; $i++ ) { |
219 | // Do the fork |
220 | $pid = pcntl_fork(); |
221 | if ( $pid === -1 ) { |
222 | echo "Error creating child processes\n"; |
223 | exit( 1 ); |
224 | } |
225 | |
226 | if ( !$pid ) { |
227 | $this->initChild(); |
228 | $this->childNumber = $i; |
229 | return 'child'; |
230 | } else { |
231 | // This is the parent process |
232 | $this->children[$pid] = true; |
233 | } |
234 | } |
235 | |
236 | return 'parent'; |
237 | } |
238 | |
239 | protected function initChild() { |
240 | $this->children = null; |
241 | pcntl_signal( SIGTERM, SIG_DFL ); |
242 | } |
243 | |
244 | protected function handleTermSignal( $signal ) { |
245 | $this->termReceived = true; |
246 | } |
247 | } |
248 | |
249 | /** @deprecated class alias since 1.40 */ |
250 | class_alias( ForkController::class, 'ForkController' ); |