36 private const FAKE_TTM =
'dry-run';
38 public function __construct() {
39 parent::__construct();
40 $this->addDescription(
'Script to bootstrap TTMServer.' );
43 '(optional) Number of threads',
49 '(optional) Server configuration identifier',
56 'Update the index mapping. Warning: Clears all existing data in the index.'
60 'Do not make any changes to the index.'
64 'Output more status information.'
68 'Only run setup and and cleanup. Skip inserting content.'
70 $this->setBatchSize( 500 );
71 $this->requireExtension(
'Translate' );
72 $this->start = microtime(
true );
75 public function statusLine( $text, $channel =
null ) {
76 $pid = sprintf(
'%5s', getmypid() );
77 $prefix = sprintf(
'%6.2f', microtime(
true ) - $this->start );
78 $mem = sprintf(
'%5.1fM', memory_get_usage(
true ) / ( 1024 * 1024 ) );
79 $this->output(
"$pid $prefix $mem $text", $channel );
82 public function execute() {
83 $dryRun = $this->hasOption(
'dry-run' );
84 $ttmServerId = $this->getOption(
'ttmserver' );
85 $shouldReindex = $this->getOption(
'reindex',
false );
87 if ( $this->mBatchSize !==
null && $this->mBatchSize < 1 ) {
88 $this->fatalError(
'Invalid value for option: "batch-size"' );
91 $servers = $this->getServers( $dryRun, $shouldReindex, $ttmServerId );
95 foreach ( array_keys( $servers ) as $serverId ) {
99 $server = $this->getWritableServer( $serverId );
100 $this->resetStateForFork();
101 $this->beginBootstrap( $server, $serverId );
103 } elseif ( $pid === -1 ) {
105 $server = $this->getWritableServer( $serverId );
106 $this->beginBootstrap( $server, $serverId );
109 $this->statusLine(
"Forked thread $pid to handle bootstrapping for '$serverId'\n" );
111 pcntl_waitpid( $pid, $status );
113 if ( !$this->verifyChildStatus( $pid, $status ) ) {
114 $this->fatalError(
"Bootstrap failed for '$serverId'." );
120 $threads = $this->getOption(
'threads', 1 );
123 if ( $this->hasOption(
'clean' ) ) {
126 $groups = MessageGroups::singleton()->getGroups();
129 foreach ( $groups as $id => $group ) {
131 if ( $group->isMeta() ) {
138 $this->resetStateForFork();
139 $this->exportGroup( $group, $servers );
141 } elseif ( $pid === -1 ) {
142 $this->exportGroup( $group, $servers );
145 $this->statusLine(
"Forked thread $pid to handle $id\n" );
149 if ( count( $pids ) >= $threads ) {
151 $pid = pcntl_wait( $status );
152 $hasErrors = $hasErrors || !$this->verifyChildStatus( $pid, $status );
153 unset( $pids[$pid] );
159 foreach ( array_keys( $pids ) as $pid ) {
161 pcntl_waitpid( $pid, $status );
162 $hasErrors = $hasErrors || !$this->verifyChildStatus( $pid, $status );
166 $this->endBootstrap( $servers );
169 $this->fatalError(
'!!! Some threads failed. Review the script output !!!' );
179 private function getServers(
182 ?
string $ttmServerId =
null
185 $ttmServerFactory = Services::getInstance()->getTtmServerFactory();
189 if ( $ttmServerId !==
null ) {
191 $servers[ $ttmServerId ] = $ttmServerFactory->create( $ttmServerId );
193 $this->fatalError(
"Error while creating TtmServer $ttmServerId: " . $e->getMessage() );
196 $servers = $ttmServerFactory->getWritable();
201 $this->fatalError(
"No writable TtmServers found." );
204 foreach ( $servers as $server ) {
205 Assert::parameterType( WritableTtmServer::class, $server,
'$server' );
207 if ( method_exists( $server,
'setLogger' ) ) {
209 $server->setLogger( $this );
212 if ( $shouldReindex ) {
214 $server->setDoReIndex();
221 protected function beginBootstrap(
WritableTtmServer $server,
string $serverId ) {
222 $this->statusLine(
"Cleaning up old entries in '$serverId'...\n" );
226 protected function endBootstrap( array $servers ) {
227 foreach ( $servers as $serverId => $server ) {
228 $this->statusLine(
"Optimizing '$serverId'...\n" );
238 private function exportGroup(
MessageGroup $group, array $servers ):
void {
240 'total' => -microtime(
true ),
250 $times[
'init' ] -= microtime(
true );
251 $collection = $this->getCollection( $group, $sourceLanguage );
252 $times[
'init' ] += microtime(
true );
254 $times[
'stats' ] -= microtime(
true );
255 $stats = MessageGroupStats::forGroup( $group->
getId() );
256 $times[
'stats' ] += microtime(
true );
257 unset( $stats[ $sourceLanguage ] );
259 $translationCount = $definitionCount = 0;
261 foreach ( $servers as $server ) {
262 $server->beginBatch();
265 foreach ( $this->getDefinitions( $collection, $sourceLanguage ) as $batch ) {
266 $definitionCount += count( $batch );
267 foreach ( $servers as $server ) {
268 $times[
'writes' ] -= microtime(
true );
269 $server->batchInsertDefinitions( $batch );
270 $times[
'writes' ] += microtime(
true );
274 $times[
'trans' ] -= microtime(
true );
275 foreach ( $stats as $targetLanguage => $numbers ) {
276 if ( $numbers[MessageGroupStats::TRANSLATED] === 0 ) {
280 foreach ( $this->getTranslations( $collection, $targetLanguage ) as $batch ) {
281 $translationCount += count( $batch );
282 foreach ( $servers as $server ) {
283 $transWrites -= microtime(
true );
284 $server->batchInsertTranslations( $batch );
285 $transWrites += microtime(
true );
290 $times[
'trans' ] += ( microtime(
true ) - $transWrites );
291 $times[
'writes' ] += $transWrites;
293 foreach ( $servers as $server ) {
297 $times[
'total' ] += microtime(
true );
298 $countItems = $translationCount + $definitionCount;
300 if ( $countItems !== 0 ) {
302 "Total %.1f s for %d items on %d server(s) >> stats/init/trans/writes %%: %d/%d/%d/%d >> %.1f ms/item",
306 $times[
'stats'] / $times[
'total'] * 100,
307 $times[
'init'] / $times[
'total'] * 100,
308 $times[
'trans'] / $times[
'total'] * 100,
309 $times[
'writes'] / $times[
'total'] * 100,
310 $times[
'total'] / $countItems * 1000
312 $this->logInfo(
"Finished exporting {$group->getId()}. $debug\n" );
316 private function getDefinitions(
MessageCollection $collection,
string $sourceLanguage ): Generator {
318 foreach ( $collection->
keys() as $mKey => $titleValue ) {
319 $title = Title::newFromLinkTarget( $titleValue );
321 $definition = [ $handle, $sourceLanguage, $collection[$mKey]->definition() ];
322 $definitions[] = $definition;
323 if ( $this->mBatchSize && count( $definitions ) === $this->mBatchSize ) {
329 if ( $definitions ) {
334 private function getTranslations(
MessageCollection $collection,
string $targetLanguage ): Generator {
336 $collection->
filter(
'ignored' );
337 $collection->
filter(
'translated',
false );
341 foreach ( $collection->
keys() as $mkey => $titleValue ) {
342 $title = Title::newFromLinkTarget( $titleValue );
344 $translations[] = [ $handle, $targetLanguage, $collection[$mkey]->translation() ];
345 if ( $this->mBatchSize && count( $translations ) === $this->mBatchSize ) {
351 if ( $translations ) {
356 private function logInfo(
string $text ) {
357 if ( $this->hasOption(
'verbose' ) ) {
358 $this->statusLine( $text );
362 protected function resetStateForFork() {
365 MediaWiki\MediaWikiServices::resetChildProcessServices();
370 MediaWiki\MediaWikiServices::getInstance()->getMessageCache()->disable();
373 private function verifyChildStatus(
int $pid,
int $status ):
bool {
374 if ( pcntl_wifexited( $status ) ) {
375 $code = pcntl_wexitstatus( $status );
377 $this->output(
"Pid $pid exited with status $code !!\n" );
380 } elseif ( pcntl_wifsignaled( $status ) ) {
381 $signum = pcntl_wtermsig( $status );
382 $this->output(
"Pid $pid terminated by signal $signum !!\n" );
390 if ( $serverId === self::FAKE_TTM ) {
394 $server = Services::getInstance()->getTtmServerFactory()->create( $serverId );
396 throw new InvalidArgumentException(
397 "$serverId TTM server does not implement WritableTtmServer interface "
406 $collection->
filter(
'ignored' );