Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateTranslatablePageJob
0.00% covered (danger)
0.00%
0 / 68
0.00% covered (danger)
0.00%
0 / 5
272
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newFromPage
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 run
0.00% covered (danger)
0.00%
0 / 37
0.00% covered (danger)
0.00%
0 / 1
30
 getTranslationUnitJobs
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
6
 getRenderJobs
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\PageTranslation;
5
6use MediaWiki\Extension\Translate\Jobs\GenericTranslateJob;
7use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
8use MediaWiki\MediaWikiServices;
9use MediaWiki\Title\Title;
10use MessageGroupStats;
11use MessageIndexRebuildJob;
12use MessageUpdateJob;
13use RunnableJob;
14
15/**
16 * Job for updating translation units and translation pages when
17 * a translatable page is marked for translation.
18 */
19class UpdateTranslatablePageJob extends GenericTranslateJob {
20    /** @inheritDoc */
21    public function __construct( Title $title, array $params = [] ) {
22        parent::__construct( 'UpdateTranslatablePageJob', $title, $params );
23    }
24
25    /**
26     * Create a job that updates a translation page.
27     *
28     * If a list of sections is provided, then the job will also update translation
29     * unit pages.
30     *
31     * @param TranslatablePage $page
32     * @param TranslationUnit[] $sections
33     */
34    public static function newFromPage( TranslatablePage $page, array $sections = [] ): self {
35        $params = [];
36        $params[ 'sections' ] = [];
37        foreach ( $sections as $section ) {
38            $params[ 'sections' ][] = $section->serializeToArray();
39        }
40
41        return new self( $page->getTitle(), $params );
42    }
43
44    public function run(): bool {
45        // WARNING: Nothing here must not depend on message index being up to date.
46        // For performance reasons, message index rebuild is run a separate job after
47        // everything else is updated.
48
49        // START: This section does not care about replication lag
50        $this->logInfo( 'Starting UpdateTranslatablePageJob' );
51
52        $sections = $this->params[ 'sections' ];
53        foreach ( $sections as $index => $section ) {
54            // Old jobs stored sections as objects because they were serialized and
55            // unserialized transparently. That is no longer supported, so we
56            // convert manually to primitive types first (to an PHP array).
57            if ( is_array( $section ) ) {
58                $sections[ $index ] = TranslationUnit::unserializeFromArray( $section );
59            }
60        }
61
62        /**
63         * Units should be updated before the render jobs are run so that the
64         * latest changes can take effect on the translation pages.
65         */
66        $page = TranslatablePage::newFromTitle( $this->title );
67        $unitJobs = self::getTranslationUnitJobs( $page, $sections );
68        foreach ( $unitJobs as $job ) {
69            $job->run();
70        }
71
72        $this->logInfo(
73            'Finished running ' . count( $unitJobs ) . ' MessageUpdate jobs for '
74            . count( $sections ) . ' sections'
75        );
76        // END: This section does not care about replication lag
77        $mwServices = MediaWikiServices::getInstance();
78        $lb = $mwServices->getDBLoadBalancerFactory();
79        if ( !$lb->waitForReplication() ) {
80            $this->logWarning( 'Continuing despite replication lag' );
81        }
82
83        // Ensure we are using the latest group definitions. This is needed so
84        // that in long running scripts we do see the page which was just
85        // marked for translation. Otherwise getMessageGroup in the next line
86        // returns null. There is no need to regenerate the global cache.
87        MessageGroups::singleton()->clearProcessCache();
88        // Ensure fresh definitions for stats
89        $page->getMessageGroup()->clearCaches();
90
91        $this->logInfo( 'Cleared caches' );
92
93        // Refresh translations statistics, we want these to be up to date for the
94        // RenderJobs, for displaying up to date statistics on the translation pages.
95        $id = $page->getMessageGroupId();
96        MessageGroupStats::forGroup(
97            $id,
98            MessageGroupStats::FLAG_NO_CACHE | MessageGroupStats::FLAG_IMMEDIATE_WRITES
99        );
100        $this->logInfo( 'Updated the message group stats' );
101
102        // Try to avoid stale statistics on the base page
103        $wikiPage = $mwServices->getWikiPageFactory()->newFromTitle( $page->getTitle() );
104        $wikiPage->doPurge();
105        $this->logInfo( 'Finished purging' );
106
107        // These can be run independently and in parallel if possible
108        $jobQueueGroup = $mwServices->getJobQueueGroup();
109        $renderJobs = self::getRenderJobs( $page );
110        $jobQueueGroup->push( $renderJobs );
111        $this->logInfo( 'Added ' . count( $renderJobs ) . ' RenderJobs to the queue' );
112
113        // Schedule message index update. Thanks to front caching, it is okay if this takes
114        // a while (and on large wikis it does take a while!). Running it as a separate job
115        // also allows de-duplication in case multiple translatable pages are being marked
116        // for translation in a short period of time.
117        $job = MessageIndexRebuildJob::newJob();
118        $jobQueueGroup->push( $job );
119
120        $this->logInfo( 'Finished UpdateTranslatablePageJob' );
121
122        return true;
123    }
124
125    /**
126     * Creates jobs needed to create or update all translation unit definition pages.
127     * @param TranslatablePage $page
128     * @param TranslationUnit[] $units
129     * @return RunnableJob[]
130     */
131    private static function getTranslationUnitJobs( TranslatablePage $page, array $units ): array {
132        $jobs = [];
133
134        $code = $page->getSourceLanguageCode();
135        $prefix = $page->getTitle()->getPrefixedText();
136
137        foreach ( $units as $unit ) {
138            $unitName = $unit->id;
139            $title = Title::makeTitle( NS_TRANSLATIONS, "$prefix/$unitName/$code" );
140
141            $fuzzy = $unit->type === 'changed';
142            $jobs[] = MessageUpdateJob::newJob( $title, $unit->getTextWithVariables(), $fuzzy );
143        }
144
145        return $jobs;
146    }
147
148    /**
149     * Creates jobs needed to create or update all translation pages.
150     * @return RunnableJob[]
151     */
152    public static function getRenderJobs( TranslatablePage $page, bool $nonPrioritizedJobs = false ): array {
153        $documentationLanguageCode = MediaWikiServices::getInstance()
154            ->getMainConfig()
155            ->get( 'TranslateDocumentationLanguageCode' );
156
157        $jobs = [];
158
159        $jobTitles = $page->getTranslationPages();
160        // Ensure that we create the source language page when page is marked for translation.
161        $jobTitles[] = $page->getTitle()->getSubpage( $page->getSourceLanguageCode() );
162        // In some cases translation page may be missing even though translations exist. One such case
163        // is when FuzzyBot makes edits, which suppresses render jobs. There may also be bugs with the
164        // render jobs failing. Add jobs based on message group stats to create self-healing process.
165        $stats = MessageGroupStats::forGroup( $page->getMessageGroupId() );
166        foreach ( $stats as $languageCode => $languageStats ) {
167            if ( $languageStats[MessageGroupStats::TRANSLATED] > 0 && $languageCode !== $documentationLanguageCode ) {
168                $jobTitles[] = $page->getTitle()->getSubpage( $languageCode );
169            }
170        }
171
172        // These jobs can be deduplicated by the job queue as well, but it's simple to do it here ourselves.
173        // Titles have __toString method that returns the prefixed text so array_unique should work.
174        $jobTitles = array_unique( $jobTitles );
175        foreach ( $jobTitles as $t ) {
176            if ( $nonPrioritizedJobs ) {
177                $jobs[] = RenderTranslationPageJob::newNonPrioritizedJob( $t );
178            } else {
179                $jobs[] = RenderTranslationPageJob::newJob( $t );
180            }
181
182        }
183
184        return $jobs;
185    }
186
187}