Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
MessageIndex.php
Go to the documentation of this file.
1<?php
11use Cdb\Reader;
12use Cdb\Writer;
14use MediaWiki\Logger\LoggerFactory;
15use MediaWiki\MediaWikiServices;
16
25abstract class MessageIndex {
26 private const CACHEKEY = 'Translate-MessageIndex-interim';
27
29 protected static $instance;
31 private static $keysCache;
33 protected $interimCache;
35 private $statusCache;
37 private $jobQueueGroup;
38
39 public function __construct() {
40 // TODO: Use dependency injection
41 $mwInstance = MediaWikiServices::getInstance();
42 $this->statusCache = $mwInstance->getMainWANObjectCache();
43 $this->jobQueueGroup = $mwInstance->getJobQueueGroup();
44 }
45
50 public static function singleton(): self {
51 if ( self::$instance === null ) {
52 self::$instance = Services::getInstance()->getMessageIndex();
53 }
54
55 return self::$instance;
56 }
57
64 public static function setInstance( self $instance ) {
65 self::$instance = $instance;
66 }
67
74 public static function getGroupIds( MessageHandle $handle ): array {
75 global $wgTranslateMessageNamespaces;
76
77 $title = $handle->getTitle();
78
79 if ( !$title->inNamespaces( $wgTranslateMessageNamespaces ) ) {
80 return [];
81 }
82
83 $namespace = $title->getNamespace();
84 $key = $handle->getKey();
85 $normkey = TranslateUtils::normaliseKey( $namespace, $key );
86
87 $cache = self::getCache();
88 $value = $cache->get( $normkey );
89 if ( $value === null ) {
90 $value = (array)self::singleton()->getWithCache( $normkey );
91 $cache->set( $normkey, $value );
92 }
93
94 return $value;
95 }
96
98 private static function getCache() {
99 if ( self::$keysCache === null ) {
100 self::$keysCache = new MapCacheLRU( 30 );
101 }
102 return self::$keysCache;
103 }
104
110 public static function getPrimaryGroupId( MessageHandle $handle ): ?string {
111 $groups = self::getGroupIds( $handle );
112
113 return count( $groups ) ? array_shift( $groups ) : null;
114 }
115
116 private function getWithCache( $key ) {
117 $interimCacheValue = $this->getInterimCache()->get( self::CACHEKEY );
118 if ( $interimCacheValue && isset( $interimCacheValue['newKeys'][$key] ) ) {
119 return $interimCacheValue['newKeys'][$key];
120 }
121
122 return $this->get( $key );
123 }
124
131 protected function get( $key ) {
132 // Default implementation
133 $mi = $this->retrieve();
134 return $mi[$key] ?? null;
135 }
136
141 abstract public function retrieve( $forRebuild = false );
142
147 public function getKeys() {
148 return array_keys( $this->retrieve() );
149 }
150
151 abstract protected function store( array $array, array $diff );
152
153 protected function lock() {
154 return true;
155 }
156
157 protected function unlock() {
158 return true;
159 }
160
168 public function rebuild( float $timestamp = null ): array {
169 $logger = LoggerFactory::getInstance( 'Translate' );
170
171 static $recursion = 0;
172
173 if ( $recursion > 0 ) {
174 $msg = __METHOD__ . ': trying to recurse - building the index first time?';
175 wfWarn( $msg );
176
177 $recursion--;
178 return [];
179 }
180 $recursion++;
181
182 $logger->info(
183 '[MessageIndex] Started rebuild. Initiated by {callers}',
184 [ 'callers' => wfGetAllCallers( 20 ) ]
185 );
186
187 $groups = MessageGroups::singleton()->getGroups();
188
189 $tsStart = microtime( true );
190 if ( !$this->lock() ) {
191 throw new MessageIndexException( __CLASS__ . ': unable to acquire lock' );
192 }
193
194 $lockWaitDuration = microtime( true ) - $tsStart;
195 $logger->info(
196 '[MessageIndex] Got lock in {duration}',
197 [ 'duration' => $lockWaitDuration ]
198 );
199
200 self::getCache()->clear();
201
202 $new = [];
203 $old = $this->retrieve( 'rebuild' );
204 $postponed = [];
205
207 foreach ( $groups as $g ) {
208 if ( !$g->exists() ) {
209 $id = $g->getId();
210 wfWarn( __METHOD__ . ": group '$id' is registered but does not exist" );
211 continue;
212 }
213
214 # Skip meta thingies
215 if ( $g->isMeta() ) {
216 $postponed[] = $g;
217 continue;
218 }
219
220 $this->checkAndAdd( $new, $g );
221 }
222
223 foreach ( $postponed as $g ) {
224 $this->checkAndAdd( $new, $g, true );
225 }
226
227 $diff = self::getArrayDiff( $old, $new );
228 $this->store( $new, $diff['keys'] );
229 $this->unlock();
230
231 $criticalSectionDuration = microtime( true ) - $tsStart - $lockWaitDuration;
232 $logger->info(
233 '[MessageIndex] Finished critical section in {duration}',
234 [ 'duration' => $criticalSectionDuration ]
235 );
236
237 $cache = $this->getInterimCache();
238 $interimCacheValue = $cache->get( self::CACHEKEY );
239 $timestamp = $timestamp ?? microtime( true );
240 if ( $interimCacheValue ) {
241 if ( $interimCacheValue['timestamp'] <= $timestamp ) {
242 $cache->delete( self::CACHEKEY );
243 } else {
244 // Cache has a later timestamp. This may be caused due to
245 // job deduplication. Just in case, spin off a new job to clean up the cache.
247 $this->jobQueueGroup->push( $job );
248 }
249 }
250
251 // Other caches can check this key to know when they need to refresh
252 $this->statusCache->touchCheckKey( $this->getStatusCacheKey() );
253
254 $this->clearMessageGroupStats( $diff );
255
256 $recursion--;
257
258 return $new;
259 }
260
265 public function getStatusCacheKey(): string {
266 return $this->statusCache->makeKey( 'Translate', 'MessageIndex', 'status' );
267 }
268
269 private function getInterimCache(): BagOStuff {
270 return ObjectCache::getInstance( CACHE_ANYTHING );
271 }
272
273 public function storeInterim( MessageGroup $group, array $newKeys ): void {
274 $namespace = $group->getNamespace();
275 $id = $group->getId();
276
277 $normalizedNewKeys = [];
278 foreach ( $newKeys as $key ) {
279 $normalizedNewKeys[TranslateUtils::normaliseKey( $namespace, $key )] = $id;
280 }
281
282 $cache = $this->getInterimCache();
283 // Merge existing with existing keys
284 $interimCacheValue = $cache->get( self::CACHEKEY, $cache::READ_LATEST );
285 if ( $interimCacheValue ) {
286 $normalizedNewKeys = array_merge( $interimCacheValue['newKeys'], $normalizedNewKeys );
287 }
288
289 $value = [
290 'timestamp' => microtime( true ),
291 'newKeys' => $normalizedNewKeys,
292 ];
293
294 $cache->set( self::CACHEKEY, $value, $cache::TTL_DAY );
295 }
296
325 public static function getArrayDiff( array $old, array $new ) {
326 $values = [];
327 $record = static function ( $groups ) use ( &$values ) {
328 foreach ( $groups as $group ) {
329 $values[$group] = true;
330 }
331 };
332
333 $keys = [
334 'add' => [],
335 'del' => [],
336 'mod' => [],
337 ];
338
339 foreach ( $new as $key => $groups ) {
340 if ( !isset( $old[$key] ) ) {
341 $keys['add'][$key] = [ [], (array)$groups ];
342 $record( (array)$groups );
343 // Using != here on purpose to ignore the order of items
344 } elseif ( $groups != $old[$key] ) {
345 $keys['mod'][$key] = [ (array)$old[$key], (array)$groups ];
346 $record( array_diff( (array)$old[$key], (array)$groups ) );
347 $record( array_diff( (array)$groups, (array)$old[$key] ) );
348 }
349 }
350
351 foreach ( $old as $key => $groups ) {
352 if ( !isset( $new[$key] ) ) {
353 $keys['del'][$key] = [ (array)$groups, [] ];
354 $record( (array)$groups );
355 }
356 // We already checked for diffs above
357 }
358
359 return [
360 'keys' => $keys,
361 'values' => array_keys( $values ),
362 ];
363 }
364
370 protected function clearMessageGroupStats( array $diff ) {
371 $job = MessageGroupStatsRebuildJob::newRefreshGroupsJob( $diff['values'] );
372 $this->jobQueueGroup->push( $job );
373
374 foreach ( $diff['keys'] as $keys ) {
375 foreach ( $keys as $key => $data ) {
376 [ $ns, $pagename ] = explode( ':', $key, 2 );
377 $title = Title::makeTitle( $ns, $pagename );
378 $handle = new MessageHandle( $title );
379 [ $oldGroups, $newGroups ] = $data;
380 Hooks::run( 'TranslateEventMessageMembershipChange',
381 [ $handle, $oldGroups, $newGroups ] );
382 }
383 }
384 }
385
391 protected function checkAndAdd( &$hugearray, MessageGroup $g, $ignore = false ) {
392 $keys = $g->getKeys();
393 $id = $g->getId();
394 $namespace = $g->getNamespace();
395
396 foreach ( $keys as $key ) {
397 # Force all keys to lower case, because the case doesn't matter and it is
398 # easier to do comparing when the case of first letter is unknown, because
399 # mediawiki forces it to upper case
400 $key = TranslateUtils::normaliseKey( $namespace, $key );
401 if ( isset( $hugearray[$key] ) ) {
402 if ( !$ignore ) {
403 $to = implode( ', ', (array)$hugearray[$key] );
404 wfWarn( "Key $key already belongs to $to, conflict with $id" );
405 }
406
407 if ( is_array( $hugearray[$key] ) ) {
408 // Hard work is already done, just add a new reference
409 $hugearray[$key][] = & $id;
410 } else {
411 // Store the actual reference, then remove it from array, to not
412 // replace the references value, but to store an array of new
413 // references instead. References are hard!
414 $value = & $hugearray[$key];
415 unset( $hugearray[$key] );
416 $hugearray[$key] = [ &$value, &$id ];
417 }
418 } else {
419 $hugearray[$key] = & $id;
420 }
421 }
422 unset( $id ); // Disconnect the previous references to this $id
423 }
424
432 protected function serialize( $data ) {
433 if ( is_array( $data ) ) {
434 return implode( '|', $data );
435 } else {
436 return $data;
437 }
438 }
439
440 protected function unserialize( $data ) {
441 if ( strpos( $data, '|' ) !== false ) {
442 return explode( '|', $data );
443 }
444
445 return $data;
446 }
447}
448
465 protected $index;
466 protected $filename = 'translate_messageindex.ser';
467
472 public function retrieve( $forRebuild = false ) {
473 if ( $this->index !== null ) {
474 return $this->index;
475 }
476
477 $file = TranslateUtils::cacheFile( $this->filename );
478 if ( file_exists( $file ) ) {
479 $this->index = unserialize( file_get_contents( $file ) );
480 } else {
481 $this->index = $this->rebuild();
482 }
483
484 return $this->index;
485 }
486
487 protected function store( array $array, array $diff ) {
488 $file = TranslateUtils::cacheFile( $this->filename );
489 file_put_contents( $file, serialize( $array ) );
490 $this->index = $array;
491 }
492}
493
507 protected $index;
508
509 protected function lock() {
510 $dbw = wfGetDB( DB_PRIMARY );
511
512 // Any transaction should be flushed after getting the lock to avoid
513 // stale pre-lock REPEATABLE-READ snapshot data.
514 $ok = $dbw->lock( 'translate-messageindex', __METHOD__, 30 );
515 if ( $ok ) {
516 $dbw->commit( __METHOD__, 'flush' );
517 }
518
519 return $ok;
520 }
521
522 protected function unlock() {
523 $fname = __METHOD__;
524 $dbw = wfGetDB( DB_PRIMARY );
525 // Unlock once the rows are actually unlocked to avoid deadlocks
526 if ( !$dbw->trxLevel() ) {
527 $dbw->unlock( 'translate-messageindex', $fname );
528 } elseif ( is_callable( [ $dbw, 'onTransactionResolution' ] ) ) { // 1.28
529 $dbw->onTransactionResolution( static function () use ( $dbw, $fname ) {
530 $dbw->unlock( 'translate-messageindex', $fname );
531 }, $fname );
532 } else {
533 $dbw->onTransactionCommitOrIdle( static function () use ( $dbw, $fname ) {
534 $dbw->unlock( 'translate-messageindex', $fname );
535 }, $fname );
536 }
537
538 return true;
539 }
540
545 public function retrieve( $forRebuild = false ) {
546 if ( $this->index !== null && !$forRebuild ) {
547 return $this->index;
548 }
549
550 $dbr = wfGetDB( $forRebuild ? DB_PRIMARY : DB_REPLICA );
551 $res = $dbr->select( 'translate_messageindex', '*', [], __METHOD__ );
552 $this->index = [];
553 foreach ( $res as $row ) {
554 $this->index[$row->tmi_key] = $this->unserialize( $row->tmi_value );
555 }
556
557 return $this->index;
558 }
559
560 protected function get( $key ) {
561 $dbr = wfGetDB( DB_REPLICA );
562 $value = $dbr->selectField(
563 'translate_messageindex',
564 'tmi_value',
565 [ 'tmi_key' => $key ],
566 __METHOD__
567 );
568
569 if ( is_string( $value ) ) {
570 $value = $this->unserialize( $value );
571 } else {
572 $value = null;
573 }
574
575 return $value;
576 }
577
578 protected function store( array $array, array $diff ) {
579 $updates = [];
580
581 foreach ( [ $diff['add'], $diff['mod'] ] as $changes ) {
582 foreach ( $changes as $key => $data ) {
583 [ , $new ] = $data;
584 $updates[] = [
585 'tmi_key' => $key,
586 'tmi_value' => $this->serialize( $new ),
587 ];
588 }
589 }
590
591 $index = [ 'tmi_key' ];
592 $deletions = array_keys( $diff['del'] );
593
594 $dbw = wfGetDB( DB_PRIMARY );
595 $dbw->startAtomic( __METHOD__ );
596
597 if ( $updates !== [] ) {
598 $dbw->replace( 'translate_messageindex', [ $index ], $updates, __METHOD__ );
599 }
600
601 if ( $deletions !== [] ) {
602 $dbw->delete( 'translate_messageindex', [ 'tmi_key' => $deletions ], __METHOD__ );
603 }
604
605 $dbw->endAtomic( __METHOD__ );
606
607 $this->index = $array;
608 }
609}
610
620 protected $key = 'translate-messageindex';
621 protected $cache;
623 protected $index;
624
625 protected function __construct() {
626 parent::__construct();
627 $this->cache = ObjectCache::getInstance( CACHE_ANYTHING );
628 }
629
634 public function retrieve( $forRebuild = false ) {
635 if ( $this->index !== null ) {
636 return $this->index;
637 }
638
639 $key = $this->cache->makeKey( $this->key );
640 $data = $this->cache->get( $key );
641 if ( is_array( $data ) ) {
642 $this->index = $data;
643 } else {
644 $this->index = $this->rebuild();
645 }
646
647 return $this->index;
648 }
649
650 protected function store( array $array, array $diff ) {
651 $key = $this->cache->makeKey( $this->key );
652 $this->cache->set( $key, $array );
653
654 $this->index = $array;
655 }
656}
657
673 protected $index;
675 protected $reader;
677 protected $filename = 'translate_messageindex.cdb';
678
683 public function retrieve( $forRebuild = false ) {
684 $reader = $this->getReader();
685 // This must be below the line above, which may fill the index
686 if ( $this->index !== null ) {
687 return $this->index;
688 }
689
690 $this->index = [];
691 foreach ( $this->getKeys() as $key ) {
692 $this->index[$key] = $this->unserialize( $reader->get( $key ) );
693 }
694
695 return $this->index;
696 }
697
698 public function getKeys() {
699 $reader = $this->getReader();
700 $keys = [];
701 while ( true ) {
702 $key = $keys === [] ? $reader->firstkey() : $reader->nextkey();
703 if ( $key === false ) {
704 break;
705 }
706 $keys[] = $key;
707 }
708
709 return $keys;
710 }
711
712 protected function get( $key ) {
713 $reader = $this->getReader();
714 // We might have the full cache loaded
715 if ( $this->index !== null ) {
716 return $this->index[$key] ?? null;
717 }
718
719 $value = $reader->get( $key );
720 if ( !is_string( $value ) ) {
721 $value = null;
722 } else {
723 $value = $this->unserialize( $value );
724 }
725
726 return $value;
727 }
728
729 protected function store( array $array, array $diff ) {
730 $this->reader = null;
731
732 $file = TranslateUtils::cacheFile( $this->filename );
733 $cache = Writer::open( $file );
734
735 foreach ( $array as $key => $value ) {
736 $value = $this->serialize( $value );
737 $cache->set( $key, $value );
738 }
739
740 $cache->close();
741
742 $this->index = $array;
743 }
744
745 protected function getReader() {
746 if ( $this->reader ) {
747 return $this->reader;
748 }
749
750 $file = TranslateUtils::cacheFile( $this->filename );
751 if ( !file_exists( $file ) ) {
752 // Create an empty index to allow rebuild
753 $this->store( [], [] );
754 $this->index = $this->rebuild();
755 }
756
757 $this->reader = Reader::open( $file );
758 return $this->reader;
759 }
760}
761
771 protected $index = [];
772
777 public function retrieve( $forRebuild = false ) {
778 return $this->index;
779 }
780
786 protected function get( $key ) {
787 return $this->index[$key] ?? null;
788 }
789
790 protected function store( array $array, array $diff ) {
791 $this->index = $array;
792 }
793
794 protected function clearMessageGroupStats( array $diff ) {
795 }
796}
return[ 'Translate:ConfigHelper'=> static function():ConfigHelper { return new ConfigHelper();}, 'Translate:CsvTranslationImporter'=> static function(MediaWikiServices $services):CsvTranslationImporter { return new CsvTranslationImporter( $services->getWikiPageFactory());}, 'Translate:EntitySearch'=> static function(MediaWikiServices $services):EntitySearch { return new EntitySearch($services->getMainWANObjectCache(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), MessageGroups::singleton(), $services->getNamespaceInfo(), $services->get( 'Translate:MessageIndex'), $services->getTitleParser(), $services->getTitleFormatter());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->getMainConfig(), $services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance( 'Translate.GroupSynchronization'), MessageIndex::singleton());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore(new RevTagStore(), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReview'=> static function(MediaWikiServices $services):MessageGroupReview { return new MessageGroupReview($services->getDBLoadBalancer(), $services->getHookContainer());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=$services->getMainConfig() ->get( 'TranslateMessageIndex');if(is_string( $params)) { $params=(array) $params;} $class=array_shift( $params);return new $class( $params);}, 'Translate:ParsingPlaceholderFactory'=> static function():ParsingPlaceholderFactory { return new ParsingPlaceholderFactory();}, 'Translate:PersistentCache'=> static function(MediaWikiServices $services):PersistentCache { return new PersistentDatabaseCache($services->getDBLoadBalancer(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'));}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleMover'=> static function(MediaWikiServices $services):TranslatableBundleMover { return new TranslatableBundleMover($services->getMovePageFactory(), $services->getJobQueueGroup(), $services->getLinkBatchFactory(), $services->get( 'Translate:TranslatableBundleFactory'), $services->get( 'Translate:SubpageListBuilder'), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), new RevTagStore(), $services->getDBLoadBalancer());}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnectionRef(DB_REPLICA);return new TranslationStashStorage( $db);}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory());}, 'Translate:TranslationUnitStoreFactory'=> static function(MediaWikiServices $services):TranslationUnitStoreFactory { return new TranslationUnitStoreFactory( $services->getDBLoadBalancer());}, 'Translate:TranslatorActivity'=> static function(MediaWikiServices $services):TranslatorActivity { $query=new TranslatorActivityQuery($services->getMainConfig(), $services->getDBLoadBalancer());return new TranslatorActivity($services->getMainObjectStash(), $query, $services->getJobQueueGroup());}, 'Translate:TtmServerFactory'=> static function(MediaWikiServices $services):TtmServerFactory { $config=$services->getMainConfig();$default=$config->get( 'TranslateTranslationDefaultService');if( $default===false) { $default=null;} return new TtmServerFactory( $config->get( 'TranslateTranslationServices'), $default);}]
@phpcs-require-sorted-array
Storage on CDB files.
retrieve( $forRebuild=false)
Storage on the object cache.
retrieve( $forRebuild=false)
Storage on the database itself.
retrieve( $forRebuild=false)
Storage on hash.
retrieve( $forRebuild=false)
clearMessageGroupStats(array $diff)
Purge stuff when set of keys have changed.
Minimal service container.
Definition Services.php:38
Class for pointing to messages, like Title class is for titles.
getTitle()
Get the original title.
getKey()
Returns the identified or guessed message key.
Creates a database of keys in all groups, so that namespace and key can be used to get the groups the...
clearMessageGroupStats(array $diff)
Purge stuff when set of keys have changed.
checkAndAdd(&$hugearray, MessageGroup $g, $ignore=false)
retrieve( $forRebuild=false)
static getPrimaryGroupId(MessageHandle $handle)
serialize( $data)
These are probably slower than serialize and unserialize, but they are more space efficient because w...
rebuild(float $timestamp=null)
Creates the index from scratch.
static singleton()
static setInstance(self $instance)
Override the global instance, for testing.
static getArrayDiff(array $old, array $new)
Compares two associative arrays.
static getGroupIds(MessageHandle $handle)
Retrieves a list of groups given MessageHandle belongs to.
Storage on serialized file.
retrieve( $forRebuild=false)
static cacheFile( $filename)
Gets the path for cache files.
static normaliseKey( $namespace, $key)
Converts page name and namespace to message index format.
Interface for message groups.
getNamespace()
Returns the namespace where messages are placed.
getId()
Returns the unique identifier for this group.
getKeys()
Shortcut for array_keys( getDefinitions() ) that can be optimized by the implementing classes.