Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
TtmServerMessageUpdateJob.php
1<?php
2declare ( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\TtmServer;
5
6use Exception;
7use Job;
8use JobQueueGroup;
12use MediaWiki\Logger\LoggerFactory;
13use MediaWiki\MediaWikiServices;
14use MediaWiki\Title\Title;
15use Psr\Log\LoggerInterface;
16
33class TtmServerMessageUpdateJob extends Job {
38 protected const MAX_ERROR_RETRY = 4;
39
45 protected const WRITE_BACKOFF_EXPONENT = 7;
46 private JobQueueGroup $jobQueueGroup;
47 private const CHANNEL_NAME = 'Translate.TtmServerUpdates';
48 private LoggerInterface $logger;
49
50 public static function newJob( MessageHandle $handle, string $command ): self {
51 return new self( $handle->getTitle(), [ 'command' => $command ] );
52 }
53
54 public function __construct( Title $title, array $params = [] ) {
55 parent::__construct(
56 'TtmServerMessageUpdateJob',
57 $title,
58 $params + [
59 'command' => 'rebuild',
60 'service' => null,
61 'errorCount' => 0,
62 ]
63 );
64
65 $this->jobQueueGroup = MediaWikiServices::getInstance()->getJobQueueGroup();
66 $this->logger = LoggerFactory::getInstance( self::CHANNEL_NAME );
67 }
68
70 public function run(): bool {
71 $services = $this->getServersToUpdate( $this->params['service'] );
72 foreach ( $services as $serviceId => $service ) {
73 $this->runCommandWithRetry( $service, $serviceId );
74 }
75 return true;
76 }
77
79 public function allowRetries() {
80 return false;
81 }
82
84 private function runCommandWithRetry( WritableTtmServer $ttmServer, string $serviceName ): void {
85 try {
86 $this->runCommand( $ttmServer, $serviceName );
87 } catch ( Exception $e ) {
88 $this->requeueError( $serviceName, $e );
89 }
90 }
91
96 private function requeueError( string $serviceName, Exception $e ): void {
97 $this->logger->warning(
98 'Exception thrown while running {command} on ' .
99 'service {service}: {errorMessage}',
100 [
101 'command' => $this->params['command'],
102 'service' => $serviceName,
103 'errorMessage' => $e->getMessage(),
104 'exception' => $e,
105 ]
106 );
107 if ( $this->params['errorCount'] >= self::MAX_ERROR_RETRY ) {
108 $this->logger->warning(
109 'Dropping failing job {command} for service {service} ' .
110 'after repeated failure',
111 [
112 'command' => $this->params['command'],
113 'service' => $serviceName,
114 ]
115 );
116 return;
117 }
118
119 $delay = self::backoffDelay( $this->params['errorCount'] );
120 $job = clone $this;
121 $job->params['errorCount']++;
122 $job->params['service'] = $serviceName;
123 $job->setDelay( $delay );
124 $this->logger->info(
125 'Update job reported failure on service {service}. ' .
126 'Re-queueing job with delay of {delay}.',
127 [
128 'service' => $serviceName,
129 'delay' => $delay
130 ]
131 );
132 $this->resend( $job );
133 }
134
136 protected function resend( self $job ): void {
137 $this->jobQueueGroup->push( $job );
138 }
139
140 private function runCommand( WritableTtmServer $ttmServer, string $serverName ): void {
141 $handle = $this->getHandle();
142 $command = $this->params['command'];
143
144 if ( $command === 'delete' ) {
145 $this->updateItem( $ttmServer, $handle, null, false );
146 } elseif ( $command === 'rebuild' ) {
147 $this->updateMessage( $ttmServer, $handle );
148 } elseif ( $command === 'refresh' ) {
149 $this->updateTranslation( $ttmServer, $handle );
150 }
151
152 $this->logger->info(
153 "{command} command completed on {server} for {handle}",
154 [
155 'command' => $command,
156 'server' => $serverName,
157 'handle' => $handle->getTitle()->getPrefixedText()
158 ]
159 );
160 }
161
163 protected function getHandle(): MessageHandle {
164 return new MessageHandle( $this->title );
165 }
166
168 protected function getTranslation( MessageHandle $handle ): ?string {
169 return Utilities::getMessageContent(
170 $handle->getKey(),
171 $handle->getCode(),
172 $handle->getTitle()->getNamespace()
173 );
174 }
175
176 private function updateMessage( WritableTtmServer $ttmServer, MessageHandle $handle ): void {
177 // Base page update, e.g. group change. Update everything.
178 $translations = Utilities::getTranslations( $handle );
179 foreach ( $translations as $page => $data ) {
180 $tTitle = Title::makeTitle( $this->title->getNamespace(), $page );
181 $tHandle = new MessageHandle( $tTitle );
182 $this->updateItem( $ttmServer, $tHandle, $data[0], $tHandle->isFuzzy() );
183 }
184 }
185
186 private function updateTranslation( WritableTtmServer $ttmServer, MessageHandle $handle ): void {
187 // Update only this translation
188 $translation = $this->getTranslation( $handle );
189 $this->updateItem( $ttmServer, $handle, $translation, $handle->isFuzzy() );
190 }
191
192 private function updateItem(
193 WritableTtmServer $ttmServer,
194 MessageHandle $handle,
195 ?string $text,
196 bool $fuzzy
197 ): void {
198 if ( $fuzzy ) {
199 $text = null;
200 }
201 $ttmServer->update( $handle, $text );
202 }
203
205 private function getServersToUpdate( ?string $requestedServiceId ): array {
206 $ttmServerFactory = Services::getInstance()->getTtmServerFactory();
207 if ( $requestedServiceId ) {
208 if ( !$ttmServerFactory->has( $requestedServiceId ) ) {
209 $this->logger->warning(
210 'Received update job for an unknown service {service}',
211 [ 'service' => $requestedServiceId ]
212 );
213 return [];
214 }
215
216 $ttmServer = $ttmServerFactory->create( $requestedServiceId );
217 if ( !$ttmServer instanceof WritableTtmServer ) {
218 $this->logger->warning(
219 'Received update job for a non writable ttm service {service}',
220 [ 'service' => $requestedServiceId ]
221 );
222 return [];
223 }
224 return [ $requestedServiceId => $ttmServer ];
225 }
226
227 try {
228 return $ttmServerFactory->getWritable();
229 } catch ( Exception $e ) {
230 $this->logger->error(
231 'There was an error while fetching writable TTM services. Error: {error}',
232 [ 'error' => $e->getMessage() ]
233 );
234 }
235
236 return [];
237 }
238
250 public function setDelay( int $delay ): void {
251 $jobQueue = $this->jobQueueGroup->get( $this->getType() );
252 if ( !$delay || !$jobQueue->delayedJobsEnabled() ) {
253 return;
254 }
255 $oldTime = $this->getReleaseTimestamp();
256 $newTime = time() + $delay;
257 if ( $oldTime !== null && $oldTime >= $newTime ) {
258 return;
259 }
260 $this->params[ 'jobReleaseTimestamp' ] = $newTime;
261 }
262
269 public static function backoffDelay( int $errorCount ): int {
270 return (int)ceil( pow(
271 2,
272 static::WRITE_BACKOFF_EXPONENT + rand( 0, min( $errorCount, 4 ) )
273 ) );
274 }
275}
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
Class for pointing to messages, like Title class is for titles.
Minimal service container.
Definition Services.php:59
getTranslation(MessageHandle $handle)
Extracted for testing purpose.
const MAX_ERROR_RETRY
Number of retries allowed, 4 means we attempt to run the job 5 times (1 initial attempt + 4 retries).
Essentially random collection of helper functions, similar to GlobalFunctions.php.
Definition Utilities.php:31
Interface for TtmServer that can be updated.
update(MessageHandle $handle, ?string $targetText)
Shovels the new translation into translation memory.