2declare( strict_types = 1 );
7use InvalidArgumentException;
38 private $initialTimeoutSeconds;
40 private $incrementalTimeoutSeconds;
43 private const GROUP_LIST_TAG =
'gsc_%group_in_sync%';
45 private const GROUP_ERROR_TAG =
'gsc_%group_with_error%';
47 private const GROUP_IN_REVIEW_TAG =
'gsc_%group_in_review%';
51 public function __construct(
53 int $initialTimeoutSeconds = 2400,
54 int $incrementalTimeoutSeconds = 600
57 $this->cache = $cache;
58 $this->initialTimeoutSeconds = $initialTimeoutSeconds;
59 $this->incrementalTimeoutSeconds = $incrementalTimeoutSeconds;
67 $groupsInSyncEntries = $this->cache->getByTag( self::GROUP_LIST_TAG );
70 foreach ( $groupsInSyncEntries as $entry ) {
71 $groups[] = $entry->value();
79 $expTime = $this->getExpireTime( $this->initialTimeoutSeconds );
82 $this->getGroupKey( $groupId ),
90 public function getSyncEndTime(
string $groupId ): ?int {
91 $cacheEntry = $this->cache->get( $this->getGroupKey( $groupId ) );
92 return $cacheEntry ? $cacheEntry[0]->exptime() :
null;
96 public function endSync(
string $groupId ): void {
97 if ( $this->cache->hasEntryWithTag( $this->getGroupTag( $groupId ) ) ) {
98 throw new InvalidArgumentException(
99 'Cannot end synchronization for a group that still has messages to be processed.'
103 $groupKey = $this->getGroupKey( $groupId );
104 $this->cache->delete( $groupKey );
109 $this->cache->deleteEntriesWithTag( $this->getGroupTag( $groupId ) );
110 $this->endSync( $groupId );
116 $groupTag = $this->getGroupTag( $groupId );
117 foreach ( $messageParams as $messageParam ) {
118 $titleKey = $this->getMessageKeys( $groupId, $messageParam->getPageName() )[0];
127 $this->cache->set( ...$messagesToAdd );
132 $groupEntry = $this->cache->get( $this->getGroupKey( $groupId ) );
133 return $groupEntry !== [];
143 $messageEntries = $this->cache->getByTag( $this->getGroupTag( $groupId ) );
145 $allMessageParams = [];
146 foreach ( $messageEntries as $entry ) {
147 $message = $entry->value();
149 $allMessageParams[$message->getPageName()] = $message;
152 throw $this->invalidArgument( $message, MessageUpdateParameter::class );
156 return $allMessageParams;
161 $messageCacheKey = $this->getMessageKeys( $groupId, $messageKey );
162 return $this->cache->has( $messageCacheKey[0] );
167 if ( !$this->isGroupBeingProcessed( $groupId ) ) {
169 throw new LogicException(
170 'Sync requested for a group currently not being processed. Check if ' .
171 'group is being processed by calling isGroupBeingProcessed() first'
175 $remainingMessages = $this->getGroupMessages( $groupId );
178 if ( !$remainingMessages ) {
182 $syncExpTime = $this->getSyncEndTime( $groupId );
183 if ( $syncExpTime ===
null ) {
185 throw new RuntimeException(
186 "Unexpected condition. Group: $groupId; Messages present, but group key not found."
190 $hasTimedOut = $this->hasGroupTimedOut( $syncExpTime );
192 return new GroupSynchronizationResponse(
200 public function removeMessages(
string $groupId,
string ...$messageKeys ): void {
201 $messageCacheKeys = $this->getMessageKeys( $groupId, ...$messageKeys );
203 $this->cache->delete( ...$messageCacheKeys );
207 $groupId = $response->getGroupId();
210 if ( !$remainingMessages ) {
211 throw new LogicException(
'Cannot add a group without any remaining messages to the errors list' );
214 $groupMessageErrorTag = $this->getGroupMessageErrorTag( $groupId );
217 foreach ( $remainingMessages as $messageParam ) {
218 $titleErrorKey = $this->getMessageErrorKey( $groupId, $messageParam->getPageName() )[0];
219 $entriesToSave[] =
new PersistentCacheEntry(
223 $groupMessageErrorTag
227 $this->cache->set( ...$entriesToSave );
229 $groupErrorKey = $this->getGroupErrorKey( $groupId );
232 $groupInfo = $this->cache->get( $groupErrorKey );
241 $trimmedGroupSyncResponse =
new GroupSynchronizationResponse(
244 $response->hasTimedOut()
247 $entriesToSave[] =
new PersistentCacheEntry(
249 $trimmedGroupSyncResponse,
251 self::GROUP_ERROR_TAG
254 $this->cache->set( ...$entriesToSave );
262 $groupsInSyncEntries = $this->cache->getByTag( self::GROUP_ERROR_TAG );
265 foreach ( $groupsInSyncEntries as $entry ) {
266 $groupResponse = $entry->value();
268 $groupIds[] = $groupResponse->getGroupId();
271 throw $this->invalidArgument( $groupResponse, GroupSynchronizationResponse::class );
280 $groupMessageErrorTag = $this->getGroupMessageErrorTag( $groupId );
281 $groupMessageEntries = $this->cache->getByTag( $groupMessageErrorTag );
283 $groupErrorKey = $this->getGroupErrorKey( $groupId );
284 $groupResponseEntry = $this->cache->get( $groupErrorKey );
285 $groupResponse = $groupResponseEntry[0] ? $groupResponseEntry[0]->value() :
null;
286 if ( $groupResponse ) {
289 throw $this->invalidArgument( $groupResponse, GroupSynchronizationResponse::class );
292 throw new LogicException(
'Requested to fetch errors for a group that has no errors.' );
296 foreach ( $groupMessageEntries as $messageEntries ) {
297 $messageParam = $messageEntries->value();
298 if ( $messageParam instanceof MessageUpdateParameter ) {
299 $messageParams[] = $messageParam;
302 throw $this->invalidArgument( $messageParam, MessageUpdateParameter::class );
306 return new GroupSynchronizationResponse(
309 $groupResponse->hasTimedOut()
315 $groupSyncResponse = $this->getGroupErrorInfo( $groupId );
318 $errorMessageKeys = [];
319 foreach ( $errorMessages as $message ) {
320 $errorMessageKeys[] = $this->getMessageErrorKey( $groupId, $message->getPageName() )[0];
323 $this->cache->delete( ...$errorMessageKeys );
324 return $this->syncGroupErrors( $groupId );
329 $messageErrorKey = $this->getMessageErrorKey( $groupId, $messagePageName )[0];
330 $messageInCache = $this->cache->get( $messageErrorKey );
331 if ( !$messageInCache ) {
332 throw new InvalidArgumentException(
333 'Message does not appear to have synchronization errors'
337 $this->cache->delete( $messageErrorKey );
342 $groupErrorKey = $this->getGroupErrorKey( $groupId );
343 return $this->cache->has( $groupErrorKey );
348 $groupSyncResponse = $this->getGroupErrorInfo( $groupId );
349 if ( $groupSyncResponse->getRemainingMessages() ) {
350 return $groupSyncResponse;
354 $groupErrorKey = $this->getGroupErrorKey( $groupId );
355 $this->cache->delete( $groupErrorKey );
357 return $groupSyncResponse;
360 public function markGroupAsInReview(
string $groupId ): void {
361 $groupReviewKey = $this->getGroupReviewKey( $groupId );
363 new PersistentCacheEntry(
367 self::GROUP_IN_REVIEW_TAG
372 public function markGroupAsReviewed(
string $groupId ): void {
373 $groupReviewKey = $this->getGroupReviewKey( $groupId );
374 $this->cache->delete( $groupReviewKey );
377 public function isGroupInReview(
string $groupId ): bool {
378 return $this->cache->has( $this->getGroupReviewKey( $groupId ) );
381 public function extendGroupExpiryTime(
string $groupId ): void {
382 $groupKey = $this->getGroupKey( $groupId );
383 $groupEntry = $this->cache->get( $groupKey );
385 if ( $groupEntry === [] ) {
387 throw new LogicException(
388 'Requested extension of expiry time for a group that is not being processed. ' .
389 'Check if group is being processed by calling isGroupBeingProcessed() first'
393 if ( $groupEntry[0]->hasExpired() ) {
394 throw new InvalidArgumentException(
395 'Cannot extend expiry time for a group that has already expired.'
399 $newExpiryTime = $this->getExpireTime( $this->incrementalTimeoutSeconds );
403 if ( $newExpiryTime < $groupEntry[0]->exptime() ) {
407 $this->cache->setExpiry( $groupKey, $newExpiryTime );
411 public function getGroupExpiryTime( $groupId ): int {
412 $groupKey = $this->getGroupKey( $groupId );
413 $groupEntry = $this->cache->get( $groupKey );
414 if ( $groupEntry === [] ) {
415 throw new InvalidArgumentException(
"$groupId currently not in processing!" );
418 return $groupEntry[0]->exptime();
421 private function hasGroupTimedOut(
int $syncExpTime ): bool {
422 return ( new DateTime() )->getTimestamp() > $syncExpTime;
425 private function getExpireTime(
int $timeoutSeconds ): int {
426 $currentTime = ( new DateTime() )->getTimestamp();
427 $expTime = (
new DateTime() )
428 ->setTimestamp( $currentTime + $timeoutSeconds )
434 private function invalidArgument( $value,
string $expectedType ): RuntimeException {
435 $valueType = $value ? get_class( $value ) : gettype( $value );
436 return new RuntimeException(
"Expected $expectedType, got $valueType" );
441 private function getGroupTag(
string $groupId ): string {
445 private function getGroupKey(
string $groupId ): string {
446 $hash = substr( hash(
'sha256', $groupId ), 0, 40 );
447 return substr(
"{$hash}_$groupId", 0, 255 );
451 private function getMessageKeys(
string $groupId,
string ...$messages ): array {
453 foreach ( $messages as $message ) {
454 $key = $groupId .
'_' . $message;
455 $hash = substr( hash(
'sha256', $key ), 0, 40 );
456 $finalKey = substr( $hash .
'_' . $key, 0, 255 );
457 $messageKeys[] = $finalKey;
463 private function getGroupErrorKey(
string $groupId ): string {
464 $hash = substr( hash(
'sha256', $groupId ), 0, 40 );
465 return substr(
"{$hash}_gsc_error_$groupId", 0, 255 );
469 private function getMessageErrorKey(
string $groupId,
string ...$messages ): array {
471 foreach ( $messages as $message ) {
472 $key = $groupId .
'_' . $message;
473 $hash = substr( hash(
'sha256', $key ), 0, 40 );
474 $finalKey = substr( $hash .
'_gsc_error_' . $key, 0, 255 );
475 $messageKeys[] = $finalKey;
481 private function getGroupMessageErrorTag(
string $groupId ): string {
482 return "gsc_%error%_$groupId";
485 private function getGroupReviewKey(
string $groupId ): string {
486 $hash = substr( hash(
'sha256', $groupId ), 0, 40 );
487 return substr(
"{$hash}_gsc_%review%_$groupId", 0, 255 );
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
Represents a single result from the persistent cache.