14use MediaWiki\Logger\LoggerFactory;
15use MediaWiki\MediaWikiServices;
26 private const CACHEKEY =
'Translate-MessageIndex-interim';
29 protected static $instance;
31 private static $keysCache;
33 protected $interimCache;
37 private $jobQueueGroup;
39 public function __construct() {
41 $mwInstance = MediaWikiServices::getInstance();
42 $this->statusCache = $mwInstance->getMainWANObjectCache();
43 $this->jobQueueGroup = $mwInstance->getJobQueueGroup();
51 if ( self::$instance === null ) {
52 self::$instance = Services::getInstance()->getMessageIndex();
55 return self::$instance;
65 self::$instance = $instance;
75 global $wgTranslateMessageNamespaces;
79 if ( !$title->inNamespaces( $wgTranslateMessageNamespaces ) ) {
83 $namespace = $title->getNamespace();
85 $normkey = TranslateUtils::normaliseKey( $namespace, $key );
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 );
98 private static function getCache() {
99 if ( self::$keysCache ===
null ) {
100 self::$keysCache =
new MapCacheLRU( 30 );
102 return self::$keysCache;
111 $groups = self::getGroupIds( $handle );
113 return count( $groups ) ? array_shift( $groups ) : null;
116 private function getWithCache( $key ) {
117 $interimCacheValue = $this->getInterimCache()->get( self::CACHEKEY );
118 if ( $interimCacheValue && isset( $interimCacheValue[
'newKeys'][$key] ) ) {
119 return $interimCacheValue[
'newKeys'][$key];
122 return $this->
get( $key );
131 protected function get( $key ) {
133 $mi = $this->retrieve();
134 return $mi[$key] ??
null;
141 abstract public function retrieve( $forRebuild =
false );
148 return array_keys( $this->retrieve() );
151 abstract protected function store( array $array, array $diff );
153 protected function lock() {
157 protected function unlock() {
168 public function rebuild(
float $timestamp =
null ): array {
169 $logger = LoggerFactory::getInstance(
'Translate' );
171 static $recursion = 0;
173 if ( $recursion > 0 ) {
174 $msg = __METHOD__ .
': trying to recurse - building the index first time?';
183 '[MessageIndex] Started rebuild. Initiated by {callers}',
184 [
'callers' => wfGetAllCallers( 20 ) ]
187 $groups = MessageGroups::singleton()->getGroups();
189 $tsStart = microtime(
true );
190 if ( !$this->lock() ) {
194 $lockWaitDuration = microtime(
true ) - $tsStart;
196 '[MessageIndex] Got lock in {duration}',
197 [
'duration' => $lockWaitDuration ]
200 self::getCache()->clear();
203 $old = $this->retrieve(
'rebuild' );
207 foreach ( $groups as $g ) {
208 if ( !$g->exists() ) {
210 wfWarn( __METHOD__ .
": group '$id' is registered but does not exist" );
215 if ( $g->isMeta() ) {
220 $this->checkAndAdd( $new, $g );
223 foreach ( $postponed as $g ) {
224 $this->checkAndAdd( $new, $g,
true );
227 $diff = self::getArrayDiff( $old, $new );
228 $this->store( $new, $diff[
'keys'] );
231 $criticalSectionDuration = microtime(
true ) - $tsStart - $lockWaitDuration;
233 '[MessageIndex] Finished critical section in {duration}',
234 [
'duration' => $criticalSectionDuration ]
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 );
247 $this->jobQueueGroup->push( $job );
252 $this->statusCache->touchCheckKey( $this->getStatusCacheKey() );
254 $this->clearMessageGroupStats( $diff );
266 return $this->statusCache->makeKey(
'Translate',
'MessageIndex',
'status' );
269 private function getInterimCache(): BagOStuff {
270 return ObjectCache::getInstance( CACHE_ANYTHING );
273 public function storeInterim(
MessageGroup $group, array $newKeys ): void {
274 $namespace = $group->getNamespace();
275 $id = $group->
getId();
277 $normalizedNewKeys = [];
278 foreach ( $newKeys as $key ) {
282 $cache = $this->getInterimCache();
284 $interimCacheValue = $cache->get( self::CACHEKEY, $cache::READ_LATEST );
285 if ( $interimCacheValue ) {
286 $normalizedNewKeys = array_merge( $interimCacheValue[
'newKeys'], $normalizedNewKeys );
290 'timestamp' => microtime(
true ),
291 'newKeys' => $normalizedNewKeys,
294 $cache->set( self::CACHEKEY, $value, $cache::TTL_DAY );
327 $record =
static function ( $groups ) use ( &$values ) {
328 foreach ( $groups as $group ) {
329 $values[$group] =
true;
339 foreach ( $new as $key => $groups ) {
340 if ( !isset( $old[$key] ) ) {
341 $keys[
'add'][$key] = [ [], (array)$groups ];
342 $record( (array)$groups );
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] ) );
351 foreach ( $old as $key => $groups ) {
352 if ( !isset( $new[$key] ) ) {
353 $keys[
'del'][$key] = [ (array)$groups, [] ];
354 $record( (array)$groups );
361 'values' => array_keys( $values ),
371 $job = MessageGroupStatsRebuildJob::newRefreshGroupsJob( $diff[
'values'] );
372 $this->jobQueueGroup->push( $job );
374 foreach ( $diff[
'keys'] as $keys ) {
375 foreach ( $keys as $key => $data ) {
376 [ $ns, $pagename ] = explode(
':', $key, 2 );
377 $title = Title::makeTitle( $ns, $pagename );
379 [ $oldGroups, $newGroups ] = $data;
380 Hooks::run(
'TranslateEventMessageMembershipChange',
381 [ $handle, $oldGroups, $newGroups ] );
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] ) ) {
403 $to = implode(
', ', (array)$hugearray[$key] );
404 wfWarn(
"Key $key already belongs to $to, conflict with $id" );
407 if ( is_array( $hugearray[$key] ) ) {
409 $hugearray[$key][] = & $id;
414 $value = & $hugearray[$key];
415 unset( $hugearray[$key] );
416 $hugearray[$key] = [ &$value, &$id ];
419 $hugearray[$key] = & $id;
433 if ( is_array( $data ) ) {
434 return implode(
'|', $data );
440 protected function unserialize( $data ) {
441 if ( strpos( $data,
'|' ) !==
false ) {
442 return explode(
'|', $data );
466 protected $filename =
'translate_messageindex.ser';
473 if ( $this->index !==
null ) {
477 $file = TranslateUtils::cacheFile( $this->filename );
478 if ( file_exists( $file ) ) {
479 $this->index = unserialize( file_get_contents( $file ) );
481 $this->index = $this->
rebuild();
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;
509 protected function lock() {
510 $dbw = wfGetDB( DB_PRIMARY );
514 $ok = $dbw->lock(
'translate-messageindex', __METHOD__, 30 );
516 $dbw->commit( __METHOD__,
'flush' );
522 protected function unlock() {
524 $dbw = wfGetDB( DB_PRIMARY );
526 if ( !$dbw->trxLevel() ) {
527 $dbw->unlock(
'translate-messageindex', $fname );
528 } elseif ( is_callable( [ $dbw,
'onTransactionResolution' ] ) ) {
529 $dbw->onTransactionResolution(
static function () use ( $dbw, $fname ) {
530 $dbw->unlock(
'translate-messageindex', $fname );
533 $dbw->onTransactionCommitOrIdle(
static function () use ( $dbw, $fname ) {
534 $dbw->unlock(
'translate-messageindex', $fname );
546 if ( $this->index !==
null && !$forRebuild ) {
550 $dbr = wfGetDB( $forRebuild ? DB_PRIMARY : DB_REPLICA );
551 $res = $dbr->select(
'translate_messageindex',
'*', [], __METHOD__ );
553 foreach ( $res as $row ) {
554 $this->index[$row->tmi_key] = $this->unserialize( $row->tmi_value );
560 protected function get( $key ) {
561 $dbr = wfGetDB( DB_REPLICA );
562 $value = $dbr->selectField(
563 'translate_messageindex',
565 [
'tmi_key' => $key ],
569 if ( is_string( $value ) ) {
570 $value = $this->unserialize( $value );
578 protected function store( array $array, array $diff ) {
581 foreach ( [ $diff[
'add'], $diff[
'mod'] ] as $changes ) {
582 foreach ( $changes as $key => $data ) {
591 $index = [
'tmi_key' ];
592 $deletions = array_keys( $diff[
'del'] );
594 $dbw = wfGetDB( DB_PRIMARY );
595 $dbw->startAtomic( __METHOD__ );
597 if ( $updates !== [] ) {
598 $dbw->replace(
'translate_messageindex', [ $index ], $updates, __METHOD__ );
601 if ( $deletions !== [] ) {
602 $dbw->delete(
'translate_messageindex', [
'tmi_key' => $deletions ], __METHOD__ );
605 $dbw->endAtomic( __METHOD__ );
607 $this->index = $array;
620 protected $key =
'translate-messageindex';
625 protected function __construct() {
626 parent::__construct();
627 $this->cache = ObjectCache::getInstance( CACHE_ANYTHING );
635 if ( $this->index !==
null ) {
639 $key = $this->cache->makeKey( $this->key );
640 $data = $this->cache->get( $key );
641 if ( is_array( $data ) ) {
642 $this->index = $data;
644 $this->index = $this->
rebuild();
650 protected function store( array $array, array $diff ) {
651 $key = $this->cache->makeKey( $this->key );
652 $this->cache->set( $key, $array );
654 $this->index = $array;
677 protected $filename =
'translate_messageindex.cdb';
684 $reader = $this->getReader();
686 if ( $this->index !==
null ) {
691 foreach ( $this->
getKeys() as $key ) {
692 $this->index[$key] = $this->unserialize( $reader->get( $key ) );
699 $reader = $this->getReader();
702 $key = $keys === [] ? $reader->firstkey() : $reader->nextkey();
703 if ( $key ===
false ) {
712 protected function get( $key ) {
713 $reader = $this->getReader();
715 if ( $this->index !==
null ) {
716 return $this->index[$key] ??
null;
719 $value = $reader->get( $key );
720 if ( !is_string( $value ) ) {
723 $value = $this->unserialize( $value );
729 protected function store( array $array, array $diff ) {
730 $this->reader =
null;
732 $file = TranslateUtils::cacheFile( $this->filename );
733 $cache = Writer::open( $file );
735 foreach ( $array as $key => $value ) {
737 $cache->set( $key, $value );
742 $this->index = $array;
745 protected function getReader() {
746 if ( $this->reader ) {
747 return $this->reader;
751 if ( !file_exists( $file ) ) {
753 $this->store( [], [] );
754 $this->index = $this->
rebuild();
757 $this->reader = Reader::open( $file );
758 return $this->reader;
771 protected $index = [];
786 protected function get( $key ) {
787 return $this->index[$key] ??
null;
790 protected function store( array $array, array $diff ) {
791 $this->index = $array;
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
retrieve( $forRebuild=false)
Storage on the object cache.
retrieve( $forRebuild=false)
Storage on the database itself.
retrieve( $forRebuild=false)
retrieve( $forRebuild=false)
clearMessageGroupStats(array $diff)
Purge stuff when set of keys have changed.
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 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.