34 private const CACHE_KEY =
'Translate-MessageIndex-interim';
35 private const READ_LATEST =
true;
37 private MapCacheLRU $keysCache;
38 protected BagOStuff $interimCache;
39 private WANObjectCache $statusCache;
40 private JobQueueGroup $jobQueueGroup;
42 private LoggerInterface $logger;
43 private IConnectionProvider $dbProvider;
44 private array $translateMessageNamespaces;
45 public const SERVICE_OPTIONS = [
46 'TranslateMessageNamespaces'
49 public function __construct(
51 WANObjectCache $statusCache,
52 JobQueueGroup $jobQueueGroup,
54 LoggerInterface $logger,
55 BagOStuff $interimCache,
56 IConnectionProvider $dbProvider,
57 ServiceOptions $options
59 $this->messageIndexStore = $store;
60 $this->keysCache =
new MapCacheLRU( 30 );
61 $this->statusCache = $statusCache;
62 $this->jobQueueGroup = $jobQueueGroup;
63 $this->hookRunner = $hookRunner;
64 $this->logger = $logger;
65 $this->interimCache = $interimCache;
66 $this->dbProvider = $dbProvider;
67 $options->assertRequiredOptions( self::SERVICE_OPTIONS );
68 $this->translateMessageNamespaces = $options->get(
'TranslateMessageNamespaces' );
72 private function normaliseKey(
int $namespace,
string $key ):
string {
73 $key = lcfirst( $key );
75 return strtr(
"$namespace:$key",
' ',
'_' );
83 $title = $handle->getTitle();
85 if ( !$title->inNamespaces( $this->translateMessageNamespaces ) ) {
89 $namespace = $title->getNamespace();
91 $normalisedKey = $this->normaliseKey( $namespace, $key );
93 $value = $this->keysCache->get( $normalisedKey );
94 if ( $value ===
null ) {
95 $value = (array)$this->getWithCache( $normalisedKey );
96 $this->keysCache->set( $normalisedKey, $value );
110 $normalisedKey = $this->normaliseKey( $namespace, $title );
115 return (array)$this->
get( $normalisedKey ) ?? [];
118 public function getPrimaryGroupId(
MessageHandle $handle ): ?string {
119 $groups = $this->getGroupIds( $handle );
121 return count( $groups ) ? array_shift( $groups ) : null;
125 private function getWithCache(
string $key ) {
126 $interimCacheValue = $this->getInterimCache()->get( self::CACHE_KEY );
127 if ( $interimCacheValue && isset( $interimCacheValue[
'newKeys'][$key] ) ) {
128 $this->logger->debug(
129 '[MessageIndex] interim cache hit: {messageKey} with value {groupId}',
130 [
'messageKey' => $key,
'groupId' => $interimCacheValue[
'newKeys'][$key] ]
132 return $interimCacheValue[
'newKeys'][$key];
135 return $this->messageIndexStore->get( $key );
138 public function get(
string $key ) {
139 return $this->messageIndexStore->get( $key );
144 return $this->messageIndexStore->getKeys();
147 private function lock(): bool {
148 $dbw = $this->dbProvider->getPrimaryDatabase();
152 $ok = $dbw->lock(
'translate-messageindex', __METHOD__, 5 );
154 $dbw->commit( __METHOD__,
'flush' );
160 private function unlock(): void {
162 $dbw = $this->dbProvider->getPrimaryDatabase();
164 if ( !$dbw->trxLevel() ) {
165 $dbw->unlock(
'translate-messageindex', $fname );
167 $dbw->onTransactionResolution(
static function () use ( $dbw, $fname ) {
168 $dbw->unlock(
'translate-messageindex', $fname );
179 public function rebuild( ?
float $timestamp =
null ): array {
180 static $recursion = 0;
182 if ( $recursion > 0 ) {
183 $msg = __METHOD__ .
': trying to recurse - building the index first time?';
191 $this->logger->info(
'[MessageIndex] Started rebuild.' );
193 $tsStart = microtime(
true );
194 if ( !$this->lock() ) {
198 $lockWaitDuration = microtime(
true ) - $tsStart;
200 '[MessageIndex] Got lock in {duration}',
201 [
'duration' => $lockWaitDuration ]
204 $groups = MessageGroups::singleton()->getGroups();
205 $this->keysCache->clear();
208 $old = $this->messageIndexStore->retrieve( self::READ_LATEST );
211 foreach ( $groups as $messageGroup ) {
212 if ( !$messageGroup->exists() ) {
213 $id = $messageGroup->getId();
214 wfWarn( __METHOD__ .
": group '$id' is registered but does not exist" );
219 if ( $messageGroup->isMeta() ) {
220 $postponed[] = $messageGroup;
224 $this->checkAndAdd( $new, $messageGroup );
227 foreach ( $postponed as $messageGroup ) {
228 $this->checkAndAdd( $new, $messageGroup,
true );
231 $diff = self::getArrayDiff( $old, $new );
232 $this->messageIndexStore->store( $new, $diff[
'keys'] );
234 $cache = $this->getInterimCache();
235 $interimCacheValue = $cache->get( self::CACHE_KEY );
236 if ( $interimCacheValue ) {
237 $timestamp ??= microtime(
true );
238 if ( $interimCacheValue[
'timestamp'] <= $timestamp ) {
239 $cache->delete( self::CACHE_KEY );
240 $this->logger->debug(
241 '[MessageIndex] Deleted interim cache with timestamp {cacheTimestamp} <= {currentTimestamp}.',
243 'cacheTimestamp' => $interimCacheValue[
'timestamp'],
244 'currentTimestamp' => $timestamp,
250 $job = RebuildMessageIndexJob::newJob( __METHOD__ );
251 $this->jobQueueGroup->push( $job );
252 $this->logger->debug(
253 '[MessageIndex] Kept interim cache with timestamp {cacheTimestamp} > {currentTimestamp}.',
255 'cacheTimestamp' => $interimCacheValue[
'timestamp'],
256 'currentTimestamp' => $timestamp,
263 $criticalSectionDuration = microtime(
true ) - $tsStart - $lockWaitDuration;
265 '[MessageIndex] Finished critical section in {duration}',
266 [
'duration' => $criticalSectionDuration ]
270 $this->statusCache->touchCheckKey( $this->getStatusCacheKey() );
272 $this->clearMessageGroupStats( $diff );
279 public function getStatusCacheKey(): string {
280 return $this->statusCache->makeKey(
'Translate',
'MessageIndex',
'status' );
283 private function getInterimCache(): BagOStuff {
284 return $this->interimCache;
287 public function storeInterim(
MessageGroup $group, array $newKeys ): void {
288 $namespace = $group->getNamespace();
289 $id = $group->
getId();
291 $normalizedNewKeys = [];
292 foreach ( $newKeys as $key ) {
293 $normalizedNewKeys[$this->normaliseKey( $namespace, $key )] = $id;
296 $cache = $this->getInterimCache();
298 $interimCacheValue = $cache->get( self::CACHE_KEY, $cache::READ_LATEST );
299 if ( $interimCacheValue ) {
300 $normalizedNewKeys = array_merge( $interimCacheValue[
'newKeys'], $normalizedNewKeys );
301 $this->logger->debug(
302 '[MessageIndex] interim cache: merging with existing cache of size {count}',
303 [
'count' => count( $interimCacheValue[
'newKeys'] ) ]
308 'timestamp' => microtime(
true ),
309 'newKeys' => $normalizedNewKeys,
312 $cache->set( self::CACHE_KEY, $value, $cache::TTL_DAY );
313 $this->logger->debug(
314 '[MessageIndex] interim cache: added group {groupId} with new size {count} keys and ' .
315 'timestamp {cacheTimestamp}',
316 [
'groupId' => $id,
'count' => count( $normalizedNewKeys ),
'cacheTimestamp' => $value[
'timestamp'] ]
350 $record =
static function ( $groups ) use ( &$values ) {
351 foreach ( $groups as $group ) {
352 $values[$group] =
true;
362 foreach ( $new as $key => $groups ) {
363 if ( !isset( $old[$key] ) ) {
364 $keys[
'add'][$key] = [ [], (array)$groups ];
365 $record( (array)$groups );
367 } elseif ( $groups != $old[$key] ) {
368 $keys[
'mod'][$key] = [ (array)$old[$key], (array)$groups ];
369 $record( array_diff( (array)$old[$key], (array)$groups ) );
370 $record( array_diff( (array)$groups, (array)$old[$key] ) );
374 foreach ( $old as $key => $groups ) {
375 if ( !isset( $new[$key] ) ) {
376 $keys[
'del'][$key] = [ (array)$groups, [] ];
377 $record( (array)$groups );
384 'values' => array_keys( $values ),
391 $this->jobQueueGroup->push( $job );
393 foreach ( $diff[
'keys'] as $keys ) {
394 foreach ( $keys as $key => $data ) {
395 [ $ns, $pageName ] = explode(
':', $key, 2 );
396 $title = Title::makeTitle( (
int)$ns, $pageName );
398 [ $oldGroups, $newGroups ] = $data;
399 $this->hookRunner->onTranslateEventMessageMembershipChange(
400 $handle, $oldGroups, $newGroups );
405 protected function checkAndAdd( array &$hugeArray,
MessageGroup $g,
bool $ignore =
false ): void {
406 $keys = $g->getKeys();
410 foreach ( $keys as $key ) {
411 # Force all keys to lower case, because the case doesn't matter and it is
412 # easier to do comparing when the case of first letter is unknown, because
413 # mediawiki forces it to upper case
414 $key = $this->normaliseKey( $namespace, $key );
415 if ( isset( $hugeArray[$key] ) ) {
417 $to = implode(
', ', (array)$hugeArray[$key] );
418 wfWarn(
"Key $key already belongs to $to, conflict with $id" );
421 if ( is_array( $hugeArray[$key] ) ) {
423 $hugeArray[$key][] = & $id;
428 $value = & $hugeArray[$key];
429 unset( $hugeArray[$key] );
430 $hugeArray[$key] = [ &$value, &$id ];
433 $hugeArray[$key] = & $id;
return[ 'Translate:AggregateGroupManager'=> static function(MediaWikiServices $services):AggregateGroupManager { return new AggregateGroupManager($services->getTitleFactory(), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:AggregateGroupMessageGroupFactory'=> static function(MediaWikiServices $services):AggregateGroupMessageGroupFactory { return new AggregateGroupMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'));}, '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:ExternalMessageSourceStateComparator'=> static function(MediaWikiServices $services):ExternalMessageSourceStateComparator { return new ExternalMessageSourceStateComparator(new SimpleStringComparator(), $services->getRevisionLookup(), $services->getPageStore());}, 'Translate:ExternalMessageSourceStateImporter'=> static function(MediaWikiServices $services):ExternalMessageSourceStateImporter { return new ExternalMessageSourceStateImporter($services->get( 'Translate:GroupSynchronizationCache'), $services->getJobQueueGroup(), LoggerFactory::getInstance(LogNames::GROUP_SYNCHRONIZATION), $services->get( 'Translate:MessageIndex'), $services->getTitleFactory(), $services->get( 'Translate:MessageGroupSubscription'), new ServiceOptions(ExternalMessageSourceStateImporter::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:FileBasedMessageGroupFactory'=> static function(MediaWikiServices $services):FileBasedMessageGroupFactory { return new FileBasedMessageGroupFactory(new MessageGroupConfigurationParser(), new ServiceOptions(FileBasedMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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:HookDefinedMessageGroupFactory'=> static function(MediaWikiServices $services):HookDefinedMessageGroupFactory { return new HookDefinedMessageGroupFactory( $services->get( 'Translate:HookRunner'));}, 'Translate:HookRunner'=> static function(MediaWikiServices $services):HookRunner { return new HookRunner( $services->getHookContainer());}, 'Translate:MessageBundleDependencyPurger'=> static function(MediaWikiServices $services):MessageBundleDependencyPurger { return new MessageBundleDependencyPurger( $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:MessageBundleMessageGroupFactory'=> static function(MediaWikiServices $services):MessageBundleMessageGroupFactory { return new MessageBundleMessageGroupFactory($services->get( 'Translate:MessageGroupMetadata'), new ServiceOptions(MessageBundleMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:MessageBundleStore'=> static function(MediaWikiServices $services):MessageBundleStore { return new MessageBundleStore($services->get( 'Translate:RevTagStore'), $services->getJobQueueGroup(), $services->getLanguageNameUtils(), $services->get( 'Translate:MessageIndex'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:MessageBundleTranslationLoader'=> static function(MediaWikiServices $services):MessageBundleTranslationLoader { return new MessageBundleTranslationLoader( $services->getLanguageFallback());}, 'Translate:MessageGroupMetadata'=> static function(MediaWikiServices $services):MessageGroupMetadata { return new MessageGroupMetadata( $services->getConnectionProvider());}, 'Translate:MessageGroupReviewStore'=> static function(MediaWikiServices $services):MessageGroupReviewStore { return new MessageGroupReviewStore($services->getConnectionProvider(), $services->get( 'Translate:HookRunner'));}, 'Translate:MessageGroupStatsTableFactory'=> static function(MediaWikiServices $services):MessageGroupStatsTableFactory { return new MessageGroupStatsTableFactory($services->get( 'Translate:ProgressStatsTableFactory'), $services->getLinkRenderer(), $services->get( 'Translate:MessageGroupReviewStore'), $services->get( 'Translate:MessageGroupMetadata'), $services->getMainConfig() ->get( 'TranslateWorkflowStates') !==false);}, 'Translate:MessageGroupSubscription'=> static function(MediaWikiServices $services):MessageGroupSubscription { return new MessageGroupSubscription($services->get( 'Translate:MessageGroupSubscriptionStore'), $services->getJobQueueGroup(), $services->getUserIdentityLookup(), LoggerFactory::getInstance(LogNames::GROUP_SUBSCRIPTION), new ServiceOptions(MessageGroupSubscription::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:MessageGroupSubscriptionHookHandler'=> static function(MediaWikiServices $services):MessageGroupSubscriptionHookHandler { return new MessageGroupSubscriptionHookHandler($services->get( 'Translate:MessageGroupSubscription'), $services->getUserFactory());}, 'Translate:MessageGroupSubscriptionStore'=> static function(MediaWikiServices $services):MessageGroupSubscriptionStore { return new MessageGroupSubscriptionStore( $services->getConnectionProvider());}, 'Translate:MessageIndex'=> static function(MediaWikiServices $services):MessageIndex { $params=(array) $services->getMainConfig() ->get( 'TranslateMessageIndex');$class=array_shift( $params);$implementationMap=['HashMessageIndex'=> HashMessageIndex::class, 'CDBMessageIndex'=> CDBMessageIndex::class, 'DatabaseMessageIndex'=> DatabaseMessageIndex::class, 'hash'=> HashMessageIndex::class, 'cdb'=> CDBMessageIndex::class, 'database'=> DatabaseMessageIndex::class,];$messageIndexStoreClass=$implementationMap[$class] ?? $implementationMap['database'];return new MessageIndex(new $messageIndexStoreClass, $services->getMainWANObjectCache(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), LoggerFactory::getInstance(LogNames::MAIN), $services->getMainObjectStash(), $services->getConnectionProvider(), new ServiceOptions(MessageIndex::SERVICE_OPTIONS, $services->getMainConfig()),);}, '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->getConnectionProvider(), $services->getJsonCodec());}, 'Translate:ProgressStatsTableFactory'=> static function(MediaWikiServices $services):ProgressStatsTableFactory { return new ProgressStatsTableFactory($services->getLinkRenderer(), $services->get( 'Translate:ConfigHelper'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:RevTagStore'=> static function(MediaWikiServices $services):RevTagStore { return new RevTagStore( $services->getConnectionProvider());}, 'Translate:SubpageListBuilder'=> static function(MediaWikiServices $services):SubpageListBuilder { return new SubpageListBuilder($services->get( 'Translate:TranslatableBundleFactory'), $services->getLinkBatchFactory());}, 'Translate:TranslatableBundleDeleter'=> static function(MediaWikiServices $services):TranslatableBundleDeleter { return new TranslatableBundleDeleter($services->getMainObjectStash(), $services->getJobQueueGroup(), $services->get( 'Translate:SubpageListBuilder'), $services->get( 'Translate:TranslatableBundleFactory'));}, 'Translate:TranslatableBundleExporter'=> static function(MediaWikiServices $services):TranslatableBundleExporter { return new TranslatableBundleExporter($services->get( 'Translate:SubpageListBuilder'), $services->getWikiExporterFactory(), $services->getConnectionProvider());}, '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(), $services->getNamespaceInfo(), $services->getTitleFactory(), $services->getFormatterFactory());}, '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->getConnectionProvider(), $services->getObjectCacheFactory(), $services->getMainConfig() ->get( 'TranslatePageMoveLimit'));}, 'Translate:TranslatableBundleStatusStore'=> static function(MediaWikiServices $services):TranslatableBundleStatusStore { return new TranslatableBundleStatusStore($services->getConnectionProvider() ->getPrimaryDatabase(), $services->getCollationFactory() ->makeCollation( 'uca-default-u-kn'), $services->getDBLoadBalancer() ->getMaintenanceConnectionRef(DB_PRIMARY));}, 'Translate:TranslatablePageMarker'=> static function(MediaWikiServices $services):TranslatablePageMarker { return new TranslatablePageMarker($services->getConnectionProvider(), $services->getJobQueueGroup(), $services->getLinkRenderer(), MessageGroups::singleton(), $services->get( 'Translate:MessageIndex'), $services->getTitleFormatter(), $services->getTitleParser(), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:TranslatablePageStore'), $services->get( 'Translate:TranslatablePageStateStore'), $services->get( 'Translate:TranslationUnitStoreFactory'), $services->get( 'Translate:MessageGroupMetadata'), $services->getWikiPageFactory(), $services->get( 'Translate:TranslatablePageView'), $services->get( 'Translate:MessageGroupSubscription'), $services->getFormatterFactory());}, 'Translate:TranslatablePageMessageGroupFactory'=> static function(MediaWikiServices $services):TranslatablePageMessageGroupFactory { return new TranslatablePageMessageGroupFactory(new ServiceOptions(TranslatablePageMessageGroupFactory::SERVICE_OPTIONS, $services->getMainConfig()),);}, 'Translate:TranslatablePageParser'=> static function(MediaWikiServices $services):TranslatablePageParser { return new TranslatablePageParser($services->get( 'Translate:ParsingPlaceholderFactory'));}, 'Translate:TranslatablePageStateStore'=> static function(MediaWikiServices $services):TranslatablePageStateStore { return new TranslatablePageStateStore($services->get( 'Translate:PersistentCache'), $services->getPageStore());}, 'Translate:TranslatablePageStore'=> static function(MediaWikiServices $services):TranslatablePageStore { return new TranslatablePageStore($services->get( 'Translate:MessageIndex'), $services->getJobQueueGroup(), $services->get( 'Translate:RevTagStore'), $services->getConnectionProvider(), $services->get( 'Translate:TranslatableBundleStatusStore'), $services->get( 'Translate:TranslatablePageParser'), $services->get( 'Translate:MessageGroupMetadata'));}, 'Translate:TranslatablePageView'=> static function(MediaWikiServices $services):TranslatablePageView { return new TranslatablePageView($services->getConnectionProvider(), $services->get( 'Translate:TranslatablePageStateStore'), new ServiceOptions(TranslatablePageView::SERVICE_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslateSandbox'=> static function(MediaWikiServices $services):TranslateSandbox { return new TranslateSandbox($services->getUserFactory(), $services->getConnectionProvider(), $services->getPermissionManager(), $services->getAuthManager(), $services->getUserGroupManager(), $services->getActorStore(), $services->getUserOptionsManager(), $services->getJobQueueGroup(), $services->get( 'Translate:HookRunner'), new ServiceOptions(TranslateSandbox::CONSTRUCTOR_OPTIONS, $services->getMainConfig()));}, 'Translate:TranslationStashReader'=> static function(MediaWikiServices $services):TranslationStashReader { return new TranslationStashStorage( $services->getConnectionProvider() ->getPrimaryDatabase());}, 'Translate:TranslationStatsDataProvider'=> static function(MediaWikiServices $services):TranslationStatsDataProvider { return new TranslationStatsDataProvider(new ServiceOptions(TranslationStatsDataProvider::CONSTRUCTOR_OPTIONS, $services->getMainConfig()), $services->getObjectFactory(), $services->getConnectionProvider());}, '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