Translate extension for MediaWiki
 
Loading...
Searching...
No Matches
UpdateTranslatablePageJob.php
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
11use MediaWiki\MediaWikiServices;
12use MediaWiki\Title\Title;
13use RunnableJob;
14
20 private const MAX_TRIES = 3;
21
23 public function __construct( Title $title, array $params = [] ) {
24 parent::__construct( 'UpdateTranslatablePageJob', $title, $params );
25 }
26
36 public static function newFromPage( TranslatablePage $page, array $sections = [] ): self {
37 $params = [];
38 $params['sections'] = [];
39 foreach ( $sections as $section ) {
40 $params['sections'][] = $section->serializeToArray();
41 }
42
43 return new self( $page->getTitle(), $params );
44 }
45
46 public function run(): bool {
47 // WARNING: Nothing here must not depend on message index being up to date.
48 // For performance reasons, message index rebuild is run a separate job after
49 // everything else is updated.
50
51 // START: This section does not care about replication lag
52 $this->logInfo( 'Starting UpdateTranslatablePageJob' );
53
54 $sections = $this->params['sections'];
55 foreach ( $sections as $index => $section ) {
56 // Old jobs stored sections as objects because they were serialized and
57 // unserialized transparently. That is no longer supported, so we
58 // convert manually to primitive types first (to an PHP array).
59 if ( is_array( $section ) ) {
60 $sections[$index] = TranslationUnit::unserializeFromArray( $section );
61 }
62 }
63
68 $page = TranslatablePage::newFromTitle( $this->title );
69 $unitJobs = self::getTranslationUnitJobs( $page, $sections );
70 foreach ( $unitJobs as $job ) {
71 $job->run();
72 }
73
74 $this->logInfo(
75 'Finished running ' . count( $unitJobs ) . ' MessageUpdate jobs for '
76 . count( $sections ) . ' sections'
77 );
78 // END: This section does not care about replication lag
79 $mwServices = MediaWikiServices::getInstance();
80 $lb = $mwServices->getDBLoadBalancerFactory();
81 if ( !$lb->waitForReplication() ) {
82 $this->logWarning( 'Continuing despite replication lag' );
83 }
84
85 $attemptsCount = 0;
86 do {
87 // Ensure we are using the latest group definitions. This is needed so long-running
88 // scripts detect the page which was just marked for translation. Otherwise, getMessageGroup
89 // in the next line returns null. There is no need to regenerate the global cache.
90 MessageGroups::singleton()->clearProcessCache();
91 // Ensure fresh definitions for stats
92
93 // Message group may return null due to stale caches, attempt to fetch the group a few
94 // times before giving up.
95 $messageGroup = $page->getMessageGroup();
96 ++$attemptsCount;
97 if ( $messageGroup ) {
98 break;
99 }
100
101 // The message group cache regen time on production is around 600ms
102 usleep( 500 * 1000 );
103 } while ( $attemptsCount <= self::MAX_TRIES );
104
105 if ( $messageGroup ) {
106 $messageGroup->clearCaches();
107 $this->logInfo(
108 'Cleared caches after {attemptsCount} attempt(s)',
109 [ 'attemptsCount' => $attemptsCount ]
110 );
111 } else {
112 $this->logWarning(
113 'No message group found for page {pageTitle} after {attemptsCount} attempt(s)',
114 [
115 'pageTitle' => $page->getTitle()->getPrefixedText(),
116 'attemptsCount' => self::MAX_TRIES
117 ]
118 );
119 }
120
121 // Refresh translations statistics, we want these to be up to date for the
122 // RenderJobs, for displaying up to date statistics on the translation pages.
123 $id = $page->getMessageGroupId();
124 MessageGroupStats::forGroup(
125 $id,
126 MessageGroupStats::FLAG_NO_CACHE | MessageGroupStats::FLAG_IMMEDIATE_WRITES
127 );
128 $this->logInfo( 'Updated the message group stats' );
129
130 // Try to avoid stale statistics on the base page
131 $wikiPage = $mwServices->getWikiPageFactory()->newFromTitle( $page->getTitle() );
132 $wikiPage->doPurge();
133 $this->logInfo( 'Finished purging' );
134
135 // These can be run independently and in parallel if possible
136 $jobQueueGroup = $mwServices->getJobQueueGroup();
137 $renderJobs = self::getRenderJobs( $page );
138 $jobQueueGroup->push( $renderJobs );
139 $this->logInfo( 'Added ' . count( $renderJobs ) . ' RenderJobs to the queue' );
140
141 // Schedule message index update. Thanks to front caching, it is okay if this takes
142 // a while (and on large wikis it does take a while!). Running it as a separate job
143 // also allows de-duplication in case multiple translatable pages are being marked
144 // for translation in a short period of time.
145 $job = RebuildMessageIndexJob::newJob();
146 $jobQueueGroup->push( $job );
147
148 $this->logInfo( 'Finished UpdateTranslatablePageJob' );
149
150 return true;
151 }
152
159 private static function getTranslationUnitJobs( TranslatablePage $page, array $units ): array {
160 $jobs = [];
161
162 $code = $page->getSourceLanguageCode();
163 $prefix = $page->getTitle()->getPrefixedText();
164
165 foreach ( $units as $unit ) {
166 $unitName = $unit->id;
167 $title = Title::makeTitle( NS_TRANSLATIONS, "$prefix/$unitName/$code" );
168
169 $fuzzy = $unit->type === 'changed';
170 $jobs[] = UpdateMessageJob::newJob( $title, $unit->getTextWithVariables(), $fuzzy );
171 }
172
173 return $jobs;
174 }
175
180 public static function getRenderJobs( TranslatablePage $page, bool $nonPrioritizedJobs = false ): array {
181 $documentationLanguageCode = MediaWikiServices::getInstance()
182 ->getMainConfig()
183 ->get( 'TranslateDocumentationLanguageCode' );
184
185 $jobs = [];
186
187 $jobTitles = $page->getTranslationPages();
188 // Ensure that we create the source language page when page is marked for translation.
189 $jobTitles[] = $page->getTitle()->getSubpage( $page->getSourceLanguageCode() );
190 // In some cases translation page may be missing even though translations exist. One such case
191 // is when FuzzyBot makes edits, which suppresses render jobs. There may also be bugs with the
192 // render jobs failing. Add jobs based on message group stats to create self-healing process.
193 $stats = MessageGroupStats::forGroup( $page->getMessageGroupId() );
194 foreach ( $stats as $languageCode => $languageStats ) {
195 if ( $languageStats[MessageGroupStats::TRANSLATED] > 0 && $languageCode !== $documentationLanguageCode ) {
196 $jobTitles[] = $page->getTitle()->getSubpage( $languageCode );
197 }
198 }
199
200 // These jobs can be deduplicated by the job queue as well, but it's simple to do it here ourselves.
201 // Titles have __toString method that returns the prefixed text so array_unique should work.
202 $jobTitles = array_unique( $jobTitles );
203 foreach ( $jobTitles as $t ) {
204 if ( $nonPrioritizedJobs ) {
205 $jobs[] = RenderTranslationPageJob::newNonPrioritizedJob( $t );
206 } else {
207 $jobs[] = RenderTranslationPageJob::newJob( $t );
208 }
209
210 }
211
212 return $jobs;
213 }
214}
Factory class for accessing message groups individually by id or all of them as a list.
Mixed bag of methods related to translatable pages.
static newFromTitle(PageIdentity $title)
Constructs a translatable page from title.
getSourceLanguageCode()
Returns the source language of this translatable page.
getMessageGroup()
Returns MessageGroup used for translating this page.
Job for updating translation units and translation pages when a translatable page is marked for trans...
static getRenderJobs(TranslatablePage $page, bool $nonPrioritizedJobs=false)
Creates jobs needed to create or update all translation pages.
static newFromPage(TranslatablePage $page, array $sections=[])
Create a job that updates a translation page.
This class aims to provide efficient mechanism for fetching translation completion stats.
Job for updating translation pages when translation or message definition changes.