Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
UpdateMessageBundleJob
0.00% covered (danger)
0.00%
0 / 69
0.00% covered (danger)
0.00%
0 / 4
90
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 newJob
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 run
0.00% covered (danger)
0.00%
0 / 56
0.00% covered (danger)
0.00%
0 / 1
20
 shouldFuzzy
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2declare( strict_types = 1 );
3
4namespace MediaWiki\Extension\Translate\MessageBundleTranslation;
5
6use Job;
7use MediaWiki\Extension\Translate\MessageGroupProcessing\MessageGroups;
8use MediaWiki\Extension\Translate\Services;
9use MediaWiki\Logger\LoggerFactory;
10use MediaWiki\MediaWikiServices;
11use MediaWiki\Title\Title;
12use MessageGroupStats;
13use MessageIndexRebuildJob;
14use MessageUpdateJob;
15
16/**
17 * @author Niklas Laxström
18 * @license GPL-2.0-or-later
19 * @since 2021.12
20 */
21class UpdateMessageBundleJob extends Job {
22    /** @inheritDoc */
23    public function __construct( Title $title, $params = [] ) {
24        parent::__construct( 'UpdateMessageBundle', $title, $params );
25    }
26
27    public static function newJob( Title $bundlePageTitle, int $revisionId, ?int $previousRevisionId ): self {
28        return new self(
29            $bundlePageTitle,
30            [
31                'revisionId' => $revisionId,
32                'previousRevisionId' => $previousRevisionId,
33            ]
34        );
35    }
36
37    /** @inheritDoc */
38    public function run(): bool {
39        $mwInstance = MediaWikiServices::getInstance();
40        $lb = $mwInstance->getDBLoadBalancerFactory();
41        $jobQueue = $mwInstance->getJobQueueGroup();
42        $logger = LoggerFactory::getInstance( 'Translate.MessageBundle' );
43        $messageIndex = Services::getInstance()->getMessageIndex();
44
45        $logger->info( 'UpdateMessageBundleJob: Starting job for: ' . $this->getTitle()->getPrefixedText() );
46
47        // Not sure if this is necessary, but it should ensure that this job, which was created
48        // when a revision was saved, can read that revision from the replica. In addition, this
49        // may potentially do a bunch of more writes that could cause more replication lag.
50        if ( !$lb->waitForReplication() ) {
51            $logger->warning( 'UpdateMessageBundleJob: Continuing despite replication lag' );
52        }
53
54        // Setup
55        $bundlePageTitle = $this->getTitle();
56        $name = $bundlePageTitle->getPrefixedText();
57        $pageId = $bundlePageTitle->getId();
58        $groupId = MessageBundleMessageGroup::getGroupId( $name );
59        $params = $this->getParams();
60        // We don't care about the group description or label, so no need to pass it through
61        $group = new MessageBundleMessageGroup(
62            $groupId, $name, $pageId, $params['revisionId'], null, null
63        );
64        $messages = $group->getDefinitions();
65        $previousMessages = [];
66        if ( $params['previousRevisionId'] ) {
67            $groupPreviousVersion = new MessageBundleMessageGroup(
68                $groupId, $name, $pageId, $params['previousRevisionId'], null, null
69            );
70            $previousMessages = $groupPreviousVersion->getDefinitions();
71        }
72
73        // Fill in the front-cache. Ideally this should be done right away, but hopefully
74        // this is okay since we only trigger message group cache rebuild later in this job.
75        // It's possible that some other change triggers it earlier and makes the new group
76        // available before this step is complete.
77        $newKeys = array_diff( array_keys( $messages ), array_keys( $previousMessages ) );
78        $messageIndex->storeInterim( $group, $newKeys );
79
80        // Create jobs that will update the '/' source language pages. These pages should
81        // exist so that the editor can show differences for changed messages. Also compare
82        // against previous version (if any) to determine whether to mark translations as
83        // outdated. There is no support for renames.
84        $jobs = [];
85        $namespace = $group->getNamespace();
86        $code = $group->getSourceLanguage();
87        foreach ( $messages as $key => $value ) {
88            $title = Title::makeTitle( $namespace, "$key/$code" );
89            $fuzzy = $this->shouldFuzzy( $previousMessages, $newKeys, $key, $value );
90            $jobs[] = MessageUpdateJob::newJob( $title, $value, $fuzzy );
91        }
92        $jobQueue->push( $jobs );
93        $logger->info(
94            'UpdateMessageBundleJob: Added {number} MessageUpdateJobs to the job queue for: {title}',
95            [
96                'number' => count( $jobs ),
97                'title' => $name
98            ]
99        );
100
101        // This is somewhat slow, so it has been postponed until now, but it's needed to
102        // make the group available for the message index rebuild.
103        MessageGroups::singleton()->recache();
104
105        $logger->info(
106            'UpdateMessageBundleJob: {title}: Recaching message groups',
107            [ 'title' => $name ]
108        );
109
110        // Schedule message index update. Thanks to front caching, it is okay if this takes
111        // a while (and on large wikis it does take a while!). Running it as a separate job
112        // also allows de-duplication.
113        $job = MessageIndexRebuildJob::newJob();
114        $jobQueue->push( $job );
115        $logger->info(
116            'UpdateMessageBundleJob: {title}: Queue MessageIndexRebuildJob',
117            [ 'title' => $name ]
118        );
119
120        // Refresh or fill translations statistics. If this a new group, this prevents
121        // calculating the stats on the fly during read requests. If an existing group, this
122        // makes sure that the statistics are up-to-date.
123        MessageGroupStats::forGroup(
124            $groupId,
125            MessageGroupStats::FLAG_NO_CACHE | MessageGroupStats::FLAG_IMMEDIATE_WRITES
126        );
127
128        return true;
129    }
130
131    private function shouldFuzzy(
132        ?array $previousMessages,
133        array $newKeys,
134        string $key,
135        string $value
136    ): bool {
137        // Mark new keys as fuzzy
138        if ( in_array( $key, $newKeys ) ) {
139            return true;
140        }
141
142        $previousValue = $previousMessages[$key] ?? null;
143        return $previousMessages !== null && $previousValue !== $value;
144    }
145}