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