Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 69 |
|
0.00% |
0 / 7 |
CRAP | |
0.00% |
0 / 1 |
ForkController | |
0.00% |
0 / 68 |
|
0.00% |
0 / 7 |
650 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 16 |
|
0.00% |
0 / 1 |
20 | |||
start | |
0.00% |
0 / 33 |
|
0.00% |
0 / 1 |
182 | |||
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 ObjectCache; |
25 | use RedisConnectionPool; |
26 | use RuntimeException; |
27 | |
28 | /** |
29 | * Manage forking inside CLI maintenance scripts. |
30 | * |
31 | * Only handles forking and process control. In the future, this could |
32 | * be extended to provide IPC and job dispatch. |
33 | * |
34 | * This class requires the posix and pcntl extensions. |
35 | * |
36 | * @ingroup Maintenance |
37 | */ |
38 | class ForkController { |
39 | /** @var array|null */ |
40 | protected $children = []; |
41 | |
42 | /** @var int */ |
43 | protected $childNumber = 0; |
44 | |
45 | /** @var bool */ |
46 | protected $termReceived = false; |
47 | |
48 | /** @var int */ |
49 | protected $flags = 0; |
50 | |
51 | /** @var int */ |
52 | protected $procsToStart = 0; |
53 | |
54 | protected static $RESTARTABLE_SIGNALS = []; |
55 | |
56 | /** |
57 | * Pass this flag to __construct() to cause the class to automatically restart |
58 | * workers that exit with non-zero exit status or a signal such as SIGSEGV. |
59 | */ |
60 | private const RESTART_ON_ERROR = 1; |
61 | |
62 | /** |
63 | * @param int $numProcs The number of worker processes to fork |
64 | * @param int $flags |
65 | */ |
66 | public function __construct( $numProcs, $flags = 0 ) { |
67 | if ( !wfIsCLI() ) { |
68 | throw new RuntimeException( "MediaWiki\Maintenance\ForkController cannot be used from the web." ); |
69 | } elseif ( !extension_loaded( 'pcntl' ) ) { |
70 | throw new RuntimeException( |
71 | 'MediaWiki\Maintenance\ForkController requires pcntl extension to be installed.' |
72 | ); |
73 | } elseif ( !extension_loaded( 'posix' ) ) { |
74 | throw new RuntimeException( |
75 | 'MediaWiki\Maintenance\ForkController requires posix extension to be installed.' |
76 | ); |
77 | } |
78 | $this->procsToStart = $numProcs; |
79 | $this->flags = $flags; |
80 | |
81 | // Define this only after confirming PCNTL support |
82 | self::$RESTARTABLE_SIGNALS = [ |
83 | SIGFPE, SIGILL, SIGSEGV, SIGBUS, |
84 | SIGABRT, SIGSYS, SIGPIPE, SIGXCPU, SIGXFSZ, |
85 | ]; |
86 | } |
87 | |
88 | /** |
89 | * Start the child processes. |
90 | * |
91 | * This should only be called from the command line. It should be called |
92 | * as early as possible during execution. |
93 | * |
94 | * This will return 'child' in the child processes. In the parent process, |
95 | * it will run until all the child processes exit or a TERM signal is |
96 | * received. It will then return 'done'. |
97 | * @return string |
98 | */ |
99 | public function start() { |
100 | // Trap SIGTERM |
101 | pcntl_signal( SIGTERM, [ $this, 'handleTermSignal' ], false ); |
102 | |
103 | do { |
104 | // Start child processes |
105 | if ( $this->procsToStart ) { |
106 | if ( $this->forkWorkers( $this->procsToStart ) == 'child' ) { |
107 | return 'child'; |
108 | } |
109 | $this->procsToStart = 0; |
110 | } |
111 | |
112 | // Check child status |
113 | $status = false; |
114 | $deadPid = pcntl_wait( $status ); |
115 | |
116 | if ( $deadPid > 0 ) { |
117 | // Respond to child process termination |
118 | unset( $this->children[$deadPid] ); |
119 | if ( $this->flags & self::RESTART_ON_ERROR ) { |
120 | if ( pcntl_wifsignaled( $status ) ) { |
121 | // Restart if the signal was abnormal termination |
122 | // Don't restart if it was deliberately killed |
123 | $signal = pcntl_wtermsig( $status ); |
124 | if ( in_array( $signal, self::$RESTARTABLE_SIGNALS ) ) { |
125 | echo "Worker exited with signal $signal, restarting\n"; |
126 | $this->procsToStart++; |
127 | } |
128 | } elseif ( pcntl_wifexited( $status ) ) { |
129 | // Restart on non-zero exit status |
130 | $exitStatus = pcntl_wexitstatus( $status ); |
131 | if ( $exitStatus != 0 ) { |
132 | echo "Worker exited with status $exitStatus, restarting\n"; |
133 | $this->procsToStart++; |
134 | } else { |
135 | echo "Worker exited normally\n"; |
136 | } |
137 | } |
138 | } |
139 | // Throttle restarts |
140 | if ( $this->procsToStart ) { |
141 | usleep( 500_000 ); |
142 | } |
143 | } |
144 | |
145 | // Run signal handlers |
146 | if ( function_exists( 'pcntl_signal_dispatch' ) ) { |
147 | pcntl_signal_dispatch(); |
148 | } else { |
149 | declare( ticks=1 ) { |
150 | // @phan-suppress-next-line PhanPluginDuplicateExpressionAssignment |
151 | $status = $status; |
152 | } |
153 | } |
154 | // Respond to TERM signal |
155 | if ( $this->termReceived ) { |
156 | foreach ( $this->children as $childPid => $unused ) { |
157 | posix_kill( $childPid, SIGTERM ); |
158 | } |
159 | $this->termReceived = false; |
160 | } |
161 | } while ( count( $this->children ) ); |
162 | pcntl_signal( SIGTERM, SIG_DFL ); |
163 | return 'done'; |
164 | } |
165 | |
166 | /** |
167 | * Get the number of the child currently running. Note, this |
168 | * is not the pid, but rather which of the total number of children |
169 | * we are |
170 | * @return int |
171 | */ |
172 | public function getChildNumber() { |
173 | return $this->childNumber; |
174 | } |
175 | |
176 | protected function prepareEnvironment() { |
177 | // Don't share DB, storage, or memcached connections |
178 | MediaWikiServices::resetChildProcessServices(); |
179 | ObjectCache::clear(); |
180 | RedisConnectionPool::destroySingletons(); |
181 | } |
182 | |
183 | /** |
184 | * Fork a number of worker processes. |
185 | * |
186 | * @param int $numProcs |
187 | * @return string |
188 | */ |
189 | protected function forkWorkers( $numProcs ) { |
190 | $this->prepareEnvironment(); |
191 | |
192 | // Create the child processes |
193 | for ( $i = 0; $i < $numProcs; $i++ ) { |
194 | // Do the fork |
195 | $pid = pcntl_fork(); |
196 | if ( $pid === -1 ) { |
197 | echo "Error creating child processes\n"; |
198 | exit( 1 ); |
199 | } |
200 | |
201 | if ( !$pid ) { |
202 | $this->initChild(); |
203 | $this->childNumber = $i; |
204 | return 'child'; |
205 | } else { |
206 | // This is the parent process |
207 | $this->children[$pid] = true; |
208 | } |
209 | } |
210 | |
211 | return 'parent'; |
212 | } |
213 | |
214 | protected function initChild() { |
215 | $this->children = null; |
216 | pcntl_signal( SIGTERM, SIG_DFL ); |
217 | } |
218 | |
219 | protected function handleTermSignal( $signal ) { |
220 | $this->termReceived = true; |
221 | } |
222 | } |
223 | |
224 | /** @deprecated class alias since 1.40 */ |
225 | class_alias( ForkController::class, 'ForkController' ); |