29 private const CACHEKEY =
'Translate-MessageIndex-interim';
31 private const READ_LATEST =
true;
34 protected static $instance;
36 private static $keysCache;
38 protected $interimCache;
42 private $jobQueueGroup;
46 public function __construct() {
48 $mwInstance = MediaWikiServices::getInstance();
49 $this->statusCache = $mwInstance->getMainWANObjectCache();
50 $this->jobQueueGroup = $mwInstance->getJobQueueGroup();
51 $this->hookRunner = Services::getInstance()->getHookRunner();
59 if ( self::$instance === null ) {
60 self::$instance = Services::getInstance()->getMessageIndex();
63 return self::$instance;
73 self::$instance = $instance;
83 global $wgTranslateMessageNamespaces;
87 if ( !$title->inNamespaces( $wgTranslateMessageNamespaces ) ) {
91 $namespace = $title->getNamespace();
93 $normkey = Utilities::normaliseKey( $namespace, $key );
95 $cache = self::getCache();
96 $value = $cache->get( $normkey );
97 if ( $value ===
null ) {
98 $value = (array)self::singleton()->getWithCache( $normkey );
99 $cache->set( $normkey, $value );
106 private static function getCache() {
107 if ( self::$keysCache ===
null ) {
108 self::$keysCache =
new MapCacheLRU( 30 );
110 return self::$keysCache;
119 $groups = self::getGroupIds( $handle );
121 return count( $groups ) ? array_shift( $groups ) : null;
124 private function getWithCache( $key ) {
125 $interimCacheValue = $this->getInterimCache()->get( self::CACHEKEY );
126 if ( $interimCacheValue && isset( $interimCacheValue[
'newKeys'][$key] ) ) {
127 return $interimCacheValue[
'newKeys'][$key];
130 return $this->
get( $key );
139 protected function get( $key ) {
141 $mi = $this->retrieve();
142 return $mi[$key] ??
null;
145 abstract public function retrieve(
bool $readLatest =
false ): array;
152 return array_keys( $this->retrieve() );
155 abstract protected function store( array $array, array $diff );
157 protected function lock() {
161 protected function unlock() {
172 public function rebuild(
float $timestamp =
null ): array {
173 $logger = LoggerFactory::getInstance(
'Translate' );
175 static $recursion = 0;
177 if ( $recursion > 0 ) {
178 $msg = __METHOD__ .
': trying to recurse - building the index first time?';
187 '[MessageIndex] Started rebuild. Initiated by {callers}',
188 [
'callers' => wfGetAllCallers( 20 ) ]
191 $groups = MessageGroups::singleton()->getGroups();
193 $tsStart = microtime(
true );
194 if ( !$this->lock() ) {
198 $lockWaitDuration = microtime(
true ) - $tsStart;
200 '[MessageIndex] Got lock in {duration}',
201 [
'duration' => $lockWaitDuration ]
204 self::getCache()->clear();
207 $old = $this->retrieve( self::READ_LATEST );
211 foreach ( $groups as $g ) {
212 if ( !$g->exists() ) {
214 wfWarn( __METHOD__ .
": group '$id' is registered but does not exist" );
219 if ( $g->isMeta() ) {
224 $this->checkAndAdd( $new, $g );
227 foreach ( $postponed as $g ) {
228 $this->checkAndAdd( $new, $g,
true );
231 $diff = self::getArrayDiff( $old, $new );
232 $this->store( $new, $diff[
'keys'] );
235 $criticalSectionDuration = microtime(
true ) - $tsStart - $lockWaitDuration;
237 '[MessageIndex] Finished critical section in {duration}',
238 [
'duration' => $criticalSectionDuration ]
241 $cache = $this->getInterimCache();
242 $interimCacheValue = $cache->get( self::CACHEKEY );
243 if ( $interimCacheValue ) {
244 $timestamp ??= microtime(
true );
245 if ( $interimCacheValue[
'timestamp'] <= $timestamp ) {
246 $cache->delete( self::CACHEKEY );
251 $this->jobQueueGroup->push( $job );
256 $this->statusCache->touchCheckKey( $this->getStatusCacheKey() );
258 $this->clearMessageGroupStats( $diff );
270 return $this->statusCache->makeKey(
'Translate',
'MessageIndex',
'status' );
273 private function getInterimCache(): BagOStuff {
274 return ObjectCache::getInstance( CACHE_ANYTHING );
277 public function storeInterim(
MessageGroup $group, array $newKeys ): void {
278 $namespace = $group->getNamespace();
279 $id = $group->
getId();
281 $normalizedNewKeys = [];
282 foreach ( $newKeys as $key ) {
283 $normalizedNewKeys[Utilities::normaliseKey( $namespace, $key )] = $id;
286 $cache = $this->getInterimCache();
288 $interimCacheValue = $cache->get( self::CACHEKEY, $cache::READ_LATEST );
289 if ( $interimCacheValue ) {
290 $normalizedNewKeys = array_merge( $interimCacheValue[
'newKeys'], $normalizedNewKeys );
294 'timestamp' => microtime(
true ),
295 'newKeys' => $normalizedNewKeys,
298 $cache->set( self::CACHEKEY, $value, $cache::TTL_DAY );
331 $record =
static function ( $groups ) use ( &$values ) {
332 foreach ( $groups as $group ) {
333 $values[$group] =
true;
343 foreach ( $new as $key => $groups ) {
344 if ( !isset( $old[$key] ) ) {
345 $keys[
'add'][$key] = [ [], (array)$groups ];
346 $record( (array)$groups );
348 } elseif ( $groups != $old[$key] ) {
349 $keys[
'mod'][$key] = [ (array)$old[$key], (array)$groups ];
350 $record( array_diff( (array)$old[$key], (array)$groups ) );
351 $record( array_diff( (array)$groups, (array)$old[$key] ) );
355 foreach ( $old as $key => $groups ) {
356 if ( !isset( $new[$key] ) ) {
357 $keys[
'del'][$key] = [ (array)$groups, [] ];
358 $record( (array)$groups );
365 'values' => array_keys( $values ),
375 $job = MessageGroupStatsRebuildJob::newRefreshGroupsJob( $diff[
'values'] );
376 $this->jobQueueGroup->push( $job );
378 foreach ( $diff[
'keys'] as $keys ) {
379 foreach ( $keys as $key => $data ) {
380 [ $ns, $pagename ] = explode(
':', $key, 2 );
381 $title = Title::makeTitle( (
int)$ns, $pagename );
383 [ $oldGroups, $newGroups ] = $data;
384 $this->hookRunner->onTranslateEventMessageMembershipChange(
385 $handle, $oldGroups, $newGroups );
400 foreach ( $keys as $key ) {
401 # Force all keys to lower case, because the case doesn't matter and it is
402 # easier to do comparing when the case of first letter is unknown, because
403 # mediawiki forces it to upper case
404 $key = Utilities::normaliseKey( $namespace, $key );
405 if ( isset( $hugearray[$key] ) ) {
407 $to = implode(
', ', (array)$hugearray[$key] );
408 wfWarn(
"Key $key already belongs to $to, conflict with $id" );
411 if ( is_array( $hugearray[$key] ) ) {
413 $hugearray[$key][] = & $id;
418 $value = & $hugearray[$key];
419 unset( $hugearray[$key] );
420 $hugearray[$key] = [ &$value, &$id ];
423 $hugearray[$key] = & $id;
437 return is_array( $data ) ? implode(
'|', $data ) : $data;
440 protected function unserialize( $data ) {
441 $array = explode(
'|', $data );
442 return count( $array ) > 1 ? $array : $data;
502 protected function lock() {
503 $dbw = wfGetDB( DB_PRIMARY );
507 $ok = $dbw->lock(
'translate-messageindex', __METHOD__, 30 );
509 $dbw->commit( __METHOD__,
'flush' );
515 protected function unlock() {
517 $dbw = wfGetDB( DB_PRIMARY );
519 if ( !$dbw->trxLevel() ) {
520 $dbw->unlock(
'translate-messageindex', $fname );
521 } elseif ( is_callable( [ $dbw,
'onTransactionResolution' ] ) ) {
522 $dbw->onTransactionResolution(
static function () use ( $dbw, $fname ) {
523 $dbw->unlock(
'translate-messageindex', $fname );
526 $dbw->onTransactionCommitOrIdle(
static function () use ( $dbw, $fname ) {
527 $dbw->unlock(
'translate-messageindex', $fname );
534 public function retrieve(
bool $readLatest =
false ): array {
535 if ( $this->index !==
null && !$readLatest ) {
539 $dbr = wfGetDB( $readLatest ? DB_PRIMARY : DB_REPLICA );
540 $res = $dbr->select(
'translate_messageindex',
'*', [], __METHOD__ );
542 foreach ( $res as $row ) {
543 $this->index[$row->tmi_key] = $this->unserialize( $row->tmi_value );
549 protected function get( $key ) {
550 $dbr = wfGetDB( DB_REPLICA );
551 $value = $dbr->selectField(
552 'translate_messageindex',
554 [
'tmi_key' => $key ],
558 return is_string( $value ) ? $this->unserialize( $value ) :
null;
561 protected function store( array $array, array $diff ) {
564 foreach ( [ $diff[
'add'], $diff[
'mod'] ] as $changes ) {
565 foreach ( $changes as $key => $data ) {
574 $index = [
'tmi_key' ];
575 $deletions = array_keys( $diff[
'del'] );
577 $dbw = wfGetDB( DB_PRIMARY );
578 $dbw->startAtomic( __METHOD__ );
580 if ( $updates !== [] ) {
581 $dbw->replace(
'translate_messageindex', [ $index ], $updates, __METHOD__ );
584 if ( $deletions !== [] ) {
585 $dbw->delete(
'translate_messageindex', [
'tmi_key' => $deletions ], __METHOD__ );
588 $dbw->endAtomic( __METHOD__ );
590 $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'), $services->get( 'Translate:MessageIndex'));}, 'Translate:FileFormatFactory'=> static function(MediaWikiServices $services):FileFormatFactory { return new FileFormatFactory( $services->getObjectFactory());}, 'Translate:GroupSynchronizationCache'=> static function(MediaWikiServices $services):GroupSynchronizationCache { return new GroupSynchronizationCache( $services->get( 'Translate:PersistentCache'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'));}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getDBLoadBalancer(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getDBLoadBalancer(), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, '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:MessagePrefixStats'=> static function(MediaWikiServices $services):MessagePrefixStats { return new MessagePrefixStats( $services->getTitleParser());}, '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:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore($services->getDBLoadBalancerFactory());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getDBLoadBalancer());}, 'Translate:TranslatableBundleFactory'=> static function(MediaWikiServices $services):TranslatableBundleFactory { return new TranslatableBundleFactory($services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:MessageBundleStore'));}, 'Translate:TranslatableBundleImporter'=> static function(MediaWikiServices $services):TranslatableBundleImporter { return new TranslatableBundleImporter($services->getWikiImporterFactory(), $services->get( 'Translate:TranslatablePageParser'), $services->getRevisionLookup());}, '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:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getDBLoadBalancer() ->getConnection(DB_PRIMARY), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, '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(), $services->get( 'Translate:RevTagStore'), $services->getDBLoadBalancer(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'),);}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { $db=$services->getDBLoadBalancer() ->getConnection(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(), $services->getDBLoadBalancer());}, '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